林海 1 месяц назад
Сommit
dd671a7ea0
49 измененных файлов с 5541 добавлено и 0 удалено
  1. 14 0
      .gitignore
  2. 70 0
      app.js
  3. 58 0
      app.json
  4. 39 0
      custom-tab-bar/index.js
  5. 4 0
      custom-tab-bar/index.json
  6. 27 0
      custom-tab-bar/index.wxml
  7. 141 0
      custom-tab-bar/index.wxss
  8. BIN
      images/circle-active.png
  9. BIN
      images/circle.png
  10. BIN
      images/home-active.png
  11. BIN
      images/home.png
  12. BIN
      images/tree-active.png
  13. BIN
      images/tree.png
  14. 343 0
      pages/add_member/add_member.js
  15. 3 0
      pages/add_member/add_member.json
  16. 395 0
      pages/add_member/add_member.wxml
  17. 683 0
      pages/add_member/add_member.wxss
  18. 120 0
      pages/family_circle/family_circle.js
  19. 3 0
      pages/family_circle/family_circle.json
  20. 92 0
      pages/family_circle/family_circle.wxml
  21. 270 0
      pages/family_circle/family_circle.wxss
  22. 81 0
      pages/index/index.js
  23. 3 0
      pages/index/index.json
  24. 179 0
      pages/index/index.wxml
  25. 322 0
      pages/index/index.wxss
  26. 221 0
      pages/lineage/lineage.js
  27. 3 0
      pages/lineage/lineage.json
  28. 223 0
      pages/lineage/lineage.wxml
  29. 558 0
      pages/lineage/lineage.wxss
  30. 138 0
      pages/login/login.js
  31. 3 0
      pages/login/login.json
  32. 37 0
      pages/login/login.wxml
  33. 126 0
      pages/login/login.wxss
  34. 81 0
      pages/member_detail/member_detail.js
  35. 3 0
      pages/member_detail/member_detail.json
  36. 157 0
      pages/member_detail/member_detail.wxml
  37. 141 0
      pages/member_detail/member_detail.wxss
  38. 170 0
      pages/my_entries/my_entries.js
  39. 4 0
      pages/my_entries/my_entries.json
  40. 142 0
      pages/my_entries/my_entries.wxml
  41. 370 0
      pages/my_entries/my_entries.wxss
  42. 111 0
      pages/test/test.js
  43. 3 0
      pages/test/test.json
  44. 28 0
      pages/test/test.wxml
  45. 70 0
      pages/test/test.wxss
  46. 24 0
      project.config.json
  47. 14 0
      project.private.config.json
  48. 7 0
      sitemap.json
  49. 60 0
      utils/helpers.wxs

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# Windows
+[Dd]esktop.ini
+Thumbs.db
+$RECYCLE.BIN/
+
+# macOS
+.DS_Store
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+
+# Node.js
+node_modules/

+ 70 - 0
app.js

@@ -0,0 +1,70 @@
+App({
+  globalData: {
+    userInfo: null,
+    token: null,
+    isLoggedIn: false,
+    baseUrl: 'http://192.168.124.37:5001/manager'
+  },
+
+  onLaunch: function () {
+    this.checkLoginStatus();
+  },
+
+  checkLoginStatus: function () {
+    const token = wx.getStorageSync('token');
+    const userInfo = wx.getStorageSync('userInfo');
+    
+    if (token && userInfo) {
+      this.globalData.token = token;
+      this.globalData.userInfo = userInfo;
+      this.globalData.isLoggedIn = true;
+    }
+  },
+
+  login: function (callback) {
+    wx.login({
+      success: (res) => {
+        if (res.code) {
+          wx.request({
+            url: `${this.globalData.baseUrl}/api/wechat/login`,
+            method: 'POST',
+            data: {
+              code: res.code
+            },
+            success: (response) => {
+              if (response.data.success) {
+                this.globalData.token = response.data.token;
+                this.globalData.userInfo = response.data.user;
+                this.globalData.isLoggedIn = true;
+                
+                wx.setStorageSync('token', response.data.token);
+                wx.setStorageSync('userInfo', response.data.user);
+                
+                if (callback) callback(null, response.data);
+              } else {
+                if (callback) callback(new Error(response.data.message));
+              }
+            },
+            fail: (err) => {
+              if (callback) callback(err);
+            }
+          });
+        } else {
+          if (callback) callback(new Error('登录失败'));
+        }
+      },
+      fail: (err) => {
+        if (callback) callback(err);
+      }
+    });
+  },
+
+  logout: function () {
+    this.globalData.token = null;
+    this.globalData.userInfo = null;
+    this.globalData.isLoggedIn = false;
+    
+    wx.removeStorageSync('token');
+    wx.removeStorageSync('userInfo');
+  }
+});

+ 58 - 0
app.json

@@ -0,0 +1,58 @@
+{
+  "pages": [
+    "pages/index/index",
+    "pages/test/test",
+    "pages/login/login",
+    "pages/add_member/add_member",
+    "pages/lineage/lineage",
+    "pages/member_detail/member_detail",
+    "pages/family_circle/family_circle",
+    "pages/my_entries/my_entries"
+  ],
+  "window": {
+    "backgroundTextStyle": "light",
+    "navigationBarBackgroundColor": "#ffffff",
+    "navigationBarTitleText": "留家族",
+    "navigationBarTextStyle": "black",
+    "backgroundColor": "#f5f5f5"
+  },
+  "tabBar": {
+    "custom": true,
+    "color": "#999999",
+    "selectedColor": "#8B4513",
+    "borderStyle": "black",
+    "backgroundColor": "#ffffff",
+    "list": [
+      {
+        "pagePath": "pages/index/index",
+        "text": "首页",
+        "iconPath": "images/home.png",
+        "selectedIconPath": "images/home-active.png"
+      },
+      {
+        "pagePath": "pages/lineage/lineage",
+        "text": "世系查询",
+        "iconPath": "images/tree.png",
+        "selectedIconPath": "images/tree-active.png"
+      },
+      {
+        "pagePath": "pages/my_entries/my_entries",
+        "text": "我的录入",
+        "iconPath": "images/tree.png",
+        "selectedIconPath": "images/tree-active.png"
+      },
+      {
+        "pagePath": "pages/family_circle/family_circle",
+        "text": "家族圈",
+        "iconPath": "images/circle.png",
+        "selectedIconPath": "images/circle-active.png"
+      }
+    ]
+  },
+  "permission": {
+    "scope.userLocation": {
+      "desc": "用于获取用户位置"
+    }
+  },
+  "sitemapLocation": "sitemap.json"
+}

+ 39 - 0
custom-tab-bar/index.js

@@ -0,0 +1,39 @@
+Component({
+  data: {
+    selected: 0,
+    tabs: [
+      {
+        pagePath: '/pages/index/index',
+        name: '首页',
+        icon: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
+        iconActive: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'
+      },
+      {
+        pagePath: '/pages/lineage/lineage',
+        name: '世系查询',
+        icon: 'M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z',
+        iconActive: 'M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z'
+      },
+      {
+        pagePath: '/pages/my_entries/my_entries',
+        name: '我的录入',
+        icon: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
+        iconActive: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'
+      },
+      {
+        pagePath: '/pages/family_circle/family_circle',
+        name: '家族圈',
+        icon: 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z',
+        iconActive: 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'
+      }
+    ]
+  },
+
+  methods: {
+    switchTab(e) {
+      const index = e.currentTarget.dataset.index;
+      const tab = this.data.tabs[index];
+      wx.switchTab({ url: tab.pagePath });
+    }
+  }
+});

+ 4 - 0
custom-tab-bar/index.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 27 - 0
custom-tab-bar/index.wxml

@@ -0,0 +1,27 @@
+<view class="tab-bar">
+  <view class="tab-bar-inner">
+    <view
+      class="tab-item {{selected === index ? 'tab-item-active' : ''}}"
+      wx:for="{{tabs}}"
+      wx:key="pagePath"
+      data-index="{{index}}"
+      bindtap="switchTab"
+    >
+      <!-- 激活时顶部指示条 -->
+      <view class="tab-indicator {{selected === index ? 'indicator-show' : ''}}"></view>
+
+      <!-- 图标容器 -->
+      <view class="tab-icon-wrap {{selected === index ? 'icon-wrap-active' : ''}}">
+        <svg-icon wx:if="{{false}}"></svg-icon>
+        <view class="tab-svg-box">
+          <view class="tab-icon-bg {{selected === index ? 'icon-bg-active' : ''}}"></view>
+          <!-- SVG path via background -->
+          <view class="tab-icon-img icon-{{index}} {{selected === index ? 'icon-img-active' : ''}}"></view>
+        </view>
+      </view>
+
+      <!-- 标签文字 -->
+      <text class="tab-label {{selected === index ? 'tab-label-active' : ''}}">{{item.name}}</text>
+    </view>
+  </view>
+</view>

+ 141 - 0
custom-tab-bar/index.wxss

@@ -0,0 +1,141 @@
+/* 整体容器 */
+.tab-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 999;
+  background: transparent;
+  /* 为安全区留空 */
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.tab-bar-inner {
+  display: flex;
+  align-items: stretch;
+  background: #fff;
+  border-radius: 32rpx 32rpx 0 0;
+  box-shadow: 0 -4rpx 24rpx rgba(100, 60, 20, 0.12);
+  border-top: 1rpx solid rgba(200, 168, 130, 0.25);
+  overflow: hidden;
+}
+
+/* 每个 Tab 项 */
+.tab-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 12rpx 0 18rpx;
+  position: relative;
+  transition: all 0.2s ease;
+}
+
+.tab-item:active {
+  opacity: 0.75;
+}
+
+/* 顶部激活条 */
+.tab-indicator {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%) scaleX(0);
+  width: 48rpx;
+  height: 6rpx;
+  border-radius: 0 0 6rpx 6rpx;
+  background: linear-gradient(90deg, #8B4513, #D2691E);
+  transition: transform 0.25s ease;
+}
+
+.indicator-show {
+  transform: translateX(-50%) scaleX(1);
+}
+
+/* 图标区域 */
+.tab-icon-wrap {
+  width: 80rpx;
+  height: 56rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 6rpx;
+  position: relative;
+}
+
+.tab-svg-box {
+  position: relative;
+  width: 52rpx;
+  height: 52rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 激活时的圆形背景 */
+.tab-icon-bg {
+  position: absolute;
+  inset: -8rpx;
+  border-radius: 20rpx;
+  background: transparent;
+  transition: background 0.2s ease;
+}
+
+.icon-bg-active {
+  background: rgba(139, 69, 19, 0.1);
+}
+
+/* 图标本体 — 用 mask + background 实现着色 SVG */
+.tab-icon-img {
+  width: 44rpx;
+  height: 44rpx;
+  background-color: #b0a090;
+  transition: background-color 0.2s ease;
+  -webkit-mask-size: contain;
+  -webkit-mask-repeat: no-repeat;
+  -webkit-mask-position: center;
+  mask-size: contain;
+  mask-repeat: no-repeat;
+  mask-position: center;
+}
+
+.icon-img-active {
+  background-color: #8B4513;
+}
+
+/* 首页 - 房子图标 */
+.icon-0 {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E");
+}
+
+/* 世系查询 - 树形/关系图标 */
+.icon-1 {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z'/%3E%3C/svg%3E");
+}
+
+/* 我的录入 - 编辑图标 */
+.icon-2 {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E");
+}
+
+/* 家族圈 - 人群图标 */
+.icon-3 {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E");
+}
+
+/* 文字标签 */
+.tab-label {
+  font-size: 20rpx;
+  color: #b0a090;
+  transition: color 0.2s ease, font-weight 0.2s ease;
+  letter-spacing: 1rpx;
+}
+
+.tab-label-active {
+  color: #8B4513;
+  font-weight: 700;
+}

BIN
images/circle-active.png


BIN
images/circle.png


BIN
images/home-active.png


BIN
images/home.png


BIN
images/tree-active.png


BIN
images/tree.png


+ 343 - 0
pages/add_member/add_member.js

@@ -0,0 +1,343 @@
+const app = getApp();
+
+Page({
+  data: {
+    formData: {
+      name: '',
+      simplified_name: '',
+      sex: '1',
+      birthday: '',
+      family_rank: '',
+      name_word_generation: '',
+      is_pass_away: 0,
+      marital_status: 0,
+      relation_type: '',
+      sub_relation_type: '',
+      former_name: '',
+      phone: '',
+      notes: ''
+    },
+    selectedMemberName: '',
+    selectedMemberId: '',
+    showDuplicateModal: false,
+    duplicateMembers: [],
+    showMemberDetailModal: false,
+    selectedMemberDetail: null,
+    isPassAwayOptions: ['健在', '已故', '未知'],
+    maritalStatusOptions: ['未知', '未婚', '已婚', '离异/丧偶'],
+    relationTypeOptions: ['父子 (关联人为父)', '母子 (关联人为母)', '夫妻', '兄弟', '姐妹'],
+    subRelationTypeOptions: ['亲生/正妻', '养父', '出继(亲生父母)', '入继(养父母)', '妾', '外室'],
+    showMemberSearch: false,
+    memberSearchKeyword: '',
+    memberSearchResults: [],
+    memberSearchLoading: false
+  },
+
+  onLoad: function () {
+    // 不自动登录,在需要时登录
+  },
+
+  // 输入处理
+  onNameInput: function (e) {
+    this.setData({
+      'formData.name': e.detail.value
+    });
+    // 输入姓名后自动检查同名
+    if (e.detail.value.length >= 2) {
+      this.checkDuplicate(e.detail.value);
+    }
+  },
+
+  onSimplifiedNameInput: function (e) {
+    this.setData({
+      'formData.simplified_name': e.detail.value
+    });
+  },
+
+  selectSex: function (e) {
+    this.setData({
+      'formData.sex': e.currentTarget.dataset.value
+    });
+  },
+
+  onBirthdayChange: function (e) {
+    this.setData({
+      'formData.birthday': e.detail.value
+    });
+  },
+
+  onFamilyRankInput: function (e) {
+    this.setData({
+      'formData.family_rank': e.detail.value
+    });
+  },
+
+  onGenerationInput: function (e) {
+    this.setData({
+      'formData.name_word_generation': e.detail.value
+    });
+  },
+
+  onIsPassAwayChange: function (e) {
+    this.setData({
+      'formData.is_pass_away': parseInt(e.detail.value)
+    });
+  },
+
+  onMaritalStatusChange: function (e) {
+    this.setData({
+      'formData.marital_status': parseInt(e.detail.value)
+    });
+  },
+
+  onRelationTypeChange: function (e) {
+    this.setData({
+      'formData.relation_type': e.detail.value
+    });
+  },
+
+  onSubRelationTypeChange: function (e) {
+    this.setData({
+      'formData.sub_relation_type': e.detail.value
+    });
+  },
+
+  onFormerNameInput: function (e) {
+    this.setData({
+      'formData.former_name': e.detail.value
+    });
+  },
+
+  onPhoneInput: function (e) {
+    this.setData({
+      'formData.phone': e.detail.value
+    });
+  },
+
+  onNotesInput: function (e) {
+    this.setData({
+      'formData.notes': e.detail.value
+    });
+  },
+
+  // 检查同名成员
+  checkDuplicate: function (name) {
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/check_duplicate`,
+      method: 'GET',
+      data: {
+        name: name
+      },
+      success: (res) => {
+        if (res.data.success && res.data.data.length > 0) {
+          this.setData({
+            duplicateMembers: res.data.data,
+            showDuplicateModal: true
+          });
+        }
+      },
+      fail: () => {
+        console.log('检查同名失败');
+      }
+    });
+  },
+
+  // 打开关联成员搜索弹层
+  selectRelatedMember: function () {
+    this.setData({
+      showMemberSearch: true,
+      memberSearchKeyword: '',
+      memberSearchResults: []
+    });
+  },
+
+  closeMemberSearch: function () {
+    this.setData({ showMemberSearch: false });
+  },
+
+  onMemberSearchInput: function (e) {
+    this.setData({ memberSearchKeyword: e.detail.value });
+  },
+
+  doMemberSearch: function () {
+    const keyword = this.data.memberSearchKeyword.trim();
+    if (!keyword) {
+      wx.showToast({ title: '请输入姓名搜索', icon: 'none' });
+      return;
+    }
+    this.setData({ memberSearchLoading: true, memberSearchResults: [] });
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/search`,
+      method: 'GET',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      data: { keyword },
+      success: (res) => {
+        this.setData({ memberSearchLoading: false });
+        if (res.data && res.data.success) {
+          const results = res.data.data || [];
+          if (results.length === 0) {
+            wx.showToast({ title: '未找到匹配成员', icon: 'none' });
+          }
+          this.setData({ memberSearchResults: results });
+        } else {
+          wx.showToast({ title: '搜索失败', icon: 'none' });
+        }
+      },
+      fail: () => {
+        this.setData({ memberSearchLoading: false });
+        wx.showToast({ title: '网络请求失败', icon: 'none' });
+      }
+    });
+  },
+
+  confirmSelectMember: function (e) {
+    const member = e.currentTarget.dataset.member;
+    this.setData({
+      selectedMemberId: member.id,
+      selectedMemberName: member.name + (member.simplified_name ? `(${member.simplified_name})` : ''),
+      showMemberSearch: false
+    });
+  },
+
+  clearSelectedMember: function () {
+    this.setData({ selectedMemberId: '', selectedMemberName: '' });
+  },
+
+  // 关闭同名弹窗
+  closeDuplicateModal: function () {
+    this.setData({
+      showDuplicateModal: false
+    });
+  },
+
+  // 查看成员详情(可从搜索结果或同名列表进入)
+  viewMemberDetail: function (e) {
+    const member = e.currentTarget.dataset.member;
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/${member.id}`,
+      method: 'GET',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        if (res.data && res.data.success) {
+          this.setData({
+            selectedMemberDetail: res.data.data,
+            // 记录是否从搜索弹层打开,用于"选择此人"回填
+            _detailFromSearch: this.data.showMemberSearch,
+            showMemberDetailModal: true,
+            showDuplicateModal: false
+          });
+        } else {
+          wx.showToast({ title: '获取详情失败', icon: 'none' });
+        }
+      },
+      fail: () => {
+        wx.showToast({ title: '网络请求失败', icon: 'none' });
+      }
+    });
+  },
+
+  // 在详情弹层中直接选择此人作为关联成员
+  selectFromDetail: function () {
+    const m = this.data.selectedMemberDetail;
+    if (!m) return;
+    this.setData({
+      selectedMemberId: m.id,
+      selectedMemberName: m.name + (m.simplified_name ? `(${m.simplified_name})` : ''),
+      showMemberDetailModal: false,
+      showMemberSearch: false
+    });
+  },
+
+  // 关闭成员详情弹窗
+  closeMemberDetailModal: function () {
+    this.setData({
+      showMemberDetailModal: false,
+      selectedMemberDetail: null
+    });
+  },
+
+  // 放弃录入
+  abortEntry: function () {
+    wx.navigateBack();
+  },
+
+  // 阻止事件冒泡
+  stopPropagation: function () {},
+
+  // 提交表单
+  submitForm: function () {
+    const { formData } = this.data;
+    
+    // 验证必填字段
+    if (!formData.name.trim()) {
+      wx.showToast({
+        title: '请输入姓名(繁体)',
+        icon: 'none'
+      });
+      return;
+    }
+    
+    if (!formData.birthday) {
+      wx.showToast({
+        title: '请选择出生日期',
+        icon: 'none'
+      });
+      return;
+    }
+
+    wx.showLoading({
+      title: '提交中...'
+    });
+
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/add`,
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${app.globalData.token}`
+      },
+      data: {
+        name: formData.name,
+        simplified_name: formData.simplified_name,
+        sex: parseInt(formData.sex),
+        birthday: formData.birthday,
+        family_rank: formData.family_rank,
+        name_word_generation: formData.name_word_generation,
+        is_pass_away: formData.is_pass_away,
+        marital_status: formData.marital_status,
+        former_name: formData.former_name,
+        phone: formData.phone,
+        notes: formData.notes,
+        relations: this.data.selectedMemberId ? [{
+          parent_mid: this.data.selectedMemberId,
+          relation_type: parseInt(formData.relation_type) || 1,
+          sub_relation_type: parseInt(formData.sub_relation_type) || 0
+        }] : []
+      },
+      success: (res) => {
+        wx.hideLoading();
+        if (res.data.success) {
+          wx.showToast({
+            title: '录入成功',
+            icon: 'success'
+          });
+          setTimeout(() => {
+            wx.navigateBack();
+          }, 1500);
+        } else {
+          wx.showToast({
+            title: res.data.message || '录入失败',
+            icon: 'none'
+          });
+        }
+      },
+      fail: () => {
+        wx.hideLoading();
+        wx.showToast({
+          title: '网络异常',
+          icon: 'none'
+        });
+      }
+    });
+  }
+});

+ 3 - 0
pages/add_member/add_member.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 395 - 0
pages/add_member/add_member.wxml

@@ -0,0 +1,395 @@
+<view class="container">
+  <!-- 页面标题 -->
+  <view class="page-header">
+    <text class="page-title">录入新成员</text>
+  </view>
+
+  <!-- 表单内容 -->
+  <view class="form-container">
+    <!-- 核心信息 -->
+    <view class="section">
+      <view class="section-title">核心信息</view>
+      
+      <view class="form-item">
+        <view class="form-label">
+          <text class="label-text">姓名(繁体)</text>
+          <text class="required">*</text>
+        </view>
+        <input 
+          class="form-input" 
+          placeholder="请输入姓名(繁体)" 
+          value="{{formData.name}}"
+          bindinput="onNameInput"
+          confirm-type="next"
+        />
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">姓名(简体)</view>
+        <input 
+          class="form-input" 
+          placeholder="请输入姓名(简体)" 
+          value="{{formData.simplified_name}}"
+          bindinput="onSimplifiedNameInput"
+          confirm-type="next"
+        />
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">
+          <text class="label-text">性别</text>
+          <text class="required">*</text>
+        </view>
+        <view class="radio-group">
+          <view 
+            class="radio-item {{formData.sex === '1' ? 'active' : ''}}" 
+            bindtap="selectSex" 
+            data-value="1"
+          >
+            <view class="radio-circle"></view>
+            <text class="radio-text">男</text>
+          </view>
+          <view 
+            class="radio-item {{formData.sex === '2' ? 'active' : ''}}" 
+            bindtap="selectSex" 
+            data-value="2"
+          >
+            <view class="radio-circle"></view>
+            <text class="radio-text">女</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">
+          <text class="label-text">出生日期</text>
+          <text class="required">*</text>
+        </view>
+        <view class="date-picker">
+          <picker mode="date" value="{{formData.birthday}}" bindchange="onBirthdayChange">
+            <view class="picker-content">
+              <text class="picker-text">{{formData.birthday || '请选择出生日期'}}</text>
+            </view>
+          </picker>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">堂内排行</view>
+        <input 
+          class="form-input" 
+          placeholder="如:长子、次子等" 
+          value="{{formData.family_rank}}"
+          bindinput="onFamilyRankInput"
+          confirm-type="next"
+        />
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">世系世代</view>
+        <input 
+          class="form-input" 
+          placeholder="如:衢州第二十九代" 
+          value="{{formData.name_word_generation}}"
+          bindinput="onGenerationInput"
+          confirm-type="next"
+        />
+      </view>
+    </view>
+
+    <!-- 状态信息 -->
+    <view class="section">
+      <view class="section-title">状态信息</view>
+      
+      <view class="form-item">
+        <view class="form-label">是否过世</view>
+        <picker mode="selector" range="{{isPassAwayOptions}}" bindchange="onIsPassAwayChange">
+          <view class="picker-content">
+            <text class="picker-text">{{isPassAwayOptions[formData.is_pass_away] || '请选择'}}</text>
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">婚姻状况</view>
+        <picker mode="selector" range="{{maritalStatusOptions}}" bindchange="onMaritalStatusChange">
+          <view class="picker-content">
+            <text class="picker-text">{{maritalStatusOptions[formData.marital_status] || '请选择'}}</text>
+          </view>
+        </picker>
+      </view>
+    </view>
+
+    <!-- 关系信息 -->
+    <view class="section">
+      <view class="section-title">关系信息</view>
+
+      <view class="form-item">
+        <view class="form-label"><text class="label-text">关联成员</text></view>
+        <view class="relation-select-row">
+          <view class="relation-select {{selectedMemberName ? 'selected' : ''}}" bindtap="selectRelatedMember">
+            <text class="relation-text">{{selectedMemberName || '点击搜索并选择关联成员'}}</text>
+            <text class="relation-arrow">›</text>
+          </view>
+          <view class="relation-clear" wx:if="{{selectedMemberName}}" bindtap="clearSelectedMember">✕</view>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <view class="form-label"><text class="label-text">关系类型</text></view>
+        <picker mode="selector" range="{{relationTypeOptions}}" bindchange="onRelationTypeChange">
+          <view class="picker-content">
+            <text class="picker-text">{{relationTypeOptions[formData.relation_type] || '请选择关系类型'}}</text>
+            <text class="relation-arrow">›</text>
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <view class="form-label"><text class="label-text">子类型</text></view>
+        <picker mode="selector" range="{{subRelationTypeOptions}}" bindchange="onSubRelationTypeChange">
+          <view class="picker-content">
+            <text class="picker-text">{{subRelationTypeOptions[formData.sub_relation_type] || '请选择子类型'}}</text>
+            <text class="relation-arrow">›</text>
+          </view>
+        </picker>
+      </view>
+    </view>
+
+    <!-- 其他信息 -->
+    <view class="section">
+      <view class="section-title">其他信息</view>
+      
+      <view class="form-item">
+        <view class="form-label">曾用名</view>
+        <input 
+          class="form-input" 
+          placeholder="请输入曾用名" 
+          value="{{formData.former_name}}"
+          bindinput="onFormerNameInput"
+          confirm-type="next"
+        />
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">手机号</view>
+        <input 
+          class="form-input" 
+          placeholder="请输入手机号" 
+          value="{{formData.phone}}"
+          bindinput="onPhoneInput"
+          confirm-type="next"
+        />
+      </view>
+
+      <view class="form-item">
+        <view class="form-label">备注</view>
+        <textarea 
+          class="form-textarea" 
+          placeholder="请输入备注信息" 
+          value="{{formData.notes}}"
+          bindinput="onNotesInput"
+        />
+      </view>
+    </view>
+  </view>
+
+  <!-- 提交按钮 -->
+  <view class="submit-section">
+    <button class="submit-btn" bindtap="submitForm">确认录入</button>
+  </view>
+
+  <!-- 同名成员弹窗 -->
+  <view class="modal-overlay" wx:if="{{showDuplicateModal}}" bindtap="closeDuplicateModal">
+    <view class="modal-content" catchtap="stopPropagation">
+      <view class="modal-header">
+        <text class="modal-title">检测到同名成员</text>
+        <view class="modal-close" bindtap="closeDuplicateModal">×</view>
+      </view>
+      <view class="modal-body">
+        <view class="duplicate-list">
+          <view 
+            class="duplicate-item" 
+            wx:for="{{duplicateMembers}}" 
+            wx:key="id"
+            bindtap="viewMemberDetail"
+            data-member="{{item}}"
+          >
+            <view class="duplicate-info">
+              <text class="duplicate-name">{{item.name}}{{item.simplified_name ? '(' + item.simplified_name + ')' : ''}}</text>
+              <text class="duplicate-detail">{{item.name_word_generation || '暂无世代信息'}}</text>
+            </view>
+            <text class="duplicate-arrow">›</text>
+          </view>
+        </view>
+      </view>
+      <view class="modal-footer">
+        <button class="modal-btn cancel-btn" bindtap="closeDuplicateModal">继续录入</button>
+        <button class="modal-btn confirm-btn" bindtap="abortEntry">放弃录入</button>
+      </view>
+    </view>
+  </view>
+
+  <!-- 关联成员搜索弹层 -->
+  <view class="modal-overlay" wx:if="{{showMemberSearch}}" bindtap="closeMemberSearch">
+    <view class="modal-content search-modal" catchtap="stopPropagation">
+      <view class="modal-header">
+        <text class="modal-title">搜索关联成员</text>
+        <view class="modal-close" bindtap="closeMemberSearch">×</view>
+      </view>
+      <view class="search-bar-wrap">
+        <input
+          class="search-input"
+          placeholder="输入姓名搜索"
+          value="{{memberSearchKeyword}}"
+          bindinput="onMemberSearchInput"
+          confirm-type="search"
+          bindconfirm="doMemberSearch"
+          focus="{{showMemberSearch}}"
+        />
+        <view class="search-btn" bindtap="doMemberSearch">搜索</view>
+      </view>
+      <view class="search-tips" wx:if="{{!memberSearchLoading && memberSearchResults.length === 0 && memberSearchKeyword}}">
+        <text>未找到匹配成员,请尝试其他关键词</text>
+      </view>
+      <view class="search-tips" wx:if="{{!memberSearchLoading && memberSearchResults.length === 0 && !memberSearchKeyword}}">
+        <text>请输入成员姓名进行搜索</text>
+      </view>
+      <view class="search-loading" wx:if="{{memberSearchLoading}}">
+        <text>搜索中...</text>
+      </view>
+      <scroll-view scroll-y class="search-results" wx:if="{{memberSearchResults.length > 0}}">
+        <view
+          class="search-result-item"
+          wx:for="{{memberSearchResults}}"
+          wx:key="id"
+        >
+          <!-- 左侧信息区 -->
+          <view class="result-info">
+            <view class="result-name-row">
+              <text class="result-name">{{item.name}}</text>
+              <text class="result-simplified" wx:if="{{item.simplified_name}}">({{item.simplified_name}})</text>
+            </view>
+            <view class="result-meta-row">
+              <text class="result-id">ID: {{item.id}}</text>
+              <text class="result-sep"> | </text>
+              <text class="result-sex">{{item.sex === 1 ? '男' : item.sex === 2 ? '女' : '未知'}}</text>
+            </view>
+            <view class="result-meta-row" wx:if="{{item.name_word_generation}}">
+              <text class="result-meta-label">世系世代: </text>
+              <text class="result-meta-val">{{item.name_word_generation}}</text>
+            </view>
+            <view class="result-meta-row" wx:if="{{item.father_name}}">
+              <text class="result-meta-label">{{item.father_relation_type === 2 ? '母亲' : '父亲'}}: </text>
+              <text class="result-meta-val">{{item.father_name}}{{item.father_simplified_name ? ' (' + item.father_simplified_name + ')' : ''}}</text>
+              <text class="result-meta-gen" wx:if="{{item.father_generation}}"> | 世系世代: {{item.father_generation}}</text>
+            </view>
+          </view>
+          <!-- 右侧按钮区 -->
+          <view class="result-actions">
+            <view class="result-select-btn" catchtap="confirmSelectMember" data-member="{{item}}">选择</view>
+            <view class="result-detail-btn" catchtap="viewMemberDetail" data-member="{{item}}">详情</view>
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+
+  <!-- 成员详情弹窗 -->
+  <view class="modal-overlay" wx:if="{{showMemberDetailModal}}" bindtap="closeMemberDetailModal">
+    <view class="modal-content detail-modal" catchtap="stopPropagation">
+      <view class="modal-header">
+        <view class="detail-modal-title-wrap">
+          <text class="modal-title">{{selectedMemberDetail.name}}</text>
+          <text class="detail-modal-sub" wx:if="{{selectedMemberDetail.simplified_name}}">({{selectedMemberDetail.simplified_name}})</text>
+        </view>
+        <view class="modal-close" bindtap="closeMemberDetailModal">×</view>
+      </view>
+
+      <scroll-view scroll-y class="detail-scroll" wx:if="{{selectedMemberDetail}}">
+
+        <!-- 基本信息 -->
+        <view class="detail-group-title">基本信息</view>
+        <view class="detail-section">
+          <view class="detail-row">
+            <text class="detail-label">ID</text>
+            <text class="detail-value">{{selectedMemberDetail.id}}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">性别</text>
+            <text class="detail-value">{{selectedMemberDetail.sex === 1 ? '男' : selectedMemberDetail.sex === 2 ? '女' : '未知'}}</text>
+          </view>
+          <view class="detail-row" wx:if="{{selectedMemberDetail.birthday_date}}">
+            <text class="detail-label">出生日期</text>
+            <text class="detail-value">{{selectedMemberDetail.birthday_date}}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">生存状态</text>
+            <text class="detail-value">{{selectedMemberDetail.is_pass_away === 1 ? '已故' : '健在'}}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">婚姻状况</text>
+            <text class="detail-value">{{selectedMemberDetail.marital_status === 1 ? '未婚' : selectedMemberDetail.marital_status === 2 ? '已婚' : selectedMemberDetail.marital_status === 3 ? '离异/丧偶' : '未知'}}</text>
+          </view>
+          <view class="detail-row" wx:if="{{selectedMemberDetail.former_name}}">
+            <text class="detail-label">曾用名</text>
+            <text class="detail-value">{{selectedMemberDetail.former_name}}</text>
+          </view>
+          <view class="detail-row" wx:if="{{selectedMemberDetail.phone}}">
+            <text class="detail-label">手机号</text>
+            <text class="detail-value">{{selectedMemberDetail.phone}}</text>
+          </view>
+        </view>
+
+        <!-- 家谱信息 -->
+        <view class="detail-group-title">家谱信息</view>
+        <view class="detail-section">
+          <view class="detail-row" wx:if="{{selectedMemberDetail.name_word_generation}}">
+            <text class="detail-label">世系世代</text>
+            <text class="detail-value">{{selectedMemberDetail.name_word_generation}}</text>
+          </view>
+          <view class="detail-row" wx:if="{{selectedMemberDetail.family_rank}}">
+            <text class="detail-label">堂内排行</text>
+            <text class="detail-value">{{selectedMemberDetail.family_rank}}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">录入来源</text>
+            <text class="detail-value detail-source-{{selectedMemberDetail.data_source}}">{{selectedMemberDetail.data_source === 'miniprogram' ? '小程序录入' : selectedMemberDetail.data_source === 'pdf_ai' ? 'PDF识别' : '后台录入'}}</text>
+          </view>
+        </view>
+
+        <!-- 亲属关系 -->
+        <view class="detail-group-title" wx:if="{{selectedMemberDetail.parents && selectedMemberDetail.parents.length > 0}}">父母</view>
+        <view class="detail-section" wx:if="{{selectedMemberDetail.parents && selectedMemberDetail.parents.length > 0}}">
+          <view class="detail-row" wx:for="{{selectedMemberDetail.parents}}" wx:key="id">
+            <text class="detail-label">{{item.relation_label}}</text>
+            <text class="detail-value">{{item.name}}{{item.simplified_name ? ' (' + item.simplified_name + ')' : ''}}</text>
+          </view>
+        </view>
+
+        <view class="detail-group-title" wx:if="{{selectedMemberDetail.children && selectedMemberDetail.children.length > 0}}">子女({{selectedMemberDetail.children.length}}人)</view>
+        <view class="detail-section" wx:if="{{selectedMemberDetail.children && selectedMemberDetail.children.length > 0}}">
+          <view class="detail-children-list">
+            <view class="detail-child-item" wx:for="{{selectedMemberDetail.children}}" wx:key="id">
+              <text class="child-sex-dot sex-dot-{{item.sex}}">●</text>
+              <text class="child-name">{{item.name}}{{item.simplified_name ? ' (' + item.simplified_name + ')' : ''}}</text>
+              <text class="child-gen" wx:if="{{item.name_word_generation}}"> · {{item.name_word_generation}}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 备注 -->
+        <view wx:if="{{selectedMemberDetail.notes}}">
+          <view class="detail-group-title">备注</view>
+          <view class="detail-notes">{{selectedMemberDetail.notes}}</view>
+        </view>
+
+      </scroll-view>
+
+      <view class="modal-footer">
+        <button class="modal-btn cancel-btn" bindtap="closeMemberDetailModal">关闭</button>
+        <button class="modal-btn confirm-btn" bindtap="selectFromDetail">选择此人</button>
+      </view>
+    </view>
+  </view>
+</view>

+ 683 - 0
pages/add_member/add_member.wxss

@@ -0,0 +1,683 @@
+.container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-bottom: 160rpx;
+}
+
+/* 页面标题 */
+.page-header {
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  padding: 40rpx 30rpx;
+}
+
+.page-title {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+/* 表单容器 */
+.form-container {
+  padding: 20rpx 30rpx;
+}
+
+/* 章节 */
+.section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  padding-left: 16rpx;
+  border-left: 6rpx solid #8B4513;
+}
+
+/* 表单项 */
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-item:last-child {
+  margin-bottom: 0;
+}
+
+.form-label {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+
+.label-text {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.required {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  margin-left: 8rpx;
+}
+
+.form-input {
+  width: 100%;
+  height: 80rpx;
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 0 24rpx;
+  font-size: 28rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.form-textarea {
+  width: 100%;
+  height: 160rpx;
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 20rpx 24rpx;
+  font-size: 28rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+/* 单选框组 */
+.radio-group {
+  display: flex;
+  gap: 40rpx;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 32rpx;
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  border: 2rpx solid transparent;
+  transition: all 0.3s ease;
+}
+
+.radio-item.active {
+  background: rgba(139, 69, 19, 0.1);
+  border-color: #8B4513;
+}
+
+.radio-circle {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+  border: 2rpx solid #ccc;
+  margin-right: 12rpx;
+  position: relative;
+}
+
+.radio-item.active .radio-circle {
+  border-color: #8B4513;
+}
+
+.radio-item.active .radio-circle::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 16rpx;
+  height: 16rpx;
+  background: #8B4513;
+  border-radius: 50%;
+}
+
+.radio-text {
+  font-size: 28rpx;
+  color: #333;
+}
+
+/* 选择器 */
+.date-picker,
+.relation-select {
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 24rpx;
+}
+
+.picker-content,
+.relation-select {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.picker-text,
+.relation-text {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.relation-arrow {
+  font-size: 36rpx;
+  color: #999;
+}
+
+/* 关系信息 */
+.relation-container {
+  margin-top: 16rpx;
+}
+
+.relation-row {
+  display: flex;
+  gap: 16rpx;
+}
+
+.relation-item {
+  flex: 1;
+}
+
+.relation-item .form-label {
+  margin-bottom: 8rpx;
+}
+
+.relation-item .picker-content {
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 20rpx 16rpx;
+}
+
+/* 提交按钮 */
+.submit-section {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.submit-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  border-radius: 44rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #fff;
+  border: none;
+}
+
+.submit-btn:active {
+  opacity: 0.8;
+}
+
+/* 弹窗遮罩 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  width: 85%;
+  max-height: 70vh;
+  background: #fff;
+  border-radius: 20rpx;
+  overflow: hidden;
+}
+
+.detail-modal {
+  max-height: 80vh;
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.modal-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.modal-close {
+  width: 48rpx;
+  height: 48rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 40rpx;
+  color: #999;
+}
+
+.modal-body {
+  padding: 30rpx;
+  max-height: 40vh;
+  overflow-y: auto;
+}
+
+/* 同名列表 */
+.duplicate-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.duplicate-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.duplicate-item:last-child {
+  border-bottom: none;
+}
+
+.duplicate-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.duplicate-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+
+.duplicate-detail {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.duplicate-arrow {
+  font-size: 36rpx;
+  color: #999;
+}
+
+/* 详情内容 */
+.detail-section {
+  display: flex;
+  flex-direction: column;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.detail-row:last-child {
+  border-bottom: none;
+}
+
+.detail-label {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.detail-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+/* 弹窗按钮 */
+.modal-footer {
+  display: flex;
+  gap: 20rpx;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  border-top: 1rpx solid #eee;
+}
+
+.modal-btn {
+  flex: 1;
+  height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  border: none;
+}
+
+.cancel-btn {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.confirm-btn {
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  color: #fff;
+}
+
+/* 详情弹窗增强 */
+.detail-modal {
+  max-height: 88vh;
+  height: 88vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.detail-modal-title-wrap {
+  display: flex;
+  align-items: baseline;
+  gap: 8rpx;
+  flex: 1;
+  min-width: 0;
+}
+
+.detail-modal-sub {
+  font-size: 26rpx;
+  color: #888;
+  font-weight: normal;
+}
+
+.detail-scroll {
+  flex: 1;
+  min-height: 0;
+}
+
+.detail-group-title {
+  font-size: 26rpx;
+  font-weight: 700;
+  color: #8B4513;
+  background: #fdf6f0;
+  padding: 16rpx 30rpx;
+  border-left: 6rpx solid #8B4513;
+  margin-top: 4rpx;
+}
+
+.detail-section {
+  background: #fff;
+  padding: 0 30rpx;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+
+.detail-row:last-child {
+  border-bottom: none;
+}
+
+.detail-label {
+  font-size: 28rpx;
+  color: #999;
+  min-width: 140rpx;
+  flex-shrink: 0;
+}
+
+.detail-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  text-align: right;
+  flex: 1;
+}
+
+.detail-source-miniprogram { color: #4a90e2; }
+.detail-source-pdf_ai { color: #9b59b6; }
+.detail-source-backend { color: #888; }
+
+/* 子女列表 */
+.detail-children-list {
+  padding: 12rpx 0;
+}
+
+.detail-child-item {
+  display: flex;
+  align-items: center;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f8f8f8;
+}
+
+.detail-child-item:last-child {
+  border-bottom: none;
+}
+
+.child-sex-dot {
+  font-size: 20rpx;
+  margin-right: 12rpx;
+}
+
+.sex-dot-1 { color: #4a90e2; }
+.sex-dot-2 { color: #e25c8a; }
+
+.child-name {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.child-gen {
+  font-size: 24rpx;
+  color: #aaa;
+}
+
+/* 备注 */
+.detail-notes {
+  background: #fff;
+  padding: 20rpx 30rpx;
+  font-size: 28rpx;
+  color: #555;
+  line-height: 1.6;
+}
+
+/* 关联成员选择行 */
+.relation-select-row {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.relation-select {
+  flex: 1;
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 24rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 2rpx solid transparent;
+}
+
+.relation-select.selected {
+  border-color: #8B4513;
+  background: rgba(139, 69, 19, 0.05);
+}
+
+.relation-clear {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 50%;
+  background: #f0f0f0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 26rpx;
+  color: #999;
+  flex-shrink: 0;
+}
+
+/* 搜索弹层 */
+.search-modal {
+  width: 92%;
+  max-height: 80vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.search-bar-wrap {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 20rpx 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.search-input {
+  flex: 1;
+  height: 72rpx;
+  background: #f8f9fa;
+  border-radius: 36rpx;
+  padding: 0 28rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.search-btn {
+  height: 72rpx;
+  padding: 0 32rpx;
+  background: linear-gradient(135deg, #8B4513, #D2691E);
+  color: #fff;
+  border-radius: 36rpx;
+  font-size: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+
+.search-tips {
+  padding: 40rpx 30rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #aaa;
+}
+
+.search-loading {
+  padding: 40rpx 30rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #8B4513;
+}
+
+.search-results {
+  flex: 1;
+  min-height: 0;
+  height: 50vh;
+}
+
+/* 搜索结果卡片 */
+.search-result-item {
+  display: flex;
+  align-items: flex-start;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+  gap: 16rpx;
+}
+
+.search-result-item:active {
+  background: #fdf6f0;
+}
+
+.result-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.result-name-row {
+  display: flex;
+  align-items: baseline;
+  flex-wrap: wrap;
+  gap: 4rpx;
+  margin-bottom: 8rpx;
+}
+
+.result-name {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #1a1a2e;
+}
+
+.result-simplified {
+  font-size: 26rpx;
+  color: #888;
+  font-weight: normal;
+}
+
+.result-meta-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  margin-top: 6rpx;
+}
+
+.result-id {
+  font-size: 24rpx;
+  color: #aaa;
+}
+
+.result-sep {
+  font-size: 24rpx;
+  color: #ccc;
+  margin: 0 4rpx;
+}
+
+.result-sex {
+  font-size: 24rpx;
+  color: #666;
+}
+
+.result-meta-label {
+  font-size: 24rpx;
+  color: #aaa;
+}
+
+.result-meta-val {
+  font-size: 24rpx;
+  color: #555;
+}
+
+.result-meta-gen {
+  font-size: 22rpx;
+  color: #aaa;
+}
+
+.result-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+  flex-shrink: 0;
+  padding-top: 4rpx;
+}
+
+.result-select-btn {
+  background: linear-gradient(135deg, #8B4513, #D2691E);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 10rpx 24rpx;
+  border-radius: 24rpx;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.result-detail-btn {
+  background: #f0f0f0;
+  color: #555;
+  font-size: 26rpx;
+  padding: 10rpx 24rpx;
+  border-radius: 24rpx;
+  text-align: center;
+  white-space: nowrap;
+}

+ 120 - 0
pages/family_circle/family_circle.js

@@ -0,0 +1,120 @@
+const app = getApp();
+
+Page({
+  data: {
+    isLoggedIn: false,
+    newsList: [
+      {
+        id: 1,
+        title: '2025年族谱修订工作正式启动',
+        description: '经过家族理事会讨论决定,2025年族谱修订工作已于本月正式启动,欢迎各位宗亲提供相关资料。',
+        author: '留越',
+        time: '2025-01-15'
+      },
+      {
+        id: 2,
+        title: '新成员录入系统上线',
+        description: '家族成员录入系统已完成升级,新增多项智能识别功能,录入更加便捷。',
+        author: '管理员',
+        time: '2025-01-10'
+      },
+      {
+        id: 3,
+        title: '家族聚会圆满成功',
+        description: '2024年度家族聚会已于12月28日圆满结束,共有150余位宗亲参加,现场气氛热烈。',
+        author: '留伟',
+        time: '2024-12-30'
+      }
+    ],
+    eventsList: [
+      {
+        id: 1,
+        month: '02',
+        day: '15',
+        title: '春节家族团聚',
+        location: '浙江省金华市',
+        description: '一年一度的春节家族团聚活动即将举行'
+      },
+      {
+        id: 2,
+        month: '03',
+        day: '08',
+        title: '清明祭祖',
+        location: '福建省泉州市',
+        description: '清明节祭祖活动,缅怀先祖'
+      },
+      {
+        id: 3,
+        month: '05',
+        day: '18',
+        title: '族谱研讨会',
+        location: '浙江省杭州市',
+        description: '族谱修订工作研讨会'
+      }
+    ]
+  },
+
+  onLoad: function () {
+    this.checkLoginStatus();
+  },
+
+  onShow: function () {
+    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+      this.getTabBar().setData({ selected: 3 });
+    }
+    this.checkLoginStatus();
+  },
+
+  checkLoginStatus: function () {
+    this.setData({
+      isLoggedIn: app.globalData.isLoggedIn
+    });
+  },
+
+  goToNews: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  goToEvents: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  goToAlbum: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  goToContact: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  goLogin: function () {
+    app.login((err) => {
+      if (!err) {
+        this.setData({
+          isLoggedIn: true
+        });
+        wx.showToast({
+          title: '登录成功',
+          icon: 'success'
+        });
+      } else {
+        wx.showToast({
+          title: '登录失败',
+          icon: 'none'
+        });
+      }
+    });
+  }
+});

+ 3 - 0
pages/family_circle/family_circle.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 92 - 0
pages/family_circle/family_circle.wxml

@@ -0,0 +1,92 @@
+<view class="container">
+  <!-- 页面标题 -->
+  <view class="page-header">
+    <text class="page-title">家族圈</text>
+  </view>
+
+  <!-- 功能入口 -->
+  <view class="function-section">
+    <view class="function-grid">
+      <view class="function-card" bindtap="goToNews">
+        <view class="card-icon news-icon">
+          <view class="icon-inner"></view>
+        </view>
+        <view class="card-title">家族动态</view>
+        <view class="card-desc">了解家族最新消息</view>
+      </view>
+
+      <view class="function-card" bindtap="goToEvents">
+        <view class="card-icon events-icon">
+          <view class="icon-inner"></view>
+        </view>
+        <view class="card-title">家族活动</view>
+        <view class="card-desc">参与家族聚会</view>
+      </view>
+
+      <view class="function-card" bindtap="goToAlbum">
+        <view class="card-icon album-icon">
+          <view class="icon-inner"></view>
+        </view>
+        <view class="card-title">家族相册</view>
+        <view class="card-desc">分享珍贵回忆</view>
+      </view>
+
+      <view class="function-card" bindtap="goToContact">
+        <view class="card-icon contact-icon">
+          <view class="icon-inner"></view>
+        </view>
+        <view class="card-title">联系我们</view>
+        <view class="card-desc">获取联系方式</view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 最新动态 -->
+  <view class="news-section">
+    <view class="section-header">
+      <text class="section-title">最新动态</text>
+      <text class="section-more">查看更多 ›</text>
+    </view>
+    
+    <view class="news-list">
+      <view class="news-item" wx:for="{{newsList}}" wx:key="id">
+        <view class="news-content">
+          <text class="news-title">{{item.title}}</text>
+          <text class="news-desc">{{item.description}}</text>
+          <view class="news-footer">
+            <text class="news-author">{{item.author}}</text>
+            <text class="news-time">{{item.time}}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 即将到来的活动 -->
+  <view class="events-section">
+    <view class="section-header">
+      <text class="section-title">即将到来</text>
+      <text class="section-more">查看更多 ›</text>
+    </view>
+    
+    <view class="events-list">
+      <view class="event-item" wx:for="{{eventsList}}" wx:key="id">
+        <view class="event-date">
+          <text class="event-month">{{item.month}}</text>
+          <text class="event-day">{{item.day}}</text>
+        </view>
+        <view class="event-content">
+          <text class="event-title">{{item.title}}</text>
+          <text class="event-location">{{item.location}}</text>
+          <text class="event-desc">{{item.description}}</text>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 底部提示 -->
+  <view class="footer-hint">
+    <text class="hint-text">登录后可查看更多家族内容</text>
+    <button class="hint-btn" wx:if="{{!isLoggedIn}}" bindtap="goLogin">立即登录</button>
+  </view>
+</view>

+ 270 - 0
pages/family_circle/family_circle.wxss

@@ -0,0 +1,270 @@
+.container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-bottom: 160rpx;
+}
+
+/* 页面标题 */
+.page-header {
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  padding: 40rpx 30rpx;
+}
+
+.page-title {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+/* 功能入口 */
+.function-section {
+  padding: 20rpx 30rpx;
+  margin-bottom: 20rpx;
+}
+
+.function-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20rpx;
+}
+
+.function-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.card-icon {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 20rpx;
+}
+
+.news-icon {
+  background: linear-gradient(135deg, #4CAF50, #8BC34A);
+}
+
+.events-icon {
+  background: linear-gradient(135deg, #2196F3, #64B5F6);
+}
+
+.album-icon {
+  background: linear-gradient(135deg, #FF9800, #FFB74D);
+}
+
+.contact-icon {
+  background: linear-gradient(135deg, #9C27B0, #CE93D8);
+}
+
+.icon-inner {
+  width: 50rpx;
+  height: 50rpx;
+}
+
+.news-icon .icon-inner {
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.events-icon .icon-inner {
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.album-icon .icon-inner {
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.contact-icon .icon-inner {
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.card-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+
+.card-desc {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* 最新动态 */
+.news-section {
+  padding: 0 30rpx;
+  margin-bottom: 20rpx;
+}
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.section-more {
+  font-size: 26rpx;
+  color: #8B4513;
+}
+
+.news-list {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+
+.news-item {
+  padding: 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.news-item:last-child {
+  border-bottom: none;
+}
+
+.news-content {
+  display: flex;
+  flex-direction: column;
+}
+
+.news-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+
+.news-desc {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.5;
+  margin-bottom: 16rpx;
+}
+
+.news-footer {
+  display: flex;
+  justify-content: space-between;
+}
+
+.news-author {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.news-time {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* 即将到来的活动 */
+.events-section {
+  padding: 0 30rpx;
+  margin-bottom: 20rpx;
+}
+
+.events-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.event-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  display: flex;
+}
+
+.event-date {
+  width: 80rpx;
+  height: 80rpx;
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  border-radius: 12rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+
+.event-month {
+  font-size: 20rpx;
+  color: rgba(255, 255, 255, 0.8);
+}
+
+.event-day {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.event-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.event-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+
+.event-location {
+  font-size: 24rpx;
+  color: #8B4513;
+  margin-bottom: 8rpx;
+}
+
+.event-desc {
+  font-size: 26rpx;
+  color: #666;
+}
+
+/* 底部提示 */
+.footer-hint {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.hint-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.hint-btn {
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  color: #fff;
+  border-radius: 30rpx;
+  padding: 16rpx 32rpx;
+  font-size: 28rpx;
+  border: none;
+}

+ 81 - 0
pages/index/index.js

@@ -0,0 +1,81 @@
+const app = getApp();
+
+Page({
+  data: {
+    userInfo: null
+  },
+
+  onLoad: function () {
+    this.setData({
+      userInfo: app.globalData.userInfo
+    });
+  },
+
+  onShow: function () {
+    this.setData({ userInfo: app.globalData.userInfo });
+    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+      this.getTabBar().setData({ selected: 0 });
+    }
+  },
+
+  // 录入信息
+  goToAddMember: function () {
+    if (!app.globalData.isLoggedIn) {
+      wx.showModal({
+        title: '提示',
+        content: '请先登录后再录入信息',
+        showCancel: false,
+        success: (res) => {
+          this.doLogin(() => {
+            wx.navigateTo({
+              url: '/pages/add_member/add_member'
+            });
+          });
+        }
+      });
+    } else {
+      wx.navigateTo({
+        url: '/pages/add_member/add_member'
+      });
+    }
+  },
+
+  // 我的录入
+  goToMyEntries: function () {
+    if (!app.globalData.isLoggedIn) {
+      wx.showModal({
+        title: '提示',
+        content: '请先登录后查看录入记录',
+        showCancel: false,
+        success: () => {
+          wx.navigateTo({ url: '/pages/login/login' });
+        }
+      });
+    } else {
+      wx.switchTab({ url: '/pages/my_entries/my_entries' });
+    }
+  },
+
+  // 家族相册(预留)
+  goToAlbum: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  // 家族文化(预留)
+  goToCulture: function () {
+    wx.showToast({
+      title: '功能开发中',
+      icon: 'none'
+    });
+  },
+
+  // 登录
+  doLogin: function (callback) {
+    wx.navigateTo({
+      url: '/pages/login/login'
+    });
+  }
+});

+ 3 - 0
pages/index/index.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 179 - 0
pages/index/index.wxml

@@ -0,0 +1,179 @@
+<view class="container">
+
+  <!-- 顶部横幅 -->
+  <view class="banner">
+    <!-- 装饰纹理点 -->
+    <view class="banner-deco deco-tl"></view>
+    <view class="banner-deco deco-tr"></view>
+    <view class="banner-deco deco-bl"></view>
+
+    <view class="banner-content">
+      <view class="banner-subtitle">留氏宗族</view>
+      <view class="banner-title-row">
+        <view class="banner-line"></view>
+        <text class="banner-title">留家族谱</text>
+        <view class="banner-line"></view>
+      </view>
+      <view class="banner-motto">
+        <text class="banner-dot">·</text>
+        <text class="motto-text">传世家族 情系后人</text>
+        <text class="banner-dot">·</text>
+      </view>
+      <view class="banner-stats">
+        <view class="stat-item">
+          <text class="stat-num">衢州</text>
+          <text class="stat-label">发源地</text>
+        </view>
+        <view class="stat-sep"></view>
+        <view class="stat-item">
+          <text class="stat-num">29</text>
+          <text class="stat-label">当前最新代</text>
+        </view>
+        <view class="stat-sep"></view>
+        <view class="stat-item">
+          <text class="stat-num">2025</text>
+          <text class="stat-label">建谱年份</text>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 功能入口 -->
+  <view class="section-wrap">
+    <view class="section-label">
+      <view class="label-line"></view>
+      <text class="label-text">功能入口</text>
+      <view class="label-line"></view>
+    </view>
+    <view class="function-grid">
+
+      <view class="function-card" bindtap="goToAddMember">
+        <view class="func-icon-wrap" style="background:linear-gradient(135deg,#e8763a,#c9521e);">
+          <text class="func-emoji">✏️</text>
+        </view>
+        <text class="func-title">录入信息</text>
+        <text class="func-desc">记录家人故事</text>
+      </view>
+
+      <view class="function-card" bindtap="goToMyEntries">
+        <view class="func-icon-wrap" style="background:linear-gradient(135deg,#3a8ee8,#1e66c9);">
+          <text class="func-emoji">📋</text>
+        </view>
+        <text class="func-title">我的录入</text>
+        <text class="func-desc">管理已录入</text>
+      </view>
+
+      <view class="function-card" bindtap="goToAlbum">
+        <view class="func-icon-wrap" style="background:linear-gradient(135deg,#e8a83a,#c98a1e);">
+          <text class="func-emoji">🖼️</text>
+        </view>
+        <text class="func-title">家族相册</text>
+        <text class="func-desc">珍藏共同记忆</text>
+      </view>
+
+      <view class="function-card" bindtap="goToCulture">
+        <view class="func-icon-wrap" style="background:linear-gradient(135deg,#7e3ae8,#5a1ec9);">
+          <text class="func-emoji">📖</text>
+        </view>
+        <text class="func-title">家族文化</text>
+        <text class="func-desc">传承的财富</text>
+      </view>
+
+    </view>
+  </view>
+
+  <!-- 编委会成员 -->
+  <view class="section-wrap">
+    <view class="section-label">
+      <view class="label-line"></view>
+      <text class="label-text">编委会成员</text>
+      <view class="label-line"></view>
+    </view>
+
+    <view class="committee-card">
+
+      <!-- 主编 -->
+      <view class="committee-row">
+        <view class="c-icon-wrap icon-gold">
+          <text class="c-icon">★</text>
+        </view>
+        <view class="c-body">
+          <text class="c-role">主编</text>
+          <text class="c-name">留文正</text>
+        </view>
+      </view>
+
+      <view class="c-divider"></view>
+
+      <!-- 编委成员 -->
+      <view class="committee-row c-row-top">
+        <view class="c-icon-wrap icon-blue">
+          <text class="c-icon">◉</text>
+        </view>
+        <view class="c-body">
+          <text class="c-role">编委成员</text>
+          <view class="member-tags">
+            <text class="member-tag">留忠德</text>
+            <text class="member-tag">留文正</text>
+            <text class="member-tag">留越</text>
+            <text class="member-tag">留良吾</text>
+            <text class="member-tag">留如藩</text>
+            <text class="member-tag">留强</text>
+            <text class="member-tag-more">……</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="c-divider"></view>
+
+      <!-- 外支顾问 -->
+      <view class="committee-row">
+        <view class="c-icon-wrap icon-green">
+          <text class="c-icon">◆</text>
+        </view>
+        <view class="c-body">
+          <text class="c-role">外支顾问</text>
+          <text class="c-name">留朝信</text>
+        </view>
+      </view>
+
+      <view class="c-divider"></view>
+
+      <!-- 系统规划 -->
+      <view class="committee-row">
+        <view class="c-icon-wrap icon-purple">
+          <text class="c-icon">⬡</text>
+        </view>
+        <view class="c-body">
+          <text class="c-role">系统规划</text>
+          <text class="c-name">留越</text>
+        </view>
+      </view>
+
+      <view class="c-divider"></view>
+
+      <!-- 技术实现 -->
+      <view class="committee-row">
+        <view class="c-icon-wrap icon-cyan">
+          <text class="c-icon">⟨/⟩</text>
+        </view>
+        <view class="c-body">
+          <text class="c-role">技术实现</text>
+          <text class="c-name c-name-long">春笋秋竹(杭州)科技有限公司</text>
+        </view>
+      </view>
+
+    </view>
+  </view>
+
+  <!-- 底部版权 -->
+  <view class="footer">
+    <view class="footer-deco">
+      <view class="footer-line"></view>
+      <text class="footer-icon">♦</text>
+      <view class="footer-line"></view>
+    </view>
+    <text class="copyright">© 2025 留家族族谱管理系统</text>
+  </view>
+
+</view>

+ 322 - 0
pages/index/index.wxss

@@ -0,0 +1,322 @@
+page {
+  background: #f2ece4;
+}
+
+.container {
+  min-height: 100vh;
+  background: #f2ece4;
+  padding-bottom: 140rpx;
+}
+
+/* ======== Banner ======== */
+.banner {
+  position: relative;
+  background: linear-gradient(160deg, #5c2a0e 0%, #8B4513 40%, #a0521a 70%, #7a3610 100%);
+  padding: 70rpx 40rpx 60rpx;
+  overflow: hidden;
+}
+
+/* 装饰圆点 */
+.banner-deco {
+  position: absolute;
+  border-radius: 50%;
+  opacity: 0.12;
+  background: #fff;
+}
+.deco-tl { width: 280rpx; height: 280rpx; top: -100rpx; left: -80rpx; }
+.deco-tr { width: 180rpx; height: 180rpx; top: -60rpx; right: -40rpx; }
+.deco-bl { width: 240rpx; height: 240rpx; bottom: -120rpx; left: 30rpx; }
+
+.banner-content {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.banner-subtitle {
+  font-size: 26rpx;
+  color: rgba(255,220,160,0.85);
+  letter-spacing: 8rpx;
+  margin-bottom: 16rpx;
+}
+
+.banner-title-row {
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+  margin-bottom: 18rpx;
+}
+
+.banner-line {
+  width: 60rpx;
+  height: 2rpx;
+  background: rgba(255,220,160,0.6);
+}
+
+.banner-title {
+  font-size: 64rpx;
+  font-weight: 800;
+  color: #fff;
+  letter-spacing: 6rpx;
+  text-shadow: 0 4rpx 12rpx rgba(0,0,0,0.3);
+}
+
+.banner-motto {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-bottom: 48rpx;
+}
+
+.banner-dot {
+  font-size: 24rpx;
+  color: rgba(255,210,130,0.7);
+}
+
+.motto-text {
+  font-size: 26rpx;
+  color: rgba(255,230,180,0.9);
+  letter-spacing: 4rpx;
+}
+
+/* 统计栏 */
+.banner-stats {
+  display: flex;
+  align-items: center;
+  background: rgba(0,0,0,0.2);
+  border-radius: 50rpx;
+  padding: 16rpx 48rpx;
+  gap: 40rpx;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-num {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #ffd280;
+}
+
+.stat-label {
+  font-size: 20rpx;
+  color: rgba(255,230,180,0.7);
+  margin-top: 4rpx;
+}
+
+.stat-sep {
+  width: 1rpx;
+  height: 48rpx;
+  background: rgba(255,255,255,0.2);
+}
+
+/* ======== Section wrapper ======== */
+.section-wrap {
+  padding: 40rpx 30rpx 0;
+}
+
+.section-label {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 24rpx;
+}
+
+.label-line {
+  flex: 1;
+  height: 1rpx;
+  background: linear-gradient(90deg, transparent, #c8a882);
+}
+
+.label-line:last-child {
+  background: linear-gradient(90deg, #c8a882, transparent);
+}
+
+.label-text {
+  font-size: 28rpx;
+  font-weight: 700;
+  color: #7a4510;
+  letter-spacing: 4rpx;
+  white-space: nowrap;
+}
+
+/* ======== Function Grid ======== */
+.function-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20rpx;
+  margin-bottom: 16rpx;
+}
+
+.function-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 36rpx 24rpx 28rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-shadow: 0 4rpx 20rpx rgba(139,69,19,0.08);
+  border: 1rpx solid rgba(200,168,130,0.25);
+}
+
+.function-card:active {
+  opacity: 0.85;
+  transform: scale(0.97);
+}
+
+.func-icon-wrap {
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 20rpx;
+  box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.15);
+}
+
+.func-emoji {
+  font-size: 44rpx;
+}
+
+.func-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #2c1a08;
+  margin-bottom: 8rpx;
+}
+
+.func-desc {
+  font-size: 22rpx;
+  color: #b08060;
+}
+
+/* ======== Committee Card ======== */
+.committee-card {
+  background: linear-gradient(160deg, #0e1c2f 0%, #172a44 60%, #0c1a2c 100%);
+  border-radius: 24rpx;
+  overflow: hidden;
+  box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.2);
+  border: 1rpx solid rgba(100,150,220,0.15);
+  margin-bottom: 16rpx;
+}
+
+.committee-row {
+  display: flex;
+  align-items: center;
+  padding: 30rpx 32rpx;
+  gap: 24rpx;
+}
+
+.c-row-top {
+  align-items: flex-start;
+}
+
+.c-divider {
+  height: 1rpx;
+  background: rgba(255,255,255,0.06);
+  margin: 0 32rpx;
+}
+
+.c-icon-wrap {
+  width: 68rpx;
+  height: 68rpx;
+  border-radius: 18rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.c-icon {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+.icon-gold   { background: linear-gradient(135deg, #b07800, #e0a800); }
+.icon-blue   { background: linear-gradient(135deg, #1755a8, #2878d8); }
+.icon-green  { background: linear-gradient(135deg, #0f6e40, #18a862); }
+.icon-purple { background: linear-gradient(135deg, #5a2ea8, #8855d8); }
+.icon-cyan   { background: linear-gradient(135deg, #0a6880, #12a0c0); }
+
+.c-body {
+  flex: 1;
+  min-width: 0;
+}
+
+.c-role {
+  display: block;
+  font-size: 22rpx;
+  color: rgba(160,195,240,0.6);
+  letter-spacing: 2rpx;
+  margin-bottom: 10rpx;
+}
+
+.c-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #e8eef8;
+}
+
+.c-name-long {
+  font-size: 26rpx;
+  line-height: 1.6;
+  color: #c8d8f0;
+}
+
+.member-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+  margin-top: 2rpx;
+}
+
+.member-tag {
+  font-size: 24rpx;
+  color: #90c0f0;
+  background: rgba(40,120,216,0.18);
+  border: 1rpx solid rgba(40,120,216,0.4);
+  border-radius: 30rpx;
+  padding: 6rpx 20rpx;
+}
+
+.member-tag-more {
+  font-size: 24rpx;
+  color: rgba(160,195,240,0.4);
+  padding: 6rpx 8rpx;
+  align-self: center;
+}
+
+/* ======== Footer ======== */
+.footer {
+  text-align: center;
+  padding: 40rpx 30rpx 20rpx;
+}
+
+.footer-deco {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+
+.footer-line {
+  flex: 1;
+  height: 1rpx;
+  background: rgba(180,140,90,0.3);
+}
+
+.footer-icon {
+  font-size: 20rpx;
+  color: rgba(180,140,90,0.5);
+}
+
+.copyright {
+  font-size: 22rpx;
+  color: #b8926a;
+  letter-spacing: 1rpx;
+}

+ 221 - 0
pages/lineage/lineage.js

@@ -0,0 +1,221 @@
+const app = getApp();
+
+Page({
+  data: {
+    searchKeyword: '',
+    searchResults: [],
+    center: null,
+    reversedGenerations: [],
+    siblings: [],
+    peers: [],
+    centerPeerIndex: 0,
+    scrollToCenterId: '',
+    children: [],
+    loading: false,
+    showDetail: false,
+    detailMember: null
+  },
+
+  onLoad: function () {},
+
+  onShow: function () {
+    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+      this.getTabBar().setData({ selected: 1 });
+    }
+  },
+
+  onSearchInput: function (e) {
+    this.setData({ searchKeyword: e.detail.value });
+  },
+
+  searchMember: function () {
+    const kw = this.data.searchKeyword.trim();
+    if (!kw) {
+      wx.showToast({ title: '请输入姓名', icon: 'none' });
+      return;
+    }
+    if (!app.globalData.isLoggedIn) {
+      wx.showModal({
+        title: '请先登录',
+        showCancel: false,
+        success: () => {
+          wx.navigateTo({ url: '/pages/login/login' });
+        }
+      });
+      return;
+    }
+    const searchUrl = `${app.globalData.baseUrl}/api/members/search`;
+    console.log('[Search] URL:', searchUrl);
+    console.log('[Search] keyword:', kw);
+    console.log('[Search] isLoggedIn:', app.globalData.isLoggedIn);
+    console.log('[Search] token:', app.globalData.token ? app.globalData.token.substring(0, 10) + '...' : 'null');
+
+    wx.request({
+      url: searchUrl,
+      method: 'GET',
+      data: { keyword: kw },
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        console.log('[Search] statusCode:', res.statusCode);
+        console.log('[Search] data:', JSON.stringify(res.data));
+        if (res.data && res.data.success) {
+          const list = res.data.data || [];
+          if (list.length === 0) {
+            wx.showToast({ title: '未找到相关成员', icon: 'none' });
+          } else if (list.length === 1) {
+            // 唯一结果,直接展示世系
+            this.setData({
+              searchResults: [],
+              searchKeyword: list[0].name,
+              loading: true,
+              center: null,
+              reversedGenerations: [],
+              siblings: [],
+              children: []
+            });
+            this.loadLineage(list[0].id);
+          } else {
+            this.setData({ searchResults: list });
+          }
+        } else {
+          wx.showToast({ title: (res.data && res.data.message) || '搜索失败', icon: 'none' });
+        }
+      },
+      fail: (err) => {
+        console.error('[Search] 网络请求失败:', JSON.stringify(err));
+        wx.showToast({ title: '网络失败:' + (err.errMsg || ''), icon: 'none', duration: 3000 });
+      }
+    });
+  },
+
+  selectMember: function (e) {
+    const member = e.currentTarget.dataset.member;
+    this.setData({
+      searchResults: [],
+      searchKeyword: member.name,
+      loading: true,
+      center: null,
+      reversedGenerations: [],
+      siblings: [],
+      children: []
+    });
+    this.loadLineage(member.id);
+  },
+
+  loadLineage: function (memberId) {
+    const lineageUrl = `${app.globalData.baseUrl}/api/lineage/${memberId}`;
+    console.log('[Lineage] 请求URL:', lineageUrl);
+    console.log('[Lineage] token:', app.globalData.token ? app.globalData.token.substring(0, 10) + '...' : 'null');
+    wx.showLoading({ title: '加载中...' });
+    wx.request({
+      url: lineageUrl,
+      method: 'GET',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        wx.hideLoading();
+        console.log('[Lineage] statusCode:', res.statusCode);
+        console.log('[Lineage] success:', res.data && res.data.success);
+        console.log('[Lineage] data keys:', res.data && res.data.data ? Object.keys(res.data.data) : 'none');
+        if (res.data && res.data.success) {
+          const d = res.data.data;
+          console.log('[Lineage] generations:', (d.generations || []).length);
+          console.log('[Lineage] siblings:', (d.siblings || []).length);
+          console.log('[Lineage] children:', (d.children || []).length);
+          console.log('[Lineage] center:', JSON.stringify(d.center));
+          const reversed = (d.generations || []).slice().reverse();
+          const center = d.center || null;
+          const siblings = d.siblings || [];
+
+          // 将 center 与兄弟合并,按 child_order 排序,null 默认为 1
+          let peers = [];
+          let centerPeerIndex = 0;
+          if (center) {
+            const allPeers = [
+              { ...center, isCenter: true },
+              ...siblings.map(s => ({ ...s, isCenter: false }))
+            ];
+            allPeers.sort((a, b) => {
+              const oa = (a.child_order || 1);
+              const ob = (b.child_order || 1);
+              return oa !== ob ? oa - ob : (a.id - b.id);
+            });
+            peers = allPeers;
+            centerPeerIndex = allPeers.findIndex(p => p.isCenter);
+          }
+
+          this.setData({
+            center,
+            reversedGenerations: reversed,
+            siblings,
+            children: d.children || [],
+            peers,
+            centerPeerIndex,
+            loading: false,
+            scrollToCenterId: 'peer-center'
+          });
+        } else {
+          console.error('[Lineage] 失败:', JSON.stringify(res.data));
+          wx.showToast({ title: (res.data && res.data.message) || '加载失败', icon: 'none' });
+          this.setData({ loading: false });
+        }
+      },
+      fail: (err) => {
+        wx.hideLoading();
+        console.error('[Lineage] 网络失败:', JSON.stringify(err));
+        wx.showToast({ title: '世系加载失败:' + (err.errMsg || ''), icon: 'none', duration: 3000 });
+        this.setData({ loading: false });
+      }
+    });
+  },
+
+  clearSearch: function () {
+    this.setData({
+      searchKeyword: '',
+      searchResults: [],
+      center: null,
+      reversedGenerations: [],
+      siblings: [],
+      peers: [],
+      centerPeerIndex: 0,
+      children: [],
+      loading: false
+    });
+  },
+
+  viewDetail: function (e) {
+    const member = e.currentTarget.dataset.member;
+    if (!member || !member.id) return;
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/${member.id}`,
+      method: 'GET',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        if (res.data && res.data.success) {
+          this.setData({ showDetail: true, detailMember: res.data.data });
+        }
+      }
+    });
+  },
+
+  switchCenter: function () {
+    if (!this.data.detailMember) return;
+    const m = this.data.detailMember;
+    this.setData({
+      showDetail: false,
+      detailMember: null,
+      searchKeyword: m.name,
+      loading: true,
+      center: null,
+      reversedGenerations: [],
+      siblings: [],
+      children: []
+    });
+    this.loadLineage(m.id);
+  },
+
+  closeDetail: function () {
+    this.setData({ showDetail: false, detailMember: null });
+  },
+
+  stopProp: function () {}
+});

+ 3 - 0
pages/lineage/lineage.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 223 - 0
pages/lineage/lineage.wxml

@@ -0,0 +1,223 @@
+<wxs src="../../utils/helpers.wxs" module="helpers"/>
+<view class="page">
+
+  <!-- ── 搜索栏 ── -->
+  <view class="search-bar">
+    <view class="search-input-wrap">
+      <text class="search-icon">🔍</text>
+      <input
+        class="search-input"
+        placeholder="搜索成员姓名"
+        value="{{searchKeyword}}"
+        bindinput="onSearchInput"
+        bindconfirm="searchMember"
+      />
+      <view class="clear-btn" wx:if="{{searchKeyword}}" bindtap="clearSearch">✕</view>
+    </view>
+    <button class="search-btn" bindtap="searchMember">查询</button>
+  </view>
+
+  <!-- ── 搜索结果下拉 ── -->
+  <view class="search-dropdown" wx:if="{{searchResults.length > 0}}">
+    <view
+      class="search-item"
+      wx:for="{{searchResults}}"
+      wx:key="id"
+      bindtap="selectMember"
+      data-member="{{item}}"
+    >
+      <text class="si-name">{{item.name}}{{item.simplified_name ? ' (' + item.simplified_name + ')' : ''}}</text>
+      <text class="si-gen">{{item.name_word_generation || ''}}</text>
+    </view>
+  </view>
+
+  <!-- ── 空状态 ── -->
+  <view class="empty-state" wx:if="{{!center && !loading && searchResults.length === 0}}">
+    <view class="empty-icon">🌳</view>
+    <text class="empty-title">世系查询</text>
+    <text class="empty-desc">输入成员姓名,查看完整家族世系脉络</text>
+  </view>
+
+  <!-- ── 加载中 ── -->
+  <view class="loading-state" wx:if="{{loading}}">
+    <view class="loading-dot"></view>
+    <text>加载中...</text>
+  </view>
+
+  <!-- ── 调试信息(有数据但不显示时排查用) ── -->
+  <view class="debug-bar" wx:if="{{center && !loading}}">
+    <text class="debug-text">✓ 已加载:{{center.name}} | 祖先{{reversedGenerations.length}}代 | 子女{{children.length}}人</text>
+  </view>
+
+  <!-- ══════════════════════════════════
+       世系树主体
+  ══════════════════════════════════ -->
+  <scroll-view scroll-y class="tree-scroll" wx:if="{{center && !loading}}">
+    <view class="tree-body">
+
+      <!-- ── 祖先链(由远及近,从上到下) ── -->
+      <block wx:for="{{reversedGenerations}}" wx:key="index" wx:for-item="gen">
+        <view class="gen-row">
+          <!-- 代际标签 -->
+          <view class="gen-label">
+            <text class="gen-label-text">{{helpers.getAncestorLabel(gen.depth)}}</text>
+          </view>
+          <!-- 本代主节点 + 兄弟横向滚动 -->
+          <view class="row-cards">
+            <view
+              class="card ancestor-card"
+              bindtap="viewDetail"
+              data-member="{{gen.ancestor}}"
+            >
+              <view class="card-name-wrap">
+                <text class="card-name">{{gen.ancestor.name}}</text>
+                <text class="card-simplified" wx:if="{{gen.ancestor.simplified_name && gen.ancestor.simplified_name !== gen.ancestor.name}}">
+                  ({{gen.ancestor.simplified_name}})
+                </text>
+              </view>
+              <text class="card-gen">{{gen.ancestor.name_word_generation || ''}}</text>
+            </view>
+
+            <!-- 该代兄弟 -->
+            <scroll-view scroll-x class="sibling-scroll" wx:if="{{gen.siblings.length > 0}}">
+              <view class="sibling-list">
+                <view
+                  class="card sibling-card"
+                  wx:for="{{gen.siblings}}"
+                  wx:key="id"
+                  wx:for-item="sib"
+                  bindtap="viewDetail"
+                  data-member="{{sib}}"
+                >
+                  <text class="card-name card-name-sm">{{sib.name}}</text>
+                  <text class="card-gen">{{sib.name_word_generation || ''}}</text>
+                </view>
+              </view>
+            </scroll-view>
+          </view>
+        </view>
+
+        <!-- 竖向连接线 -->
+        <view class="connector">
+          <view class="connector-line"></view>
+        </view>
+      </block>
+
+      <!-- ── 查询人物(含同辈,按第几子排序,center 居中) ── -->
+      <view class="gen-row center-row">
+        <view class="gen-label gen-label-center">
+          <text class="gen-label-text">查询人物</text>
+        </view>
+        <scroll-view
+          scroll-x
+          class="peer-scroll"
+          scroll-into-view="{{scrollToCenterId}}"
+          scroll-with-animation="{{true}}"
+        >
+          <view class="peer-list">
+            <view
+              wx:for="{{peers}}"
+              wx:key="id"
+              id="{{item.isCenter ? 'peer-center' : 'peer-' + item.id}}"
+              class="card {{item.isCenter ? 'center-card' : 'sibling-card'}}"
+              bindtap="viewDetail"
+              data-member="{{item}}"
+            >
+              <!-- 排行标签 -->
+              <view class="peer-order-badge {{item.isCenter ? 'order-badge-center' : ''}}">
+                <text class="peer-order-text">{{helpers.getChildOrderText(item.child_order)}}</text>
+              </view>
+              <view class="card-name-wrap">
+                <text class="card-name {{item.isCenter ? 'center-name' : 'card-name-sm'}}">{{item.name}}</text>
+                <text class="card-simplified {{item.isCenter ? 'center-simplified' : ''}}"
+                  wx:if="{{item.simplified_name && item.simplified_name !== item.name}}">
+                  ({{item.simplified_name}})
+                </text>
+              </view>
+              <text class="card-gen {{item.isCenter ? 'center-gen' : ''}}">{{item.name_word_generation || ''}}</text>
+              <view class="center-badge" wx:if="{{item.isCenter}}">查询人</view>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+
+      <!-- ── 子女 ── -->
+      <block wx:if="{{children.length > 0}}">
+        <view class="connector">
+          <view class="connector-line"></view>
+        </view>
+        <view class="children-section">
+          <view class="children-label">
+            <text class="children-label-text">子女</text>
+          </view>
+          <scroll-view scroll-x class="children-scroll">
+            <view class="children-list">
+              <view
+                class="card child-card"
+                wx:for="{{children}}"
+                wx:key="id"
+                bindtap="viewDetail"
+                data-member="{{item}}"
+              >
+                <view class="child-order-badge">
+                  <text class="child-order-text">{{helpers.getChildOrderText(item.child_order)}}</text>
+                </view>
+                <text class="card-name card-name-sm">{{item.name}}</text>
+                <text class="card-gen">{{item.name_word_generation || ''}}</text>
+                <view class="has-children-dot" wx:if="{{item.has_children}}">▼</view>
+              </view>
+            </view>
+          </scroll-view>
+        </view>
+      </block>
+
+    </view>
+  </scroll-view>
+
+  <!-- ══════════════════════════════════
+       成员详情弹窗
+  ══════════════════════════════════ -->
+  <view class="modal-mask" wx:if="{{showDetail}}" bindtap="closeDetail">
+    <view class="modal-box" catchtap="stopProp">
+      <view class="modal-header">
+        <text class="modal-title">成员详情</text>
+        <view class="modal-close" bindtap="closeDetail">✕</view>
+      </view>
+      <view class="modal-body" wx:if="{{detailMember}}">
+        <view class="detail-row">
+          <text class="dl">姓名</text>
+          <text class="dv">{{detailMember.name}}{{detailMember.simplified_name && detailMember.simplified_name !== detailMember.name ? ' (' + detailMember.simplified_name + ')' : ''}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">性别</text>
+          <text class="dv">{{detailMember.sex === 1 ? '男' : '女'}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">出生日期</text>
+          <text class="dv">{{detailMember.birthday_date || '-'}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">世系世代</text>
+          <text class="dv">{{detailMember.name_word_generation || '-'}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">堂内排行</text>
+          <text class="dv">{{detailMember.family_rank || '-'}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">婚姻状况</text>
+          <text class="dv">{{helpers.getMaritalStatusText(detailMember.marital_status)}}</text>
+        </view>
+        <view class="detail-row">
+          <text class="dl">是否过世</text>
+          <text class="dv">{{detailMember.is_pass_away === 0 ? '健在' : (detailMember.is_pass_away === 1 ? '已故' : '未知')}}</text>
+        </view>
+      </view>
+      <view class="modal-footer">
+        <button class="modal-btn btn-switch" bindtap="switchCenter">以此人为中心</button>
+        <button class="modal-btn btn-close" bindtap="closeDetail">关闭</button>
+      </view>
+    </view>
+  </view>
+
+</view>

+ 558 - 0
pages/lineage/lineage.wxss

@@ -0,0 +1,558 @@
+/* ════════════════════════════════
+   全局
+════════════════════════════════ */
+.page {
+  min-height: 100vh;
+  background: #0d1b2a;
+  padding-bottom: 40rpx;
+}
+
+/* ════════════════════════════════
+   搜索栏
+════════════════════════════════ */
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 24rpx 16rpx;
+  gap: 16rpx;
+  background: #0d1b2a;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+}
+
+.search-input-wrap {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background: #1a2e42;
+  border-radius: 48rpx;
+  padding: 0 24rpx;
+  height: 76rpx;
+  border: 1rpx solid #2d4a63;
+}
+
+.search-icon {
+  font-size: 30rpx;
+  margin-right: 12rpx;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 28rpx;
+  color: #e8f0fe;
+}
+
+.search-input::placeholder {
+  color: #4a6580;
+}
+
+.clear-btn {
+  font-size: 26rpx;
+  color: #4a6580;
+  padding: 8rpx;
+}
+
+.search-btn {
+  background: #c8960c;
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  border-radius: 48rpx;
+  padding: 0 32rpx;
+  height: 76rpx;
+  line-height: 76rpx;
+  border: none;
+  flex-shrink: 0;
+}
+
+.search-btn::after {
+  border: none;
+}
+
+/* ════════════════════════════════
+   搜索下拉
+════════════════════════════════ */
+.search-dropdown {
+  margin: 0 24rpx 8rpx;
+  background: #1a2e42;
+  border-radius: 16rpx;
+  border: 1rpx solid #2d4a63;
+  overflow: hidden;
+  z-index: 99;
+  position: relative;
+}
+
+.search-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 32rpx;
+  border-bottom: 1rpx solid #243a50;
+}
+
+.search-item:last-child {
+  border-bottom: none;
+}
+
+.si-name {
+  font-size: 30rpx;
+  color: #e8f0fe;
+  font-weight: 500;
+}
+
+.si-gen {
+  font-size: 24rpx;
+  color: #6b8fa8;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+}
+
+/* ════════════════════════════════
+   空状态 / 加载
+════════════════════════════════ */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 48rpx;
+  gap: 20rpx;
+}
+
+.empty-icon {
+  font-size: 100rpx;
+}
+
+.empty-title {
+  font-size: 36rpx;
+  color: #e8f0fe;
+  font-weight: bold;
+}
+
+.empty-desc {
+  font-size: 26rpx;
+  color: #4a6580;
+  text-align: center;
+  line-height: 1.6;
+}
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx;
+  gap: 20rpx;
+  color: #6b8fa8;
+  font-size: 28rpx;
+}
+
+.debug-bar {
+  background: #1a3a1a;
+  padding: 12rpx 24rpx;
+  margin: 0 24rpx 8rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #2a5a2a;
+}
+
+.debug-text {
+  font-size: 22rpx;
+  color: #6abf6a;
+}
+
+/* ════════════════════════════════
+   世系树主体
+════════════════════════════════ */
+.tree-scroll {
+  /* 减去搜索栏 + 自定义tabBar高度(约110rpx) */
+  height: calc(100vh - 140rpx - 110rpx);
+}
+
+.tree-body {
+  padding: 16rpx 0 60rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* ── 每代行 ── */
+.gen-row {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 16rpx;
+  box-sizing: border-box;
+}
+
+.gen-label {
+  padding: 4rpx 0 12rpx 0;
+  text-align: center;
+}
+
+.gen-label-text {
+  font-size: 22rpx;
+  color: #4a6580;
+  letter-spacing: 1rpx;
+}
+
+.gen-label-center .gen-label-text {
+  color: #c8960c;
+  font-weight: bold;
+  font-size: 24rpx;
+}
+
+.children-label {
+  padding: 4rpx 0 12rpx 0;
+  width: 100%;
+  text-align: center;
+}
+
+.children-label-text {
+  font-size: 22rpx;
+  color: #4a6580;
+  letter-spacing: 1rpx;
+}
+
+/* ── 卡片行(主节点 + 兄弟) ── */
+.row-cards {
+  width: 100%;
+  display: flex;
+  align-items: flex-start;
+  justify-content: center;
+  gap: 16rpx;
+  overflow-x: auto;
+}
+
+.center-row {
+  align-items: stretch;
+}
+
+/* ── 通用卡片 ── */
+.card {
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+  flex-shrink: 0;
+}
+
+.card-name-wrap {
+  display: flex;
+  align-items: baseline;
+  gap: 8rpx;
+  flex-wrap: wrap;
+}
+
+.card-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #fff;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-name-sm {
+  font-size: 28rpx;
+}
+
+.card-simplified {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.65);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex-shrink: 1;
+}
+
+.card-gen {
+  font-size: 21rpx;
+  color: rgba(255,255,255,0.55);
+  line-height: 1.4;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: normal;
+}
+
+/* ── 祖先卡 ── */
+.ancestor-card {
+  background: linear-gradient(135deg, #1e3a5f 0%, #2a5298 100%);
+  width: 220rpx;
+  min-width: 220rpx;
+  max-width: 220rpx;
+  box-shadow: 0 4rpx 16rpx rgba(42, 82, 152, 0.4);
+  border: 1rpx solid #2d5ea8;
+  overflow: hidden;
+}
+
+/* ── 兄弟卡 ── */
+.sibling-scroll {
+  flex: 1;
+  white-space: nowrap;
+}
+
+.sibling-list {
+  display: flex;
+  gap: 12rpx;
+  padding-right: 8rpx;
+}
+
+/* ── 查询人物同辈横排(含center居中) ── */
+.peer-scroll {
+  width: 100%;
+  white-space: nowrap;
+}
+
+.peer-list {
+  display: flex;
+  align-items: flex-start;
+  gap: 16rpx;
+  padding: 0 24rpx;
+}
+
+.peer-order-badge {
+  background: rgba(255,255,255,0.15);
+  border-radius: 8rpx;
+  padding: 4rpx 12rpx;
+  align-self: flex-start;
+  margin-bottom: 6rpx;
+}
+
+.order-badge-center {
+  background: rgba(255,255,255,0.25);
+}
+
+.peer-order-text {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.9);
+  font-weight: bold;
+}
+
+.center-badge {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.9);
+  background: rgba(0,0,0,0.2);
+  border-radius: 20rpx;
+  padding: 4rpx 16rpx;
+  margin-top: 8rpx;
+  align-self: center;
+}
+
+.sibling-card {
+  background: #1a3a5c;
+  width: 180rpx;
+  min-width: 180rpx;
+  max-width: 180rpx;
+  border: 1rpx solid #2d5280;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2);
+  overflow: hidden;
+}
+
+.sibling-card .card-name {
+  color: #a8c8f0;
+}
+
+.sibling-card .card-gen {
+  color: #4a6a8a;
+}
+
+/* ── 查询人物卡 ── */
+.center-card {
+  background: linear-gradient(135deg, #b07d10 0%, #d4a017 100%);
+  width: 220rpx;
+  min-width: 220rpx;
+  max-width: 220rpx;
+  box-shadow: 0 4rpx 24rpx rgba(212, 160, 23, 0.5);
+  border: 1rpx solid #e0b030;
+  overflow: hidden;
+}
+
+.center-name {
+  font-size: 32rpx;
+  color: #fff;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.center-simplified {
+  color: rgba(255,255,255,0.8);
+  font-size: 20rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex-shrink: 1;
+}
+
+.center-gen {
+  color: rgba(255,255,255,0.75);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: normal;
+}
+
+/* ── 子女区域 ── */
+.children-section {
+  width: 100%;
+  padding: 0 16rpx;
+  box-sizing: border-box;
+}
+
+.children-scroll {
+  white-space: nowrap;
+  width: 100%;
+}
+
+.children-list {
+  display: flex;
+  gap: 16rpx;
+  padding: 0 8rpx 8rpx;
+  justify-content: center;
+}
+
+.child-card {
+  background: linear-gradient(135deg, #1a3a2a 0%, #1e5c35 100%);
+  min-width: 160rpx;
+  max-width: 200rpx;
+  border: 1rpx solid #2a7a45;
+  box-shadow: 0 2rpx 10rpx rgba(30, 92, 53, 0.4);
+  position: relative;
+}
+
+.child-order-badge {
+  background: rgba(255,255,255,0.15);
+  border-radius: 8rpx;
+  padding: 4rpx 12rpx;
+  align-self: flex-start;
+  margin-bottom: 4rpx;
+}
+
+.child-order-text {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.9);
+  font-weight: bold;
+}
+
+.has-children-dot {
+  font-size: 18rpx;
+  color: rgba(255,255,255,0.4);
+  align-self: center;
+  margin-top: 4rpx;
+}
+
+/* ── 连接线 ── */
+.connector {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  height: 40rpx;
+  align-items: center;
+}
+
+.connector-line {
+  width: 3rpx;
+  height: 40rpx;
+  background: linear-gradient(to bottom, #2a5298, #4a6580);
+  border-radius: 2rpx;
+}
+
+/* ════════════════════════════════
+   详情弹窗
+════════════════════════════════ */
+.modal-mask {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0, 0, 0, 0.7);
+  display: flex;
+  align-items: flex-end;
+  z-index: 200;
+}
+
+.modal-box {
+  width: 100%;
+  background: #1a2e42;
+  border-radius: 32rpx 32rpx 0 0;
+  padding: 32rpx 32rpx 48rpx;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 32rpx;
+}
+
+.modal-title {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #e8f0fe;
+}
+
+.modal-close {
+  font-size: 28rpx;
+  color: #4a6580;
+  padding: 8rpx 16rpx;
+}
+
+.modal-body {
+  margin-bottom: 32rpx;
+}
+
+.detail-row {
+  display: flex;
+  padding: 18rpx 0;
+  border-bottom: 1rpx solid #243a50;
+}
+
+.detail-row:last-child {
+  border-bottom: none;
+}
+
+.dl {
+  font-size: 26rpx;
+  color: #4a6580;
+  width: 140rpx;
+  flex-shrink: 0;
+}
+
+.dv {
+  font-size: 26rpx;
+  color: #c8daf0;
+  flex: 1;
+}
+
+.modal-footer {
+  display: flex;
+  gap: 20rpx;
+}
+
+.modal-btn {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  font-size: 28rpx;
+  font-weight: bold;
+  border: none;
+}
+
+.modal-btn::after {
+  border: none;
+}
+
+.btn-switch {
+  background: linear-gradient(135deg, #b07d10, #d4a017);
+  color: #fff;
+}
+
+.btn-close {
+  background: #243a50;
+  color: #a8c8f0;
+}

+ 138 - 0
pages/login/login.js

@@ -0,0 +1,138 @@
+const app = getApp();
+
+Page({
+  data: {
+    isLoading: false
+  },
+
+  onLoad: function (options) {
+    if (app.globalData.isLoggedIn) {
+      this.goBack();
+    }
+  },
+
+  onGetPhoneNumber: function (e) {
+    if (this.data.isLoading) return;
+    
+    const detail = e.detail;
+    if (!detail.code) {
+      wx.showToast({
+        title: '授权失败',
+        icon: 'none'
+      });
+      return;
+    }
+
+    this.setData({ isLoading: true });
+
+    wx.login({
+      success: (loginRes) => {
+        if (loginRes.code) {
+          this.doLogin(loginRes.code, detail.code);
+        } else {
+          this.setData({ isLoading: false });
+          wx.showToast({
+            title: '获取登录凭证失败',
+            icon: 'none'
+          });
+        }
+      },
+      fail: () => {
+        this.setData({ isLoading: false });
+        wx.showToast({
+          title: '登录失败',
+          icon: 'none'
+        });
+      }
+    });
+  },
+
+  doLogin: function (loginCode, phoneCode) {
+    const requestUrl = `${app.globalData.baseUrl}/api/wechat/login`;
+    console.log('[Login] baseUrl:', app.globalData.baseUrl);
+    console.log('[Login] 请求地址:', requestUrl);
+    console.log('[Login] loginCode:', loginCode ? loginCode.substring(0, 10) + '...' : 'empty');
+    console.log('[Login] phoneCode:', phoneCode ? phoneCode.substring(0, 10) + '...' : 'empty');
+
+    wx.request({
+      url: requestUrl,
+      method: 'POST',
+      data: {
+        code: loginCode,
+        phoneCode: phoneCode
+      },
+      timeout: 30000,
+      success: (response) => {
+        this.setData({ isLoading: false });
+        console.log('[Login] 响应状态码:', response.statusCode);
+        console.log('[Login] 响应数据:', JSON.stringify(response.data));
+
+        if (response.data && response.data.success) {
+          app.globalData.token = response.data.token;
+          app.globalData.userInfo = response.data.user;
+          app.globalData.isLoggedIn = true;
+          
+          wx.setStorageSync('token', response.data.token);
+          wx.setStorageSync('userInfo', response.data.user);
+          
+          wx.showToast({
+            title: '登录成功',
+            icon: 'success'
+          });
+          
+          setTimeout(() => {
+            this.goBack();
+          }, 1500);
+        } else {
+          console.warn('[Login] 登录失败,服务端返回:', response.data);
+          wx.showToast({
+            title: response.data && response.data.message || '登录失败',
+            icon: 'none'
+          });
+        }
+      },
+      fail: (err) => {
+        this.setData({ isLoading: false });
+        console.error('[Login] 网络请求失败:', JSON.stringify(err));
+        console.error('[Login] errMsg:', err.errMsg);
+        console.error('[Login] errno:', err.errno);
+        wx.showToast({
+          title: '网络请求失败,请重试',
+          icon: 'none'
+        });
+      },
+      complete: () => {
+        if (this.data.isLoading) {
+          this.setData({ isLoading: false });
+        }
+      }
+    });
+  },
+
+  goBack: function () {
+    const pages = getCurrentPages();
+    if (pages.length > 1) {
+      wx.navigateBack();
+    } else {
+      wx.switchTab({
+        url: '/pages/index/index'
+      });
+    }
+  },
+
+  goToAgreement: function () {
+    wx.showModal({
+      title: '用户协议',
+      content: '用户协议内容:您同意使用本服务即表示同意遵守相关条款...',
+      showCancel: false
+    });
+  },
+
+  goToPrivacy: function () {
+    wx.showModal({
+      title: '隐私政策',
+      content: '隐私政策内容:我们重视您的隐私,将妥善保管您的个人信息...',
+      showCancel: false
+    });
+  }
+});

+ 3 - 0
pages/login/login.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 37 - 0
pages/login/login.wxml

@@ -0,0 +1,37 @@
+<view class="login-container">
+  <view class="login-header">
+    <view class="logo">
+      <view class="logo-circle">
+        <view class="logo-tree"></view>
+      </view>
+    </view>
+    <view class="title">留家族族谱</view>
+    <view class="subtitle">传承家族文化,记录血脉亲情</view>
+  </view>
+
+  <view class="login-content">
+    <view class="agreement-tip">
+      <text>登录即表示同意</text>
+      <text class="link" bindtap="goToAgreement">《用户协议》</text>
+      <text>和</text>
+      <text class="link" bindtap="goToPrivacy">《隐私政策》</text>
+    </view>
+
+    <button 
+      class="login-btn" 
+      open-type="getRealtimePhoneNumber" 
+      bindgetrealtimephonenumber="onGetPhoneNumber"
+    >
+      <view class="btn-content">
+        <view class="btn-icon">
+          <view class="phone-icon"></view>
+        </view>
+        <text class="btn-text">微信一键登录</text>
+      </view>
+    </button>
+  </view>
+
+  <view class="login-footer">
+    <text class="footer-text">© 2025 留家族族谱管理系统</text>
+  </view>
+</view>

+ 126 - 0
pages/login/login.wxss

@@ -0,0 +1,126 @@
+.login-container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #1a365d 0%, #2c5282 100%);
+  display: flex;
+  flex-direction: column;
+}
+
+.login-header {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding-top: 100rpx;
+}
+
+.logo {
+  margin-bottom: 40rpx;
+}
+
+.logo-circle {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.15);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 4rpx solid rgba(255, 255, 255, 0.3);
+}
+
+.logo-tree {
+  width: 80rpx;
+  height: 80rpx;
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.title {
+  font-size: 56rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 16rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+.login-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 48rpx;
+}
+
+.agreement-tip {
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.6);
+  margin-bottom: 48rpx;
+  text-align: center;
+}
+
+.agreement-tip .link {
+  color: #63b3ed;
+}
+
+.login-btn {
+  width: 100%;
+  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
+  border-radius: 48rpx;
+  padding: 32rpx;
+  box-shadow: 0 8rpx 32rpx rgba(72, 187, 120, 0.4);
+  /* 重置 button 默认样式 */
+  border: none;
+  margin: 0;
+  line-height: normal;
+  font-size: inherit;
+  color: inherit;
+}
+
+.login-btn::after {
+  border: none;
+}
+
+.login-btn:active {
+  opacity: 0.8;
+  transform: scale(0.98);
+}
+
+.btn-content {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.btn-icon {
+  width: 48rpx;
+  height: 48rpx;
+  margin-right: 20rpx;
+}
+
+.phone-icon {
+  width: 100%;
+  height: 100%;
+  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 100%;
+}
+
+.btn-text {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.login-footer {
+  padding: 48rpx 0;
+  text-align: center;
+}
+
+.footer-text {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.5);
+}

+ 81 - 0
pages/member_detail/member_detail.js

@@ -0,0 +1,81 @@
+const app = getApp();
+
+Page({
+  data: {
+    member: null
+  },
+
+  onLoad: function (options) {
+    if (options && options.id) {
+      this.setData({
+        memberId: options.id
+      });
+    }
+  },
+
+  onShow: function () {
+    if (this.data.memberId && !this.data.member) {
+      this.loadMember(this.data.memberId);
+    }
+  },
+
+  // 加载成员信息
+  loadMember: function (memberId) {
+    if (!app.globalData.isLoggedIn) {
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      return;
+    }
+    
+    wx.showLoading({
+      title: '加载中...'
+    });
+
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/${memberId}`,
+      method: 'GET',
+      header: {
+        'Authorization': `Bearer ${app.globalData.token}`
+      },
+      timeout: 10000,
+      success: (res) => {
+        wx.hideLoading();
+        if (res.data && res.data.success) {
+          this.setData({
+            member: res.data.data
+          });
+        } else {
+          wx.showToast({
+            title: '加载失败',
+            icon: 'none'
+          });
+        }
+      },
+      fail: () => {
+        wx.hideLoading();
+        wx.showToast({
+          title: '网络异常',
+          icon: 'none'
+        });
+      }
+    });
+  },
+
+  // 获取婚姻状况文本
+  getMaritalStatusText: function (status) {
+    const statusMap = {
+      0: '未知',
+      1: '未婚',
+      2: '已婚',
+      3: '离异/丧偶'
+    };
+    return statusMap[status] || '未知';
+  },
+
+  // 返回
+  goBack: function () {
+    wx.navigateBack();
+  }
+});

+ 3 - 0
pages/member_detail/member_detail.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 157 - 0
pages/member_detail/member_detail.wxml

@@ -0,0 +1,157 @@
+<wxs src="../../utils/helpers.wxs" module="helpers"/>
+<view class="container">
+  <!-- 页面标题 -->
+  <view class="page-header">
+    <text class="page-title">成员详情</text>
+  </view>
+
+  <!-- 成员信息卡片 -->
+  <view class="info-card" wx:if="{{member}}">
+    <!-- 基本信息 -->
+    <view class="card-section">
+      <view class="section-title">基本信息</view>
+      <view class="info-grid">
+        <view class="info-item">
+          <text class="info-label">姓名(繁体)</text>
+          <text class="info-value">{{member.name}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">姓名(简体)</text>
+          <text class="info-value">{{member.simplified_name || '-'}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">性别</text>
+          <text class="info-value">{{member.sex === 1 ? '男' : '女'}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">出生日期</text>
+          <text class="info-value">{{member.birthday_date || '未知'}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">世系世代</text>
+          <text class="info-value">{{member.name_word_generation || '-'}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">堂内排行</text>
+          <text class="info-value">{{member.family_rank || '-'}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">婚姻状况</text>
+          <text class="info-value">{{helpers.getMaritalStatusText(member.marital_status)}}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">是否过世</text>
+          <text class="info-value">{{member.is_pass_away === 0 ? '健在' : (member.is_pass_away === 1 ? '已故' : '未知')}}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 谱系详情 -->
+    <view class="card-section" wx:if="{{member.former_name || member.childhood_name || member.name_word || member.name_title || member.branch_family_hall || member.cluster_place}}">
+      <view class="section-title">谱系详情</view>
+      <view class="info-grid">
+        <view class="info-item" wx:if="{{member.former_name}}">
+          <text class="info-label">曾用名</text>
+          <text class="info-value">{{member.former_name}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.childhood_name}}">
+          <text class="info-label">幼名/乳名</text>
+          <text class="info-value">{{member.childhood_name}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.name_word}}">
+          <text class="info-label">字辈</text>
+          <text class="info-value">{{member.name_word}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.name_title}}">
+          <text class="info-label">名号/封号</text>
+          <text class="info-value">{{member.name_title}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.branch_family_hall}}">
+          <text class="info-label">分房/堂号</text>
+          <text class="info-value">{{member.branch_family_hall}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.cluster_place}}">
+          <text class="info-label">聚居地</text>
+          <text class="info-value">{{member.cluster_place}}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 联络信息 -->
+    <view class="card-section" wx:if="{{member.nation || member.phone || member.wechat_account || member.residential_address}}">
+      <view class="section-title">联络信息</view>
+      <view class="info-grid">
+        <view class="info-item" wx:if="{{member.nation}}">
+          <text class="info-label">民族</text>
+          <text class="info-value">{{member.nation}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.phone}}">
+          <text class="info-label">手机号</text>
+          <text class="info-value">{{member.phone}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.wechat_account}}">
+          <text class="info-label">微信号</text>
+          <text class="info-value">{{member.wechat_account}}</text>
+        </view>
+        <view class="info-item full-width" wx:if="{{member.residential_address}}">
+          <text class="info-label">现居住址</text>
+          <text class="info-value">{{member.residential_address}}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 个人履历 -->
+    <view class="card-section" wx:if="{{member.occupation || member.educational || member.tags || member.personal_achievements}}">
+      <view class="section-title">个人履历</view>
+      <view class="info-grid">
+        <view class="info-item" wx:if="{{member.occupation}}">
+          <text class="info-label">职业</text>
+          <text class="info-value">{{member.occupation}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.educational}}">
+          <text class="info-label">教育背景</text>
+          <text class="info-value">{{member.educational}}</text>
+        </view>
+        <view class="info-item" wx:if="{{member.tags}}">
+          <text class="info-label">标签</text>
+          <text class="info-value">{{member.tags}}</text>
+        </view>
+        <view class="info-item full-width" wx:if="{{member.personal_achievements}}">
+          <text class="info-label">个人成就</text>
+          <text class="info-value">{{member.personal_achievements}}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 族谱原文 -->
+    <view class="card-section" wx:if="{{member.genealogy_original_traditional || member.genealogy_original_simplified}}">
+      <view class="section-title">族谱原文</view>
+      <view class="genealogy-content">
+        <view class="genealogy-item" wx:if="{{member.genealogy_original_traditional}}">
+          <text class="genealogy-label">繁体</text>
+          <text class="genealogy-text">{{member.genealogy_original_traditional}}</text>
+        </view>
+        <view class="genealogy-item" wx:if="{{member.genealogy_original_simplified}}">
+          <text class="genealogy-label">简体</text>
+          <text class="genealogy-text">{{member.genealogy_original_simplified}}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 备注 -->
+    <view class="card-section" wx:if="{{member.notes}}">
+      <view class="section-title">备注</view>
+      <text class="notes-text">{{member.notes}}</text>
+    </view>
+  </view>
+
+  <!-- 空状态 -->
+  <view class="empty-state" wx:else>
+    <text class="empty-text">暂无成员信息</text>
+  </view>
+
+  <!-- 返回按钮 -->
+  <view class="back-section">
+    <button class="back-btn" bindtap="goBack">返回</button>
+  </view>
+</view>

+ 141 - 0
pages/member_detail/member_detail.wxss

@@ -0,0 +1,141 @@
+.container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-bottom: 140rpx;
+}
+
+/* 页面标题 */
+.page-header {
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  padding: 40rpx 30rpx;
+}
+
+.page-title {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+/* 信息卡片 */
+.info-card {
+  margin: 20rpx;
+}
+
+/* 卡片章节 */
+.card-section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  padding-left: 16rpx;
+  border-left: 6rpx solid #8B4513;
+}
+
+/* 信息网格 */
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20rpx;
+}
+
+.info-item {
+  display: flex;
+  flex-direction: column;
+}
+
+.info-item.full-width {
+  grid-column: span 2;
+}
+
+.info-label {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+
+.info-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+/* 族谱原文 */
+.genealogy-content {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.genealogy-item {
+  background: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 20rpx;
+}
+
+.genealogy-label {
+  font-size: 24rpx;
+  color: #8B4513;
+  font-weight: 600;
+  margin-bottom: 8rpx;
+  display: block;
+}
+
+.genealogy-text {
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.6;
+}
+
+/* 备注 */
+.notes-text {
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.6;
+}
+
+/* 空状态 */
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* 返回按钮 */
+.back-section {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.back-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
+  border-radius: 44rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #fff;
+  border: none;
+}
+
+.back-btn:active {
+  opacity: 0.8;
+}

+ 170 - 0
pages/my_entries/my_entries.js

@@ -0,0 +1,170 @@
+const app = getApp();
+
+Page({
+  data: {
+    members: [],
+    loading: true,
+    empty: false,
+    showDetail: false,
+    detailMember: null,
+    showEditModal: false,
+    editForm: {},
+    isPassAwayOptions: ['健在', '已故', '未知'],
+    maritalStatusOptions: ['未知', '未婚', '已婚', '离异/丧偶'],
+    sexOptions: ['未知', '男', '女'],
+    submitting: false
+  },
+
+  onShow() {
+    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+      this.getTabBar().setData({ selected: 2 });
+    }
+    this.loadMyEntries();
+  },
+
+  loadMyEntries() {
+    this.setData({ loading: true, empty: false });
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/my`,
+      method: 'GET',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        if (res.data && res.data.success) {
+          const members = res.data.data || [];
+          this.setData({
+            members,
+            loading: false,
+            empty: members.length === 0
+          });
+        } else {
+          wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' });
+          this.setData({ loading: false, empty: true });
+        }
+      },
+      fail: () => {
+        wx.showToast({ title: '网络请求失败', icon: 'none' });
+        this.setData({ loading: false, empty: true });
+      }
+    });
+  },
+
+  onRefresh() {
+    this.loadMyEntries();
+  },
+
+  viewDetail(e) {
+    const member = e.currentTarget.dataset.member;
+    this.setData({ showDetail: true, detailMember: member });
+  },
+
+  closeDetail() {
+    this.setData({ showDetail: false, detailMember: null });
+  },
+
+  openEdit(e) {
+    const member = e.currentTarget.dataset.member;
+    this.setData({
+      showEditModal: true,
+      showDetail: false,
+      editForm: {
+        id: member.id,
+        name: member.name || '',
+        simplified_name: member.simplified_name || '',
+        sex: member.sex != null ? member.sex : 1,
+        birthday: member.birthday_date || '',
+        family_rank: member.family_rank || '',
+        name_word_generation: member.name_word_generation || '',
+        is_pass_away: member.is_pass_away || 0,
+        marital_status: member.marital_status || 0,
+        phone: member.phone || '',
+        notes: member.notes || ''
+      }
+    });
+  },
+
+  closeEdit() {
+    this.setData({ showEditModal: false });
+  },
+
+  onEditInput(e) {
+    const field = e.currentTarget.dataset.field;
+    const editForm = { ...this.data.editForm };
+    editForm[field] = e.detail.value;
+    this.setData({ editForm });
+  },
+
+  onPickerChange(e) {
+    const field = e.currentTarget.dataset.field;
+    const editForm = { ...this.data.editForm };
+    editForm[field] = parseInt(e.detail.value);
+    this.setData({ editForm });
+  },
+
+  submitEdit() {
+    if (this.data.submitting) return;
+    const form = this.data.editForm;
+    if (!form.name || !form.name.trim()) {
+      wx.showToast({ title: '姓名不能为空', icon: 'none' });
+      return;
+    }
+    this.setData({ submitting: true });
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/member/${form.id}`,
+      method: 'PUT',
+      header: {
+        'Authorization': `Bearer ${app.globalData.token}`,
+        'Content-Type': 'application/json'
+      },
+      data: form,
+      success: (res) => {
+        this.setData({ submitting: false });
+        if (res.data && res.data.success) {
+          wx.showToast({ title: '修改成功', icon: 'success' });
+          this.setData({ showEditModal: false });
+          this.loadMyEntries();
+        } else {
+          wx.showToast({ title: res.data?.message || '修改失败', icon: 'none' });
+        }
+      },
+      fail: () => {
+        this.setData({ submitting: false });
+        wx.showToast({ title: '网络请求失败', icon: 'none' });
+      }
+    });
+  },
+
+  confirmDelete(e) {
+    const member = e.currentTarget.dataset.member;
+    wx.showModal({
+      title: '确认删除',
+      content: `确定要删除「${member.name}」的录入信息吗?此操作不可撤销。`,
+      confirmColor: '#e74c3c',
+      success: (res) => {
+        if (res.confirm) {
+          this.deleteMember(member.id);
+        }
+      }
+    });
+  },
+
+  deleteMember(id) {
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/members/${id}`,
+      method: 'DELETE',
+      header: { 'Authorization': `Bearer ${app.globalData.token}` },
+      success: (res) => {
+        if (res.data && res.data.success) {
+          wx.showToast({ title: '删除成功', icon: 'success' });
+          this.loadMyEntries();
+        } else {
+          wx.showToast({ title: res.data?.message || '删除失败', icon: 'none' });
+        }
+      },
+      fail: () => {
+        wx.showToast({ title: '网络请求失败', icon: 'none' });
+      }
+    });
+  },
+
+  stopProp(e) {}
+});

+ 4 - 0
pages/my_entries/my_entries.json

@@ -0,0 +1,4 @@
+{
+  "navigationBarTitleText": "我的录入",
+  "usingComponents": {}
+}

+ 142 - 0
pages/my_entries/my_entries.wxml

@@ -0,0 +1,142 @@
+<view class="page">
+
+  <!-- Loading -->
+  <view class="center-tip" wx:if="{{loading}}">
+    <view class="loading-spinner"></view>
+    <text class="tip-text">加载中...</text>
+  </view>
+
+  <!-- Empty -->
+  <view class="center-tip" wx:elif="{{empty}}">
+    <text class="empty-icon">📋</text>
+    <text class="tip-text">还没有录入任何成员</text>
+    <text class="tip-sub">去「录入成员」添加第一条记录吧</text>
+  </view>
+
+  <!-- List -->
+  <scroll-view scroll-y class="list-wrap" wx:else>
+    <view class="list-header">
+      <text class="list-count">共 {{members.length}} 条录入</text>
+      <view class="refresh-btn" bindtap="onRefresh">
+        <text class="refresh-icon">↻</text>
+        <text>刷新</text>
+      </view>
+    </view>
+
+    <view class="member-card" wx:for="{{members}}" wx:key="id">
+      <view class="card-main" bindtap="viewDetail" data-member="{{item}}">
+        <view class="card-avatar sex-{{item.sex}}">
+          <text>{{item.name[0]}}</text>
+        </view>
+        <view class="card-info">
+          <view class="card-name-row">
+            <text class="card-name">{{item.name}}</text>
+            <text class="card-tag sex-tag-{{item.sex}}">{{item.sex === 1 ? '男' : item.sex === 2 ? '女' : ''}}</text>
+            <text class="card-tag pass-tag" wx:if="{{item.is_pass_away === 1}}">已故</text>
+          </view>
+          <view class="card-meta">
+            <text wx:if="{{item.birthday_date}}">{{item.birthday_date}}</text>
+            <text wx:if="{{item.family_rank}}" class="meta-sep">· 第{{item.family_rank}}世</text>
+            <text wx:if="{{item.name_word_generation}}" class="meta-sep">· {{item.name_word_generation}}字辈</text>
+          </view>
+          <text class="card-time">录入于 {{item.create_time}}</text>
+        </view>
+      </view>
+      <view class="card-actions">
+        <view class="action-btn view-btn" bindtap="viewDetail" data-member="{{item}}">查看</view>
+        <view class="action-btn edit-btn" bindtap="openEdit" data-member="{{item}}">编辑</view>
+        <view class="action-btn del-btn" bindtap="confirmDelete" data-member="{{item}}">删除</view>
+      </view>
+    </view>
+  </scroll-view>
+
+  <!-- Detail Modal -->
+  <view class="modal-mask" wx:if="{{showDetail}}" bindtap="closeDetail">
+    <view class="modal-box" catchtap="stopProp">
+      <view class="modal-header">
+        <text class="modal-title">成员详情</text>
+        <view class="modal-close" bindtap="closeDetail">✕</view>
+      </view>
+      <scroll-view scroll-y class="modal-body">
+        <view class="detail-row"><text class="detail-label">姓名</text><text class="detail-value">{{detailMember.name}}</text></view>
+        <view class="detail-row" wx:if="{{detailMember.simplified_name}}"><text class="detail-label">简名</text><text class="detail-value">{{detailMember.simplified_name}}</text></view>
+        <view class="detail-row"><text class="detail-label">性别</text><text class="detail-value">{{detailMember.sex === 1 ? '男' : detailMember.sex === 2 ? '女' : '未知'}}</text></view>
+        <view class="detail-row" wx:if="{{detailMember.birthday_date}}"><text class="detail-label">出生日期</text><text class="detail-value">{{detailMember.birthday_date}}</text></view>
+        <view class="detail-row" wx:if="{{detailMember.family_rank}}"><text class="detail-label">世序</text><text class="detail-value">第{{detailMember.family_rank}}世</text></view>
+        <view class="detail-row" wx:if="{{detailMember.name_word_generation}}"><text class="detail-label">字辈</text><text class="detail-value">{{detailMember.name_word_generation}}</text></view>
+        <view class="detail-row"><text class="detail-label">状态</text><text class="detail-value">{{detailMember.is_pass_away === 1 ? '已故' : '健在'}}</text></view>
+        <view class="detail-row"><text class="detail-label">婚姻</text><text class="detail-value">{{detailMember.marital_status === 1 ? '未婚' : detailMember.marital_status === 2 ? '已婚' : detailMember.marital_status === 3 ? '离异/丧偶' : '未知'}}</text></view>
+        <view class="detail-row" wx:if="{{detailMember.phone}}"><text class="detail-label">手机</text><text class="detail-value">{{detailMember.phone}}</text></view>
+        <view class="detail-row" wx:if="{{detailMember.notes}}"><text class="detail-label">备注</text><text class="detail-value">{{detailMember.notes}}</text></view>
+        <view class="detail-row"><text class="detail-label">录入时间</text><text class="detail-value">{{detailMember.create_time}}</text></view>
+      </scroll-view>
+      <view class="modal-footer">
+        <view class="modal-btn edit-btn" bindtap="openEdit" data-member="{{detailMember}}">编辑</view>
+        <view class="modal-btn del-btn" bindtap="confirmDelete" data-member="{{detailMember}}">删除</view>
+      </view>
+    </view>
+  </view>
+
+  <!-- Edit Modal -->
+  <view class="modal-mask" wx:if="{{showEditModal}}" bindtap="closeEdit">
+    <view class="modal-box edit-modal-box" catchtap="stopProp">
+      <view class="modal-header">
+        <text class="modal-title">编辑成员信息</text>
+        <view class="modal-close" bindtap="closeEdit">✕</view>
+      </view>
+      <scroll-view scroll-y class="modal-body edit-modal-body">
+        <view class="form-item">
+          <text class="form-label">姓名 <text class="required">*</text></text>
+          <input class="form-input" value="{{editForm.name}}" placeholder="请输入姓名" data-field="name" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">简名</text>
+          <input class="form-input" value="{{editForm.simplified_name}}" placeholder="简化姓名(可选)" data-field="simplified_name" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">性别</text>
+          <picker mode="selector" range="{{sexOptions}}" value="{{editForm.sex}}" data-field="sex" bindchange="onPickerChange">
+            <view class="form-picker">{{sexOptions[editForm.sex] || '请选择'}}</view>
+          </picker>
+        </view>
+        <view class="form-item">
+          <text class="form-label">出生日期</text>
+          <input class="form-input" value="{{editForm.birthday}}" placeholder="YYYY-MM-DD" data-field="birthday" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">世序</text>
+          <input class="form-input" type="number" value="{{editForm.family_rank}}" placeholder="第几世" data-field="family_rank" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">字辈</text>
+          <input class="form-input" value="{{editForm.name_word_generation}}" placeholder="字辈" data-field="name_word_generation" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">状态</text>
+          <picker mode="selector" range="{{isPassAwayOptions}}" value="{{editForm.is_pass_away}}" data-field="is_pass_away" bindchange="onPickerChange">
+            <view class="form-picker">{{isPassAwayOptions[editForm.is_pass_away] || '请选择'}}</view>
+          </picker>
+        </view>
+        <view class="form-item">
+          <text class="form-label">婚姻状况</text>
+          <picker mode="selector" range="{{maritalStatusOptions}}" value="{{editForm.marital_status}}" data-field="marital_status" bindchange="onPickerChange">
+            <view class="form-picker">{{maritalStatusOptions[editForm.marital_status] || '请选择'}}</view>
+          </picker>
+        </view>
+        <view class="form-item">
+          <text class="form-label">手机号</text>
+          <input class="form-input" type="number" value="{{editForm.phone}}" placeholder="手机号(可选)" data-field="phone" bindinput="onEditInput" />
+        </view>
+        <view class="form-item">
+          <text class="form-label">备注</text>
+          <textarea class="form-textarea" value="{{editForm.notes}}" placeholder="备注信息(可选)" data-field="notes" bindinput="onEditInput" />
+        </view>
+      </scroll-view>
+      <view class="modal-footer">
+        <view class="modal-btn cancel-btn" bindtap="closeEdit">取消</view>
+        <view class="modal-btn submit-btn {{submitting ? 'disabled' : ''}}" bindtap="submitEdit">{{submitting ? '保存中...' : '保存'}}</view>
+      </view>
+    </view>
+  </view>
+
+</view>

+ 370 - 0
pages/my_entries/my_entries.wxss

@@ -0,0 +1,370 @@
+page {
+  background: #f4f6fb;
+  height: 100%;
+}
+
+.page {
+  min-height: 100vh;
+  background: #f4f6fb;
+}
+
+/* ---- Tips ---- */
+.center-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding-top: 160rpx;
+  gap: 20rpx;
+}
+
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 6rpx solid #e0e0e0;
+  border-top-color: #4a90e2;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.empty-icon {
+  font-size: 80rpx;
+}
+
+.tip-text {
+  font-size: 32rpx;
+  color: #555;
+  font-weight: 500;
+}
+
+.tip-sub {
+  font-size: 26rpx;
+  color: #aaa;
+}
+
+/* ---- List ---- */
+.list-wrap {
+  height: calc(100vh - 0px);
+  padding: 0 0 40rpx;
+}
+
+.list-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24rpx 32rpx 16rpx;
+}
+
+.list-count {
+  font-size: 26rpx;
+  color: #888;
+}
+
+.refresh-btn {
+  display: flex;
+  align-items: center;
+  gap: 6rpx;
+  font-size: 26rpx;
+  color: #4a90e2;
+}
+
+.refresh-icon {
+  font-size: 30rpx;
+}
+
+/* ---- Member Card ---- */
+.member-card {
+  background: #fff;
+  border-radius: 20rpx;
+  margin: 0 24rpx 24rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
+  overflow: hidden;
+}
+
+.card-main {
+  display: flex;
+  align-items: center;
+  padding: 28rpx 28rpx 20rpx;
+  gap: 24rpx;
+}
+
+.card-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #fff;
+  flex-shrink: 0;
+  background: #7ec8e3;
+}
+
+.card-avatar.sex-1 {
+  background: linear-gradient(135deg, #4a90e2, #357abd);
+}
+
+.card-avatar.sex-2 {
+  background: linear-gradient(135deg, #e25c8a, #c0396e);
+}
+
+.card-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.card-name-row {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-wrap: wrap;
+}
+
+.card-name {
+  font-size: 34rpx;
+  font-weight: 700;
+  color: #1a1a2e;
+}
+
+.card-tag {
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 20rpx;
+  line-height: 1.4;
+}
+
+.sex-tag-1 {
+  background: #e8f1fd;
+  color: #4a90e2;
+}
+
+.sex-tag-2 {
+  background: #fde8f1;
+  color: #e25c8a;
+}
+
+.pass-tag {
+  background: #f0f0f0;
+  color: #999;
+}
+
+.card-meta {
+  font-size: 24rpx;
+  color: #888;
+  margin-top: 8rpx;
+}
+
+.meta-sep {
+  margin-left: 4rpx;
+}
+
+.card-time {
+  font-size: 22rpx;
+  color: #bbb;
+  margin-top: 8rpx;
+  display: block;
+}
+
+/* ---- Card Actions ---- */
+.card-actions {
+  display: flex;
+  border-top: 1rpx solid #f0f0f0;
+}
+
+.action-btn {
+  flex: 1;
+  text-align: center;
+  padding: 22rpx 0;
+  font-size: 28rpx;
+  font-weight: 500;
+}
+
+.view-btn {
+  color: #4a90e2;
+  border-right: 1rpx solid #f0f0f0;
+}
+
+.edit-btn {
+  color: #f5a623;
+  border-right: 1rpx solid #f0f0f0;
+}
+
+.del-btn {
+  color: #e74c3c;
+}
+
+/* ---- Modal ---- */
+.modal-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,0.5);
+  z-index: 100;
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+}
+
+.modal-box {
+  background: #fff;
+  border-radius: 32rpx 32rpx 0 0;
+  width: 100%;
+  height: 80vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.edit-modal-box {
+  height: 92vh;
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 32rpx 40rpx 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  flex-shrink: 0;
+}
+
+.modal-title {
+  font-size: 34rpx;
+  font-weight: 700;
+  color: #1a1a2e;
+}
+
+.modal-close {
+  font-size: 36rpx;
+  color: #aaa;
+  padding: 8rpx;
+}
+
+.modal-body {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.edit-modal-body {
+  padding: 0 40rpx;
+}
+
+/* Detail rows */
+.detail-row {
+  display: flex;
+  padding: 24rpx 40rpx;
+  border-bottom: 1rpx solid #f8f8f8;
+  align-items: flex-start;
+}
+
+.detail-label {
+  width: 140rpx;
+  font-size: 28rpx;
+  color: #888;
+  flex-shrink: 0;
+}
+
+.detail-value {
+  flex: 1;
+  font-size: 28rpx;
+  color: #1a1a2e;
+  word-break: break-all;
+}
+
+/* Modal footer */
+.modal-footer {
+  display: flex;
+  padding: 24rpx 40rpx 48rpx;
+  gap: 24rpx;
+  border-top: 1rpx solid #f0f0f0;
+  flex-shrink: 0;
+}
+
+.modal-btn {
+  flex: 1;
+  text-align: center;
+  padding: 26rpx 0;
+  border-radius: 50rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+}
+
+.edit-btn {
+  background: linear-gradient(135deg, #f5a623, #e8901a);
+  color: #fff;
+}
+
+.del-btn {
+  background: linear-gradient(135deg, #e74c3c, #c0392b);
+  color: #fff;
+}
+
+.cancel-btn {
+  background: #f0f0f0;
+  color: #666;
+}
+
+.submit-btn {
+  background: linear-gradient(135deg, #4a90e2, #357abd);
+  color: #fff;
+}
+
+.submit-btn.disabled {
+  opacity: 0.6;
+}
+
+/* ---- Edit form ---- */
+.form-item {
+  padding: 28rpx 0;
+  border-bottom: 1rpx solid #f4f4f4;
+}
+
+.form-label {
+  font-size: 26rpx;
+  color: #888;
+  margin-bottom: 14rpx;
+  display: block;
+}
+
+.required {
+  color: #e74c3c;
+}
+
+.form-input {
+  width: 100%;
+  height: 72rpx;
+  background: #f8f9fb;
+  border-radius: 12rpx;
+  padding: 0 24rpx;
+  font-size: 30rpx;
+  color: #1a1a2e;
+  box-sizing: border-box;
+}
+
+.form-picker {
+  width: 100%;
+  height: 72rpx;
+  background: #f8f9fb;
+  border-radius: 12rpx;
+  padding: 0 24rpx;
+  font-size: 30rpx;
+  color: #1a1a2e;
+  line-height: 72rpx;
+  box-sizing: border-box;
+}
+
+.form-textarea {
+  width: 100%;
+  min-height: 120rpx;
+  background: #f8f9fb;
+  border-radius: 12rpx;
+  padding: 20rpx 24rpx;
+  font-size: 30rpx;
+  color: #1a1a2e;
+  box-sizing: border-box;
+}

+ 111 - 0
pages/test/test.js

@@ -0,0 +1,111 @@
+const app = getApp();
+
+Page({
+  data: {
+    connectionResult: '',
+    loginResult: '',
+    phone: '',
+    userInfo: null
+  },
+
+  onLoad: function () {
+    this.setData({
+      userInfo: app.globalData.userInfo
+    });
+  },
+
+  onPhoneInput: function (e) {
+    this.setData({
+      phone: e.detail.value
+    });
+  },
+
+  testConnection: function () {
+    this.setData({ connectionResult: '连接中...' });
+    
+    wx.request({
+      url: `${app.globalData.baseUrl}/api/wechat/login`,
+      method: 'POST',
+      data: {
+        code: 'test',
+        phoneCode: 'test'
+      },
+      timeout: 10000,
+      success: (res) => {
+        console.log('Connection test success:', res.data);
+        this.setData({ 
+          connectionResult: '✅ 连接成功 - ' + JSON.stringify(res.data) 
+        });
+      },
+      fail: (err) => {
+        console.error('Connection test failed:', err);
+        this.setData({ 
+          connectionResult: '❌ 连接失败 - ' + (err.errMsg || 'Unknown error') 
+        });
+      },
+      complete: () => {
+        console.log('Connection test completed');
+      }
+    });
+  },
+
+  testLogin: function () {
+    const phone = this.data.phone || '13800138000';
+    
+    this.setData({ loginResult: '登录中...' });
+    
+    wx.login({
+      success: (loginRes) => {
+        if (loginRes.code) {
+          console.log('wx.login success, code:', loginRes.code);
+          
+          wx.request({
+            url: `${app.globalData.baseUrl}/api/wechat/login`,
+            method: 'POST',
+            data: {
+              code: loginRes.code,
+              phoneCode: 'test_phone_code'
+            },
+            timeout: 10000,
+            success: (res) => {
+              console.log('Login request success:', res.data);
+              
+              if (res.data && res.data.success) {
+                app.globalData.token = res.data.token;
+                app.globalData.userInfo = res.data.user;
+                app.globalData.isLoggedIn = true;
+                
+                wx.setStorageSync('token', res.data.token);
+                wx.setStorageSync('userInfo', res.data.user);
+                
+                this.setData({ 
+                  loginResult: '✅ 登录成功',
+                  userInfo: res.data.user
+                });
+              } else {
+                this.setData({ 
+                  loginResult: '❌ 登录失败 - ' + (res.data?.message || 'Unknown error') 
+                });
+              }
+            },
+            fail: (err) => {
+              console.error('Login request failed:', err);
+              this.setData({ 
+                loginResult: '❌ 请求失败 - ' + (err.errMsg || 'Network error') 
+              });
+            },
+            complete: () => {
+              console.log('Login request completed');
+            }
+          });
+        } else {
+          this.setData({ loginResult: '❌ 获取code失败' });
+        }
+      },
+      fail: (err) => {
+        console.error('wx.login failed:', err);
+        this.setData({ loginResult: '❌ wx.login失败 - ' + err.errMsg });
+      }
+    });
+  }
+});

+ 3 - 0
pages/test/test.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 28 - 0
pages/test/test.wxml

@@ -0,0 +1,28 @@
+<view class="container">
+  <view class="title">测试登录</view>
+  
+  <view class="test-section">
+    <view class="section-title">测试后端连接</view>
+    <button class="test-btn" bindtap="testConnection">测试连接</button>
+    <view class="result-text">{{ connectionResult }}</view>
+  </view>
+  
+  <view class="test-section">
+    <view class="section-title">测试登录(模拟)</view>
+    <input class="input" placeholder="输入测试手机号" bindinput="onPhoneInput" />
+    <button class="test-btn" bindtap="testLogin">模拟登录</button>
+    <view class="result-text">{{ loginResult }}</view>
+  </view>
+  
+  <view class="test-section">
+    <view class="section-title">用户信息</view>
+    <view class="user-info" wx:if="{{ userInfo }}">
+      <view>ID: {{ userInfo.id }}</view>
+      <view>OpenID: {{ userInfo.openid }}</view>
+      <view>Phone: {{ userInfo.phone }}</view>
+    </view>
+    <view class="user-info" wx:else>
+      <view>未登录</view>
+    </view>
+  </view>
+</view>

+ 70 - 0
pages/test/test.wxss

@@ -0,0 +1,70 @@
+.container {
+  padding: 40rpx;
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 40rpx;
+  color: #333;
+}
+
+.test-section {
+  background: white;
+  border-radius: 16rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
+}
+
+.section-title {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 24rpx;
+}
+
+.test-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
+  border-radius: 44rpx;
+  color: white;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-bottom: 20rpx;
+}
+
+.test-btn:active {
+  opacity: 0.8;
+}
+
+.input {
+  width: 100%;
+  height: 80rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 12rpx;
+  padding: 0 24rpx;
+  margin-bottom: 20rpx;
+  font-size: 28rpx;
+}
+
+.result-text {
+  font-size: 26rpx;
+  color: #666;
+  word-break: break-all;
+  padding: 16rpx;
+  background: #f9f9f9;
+  border-radius: 8rpx;
+}
+
+.user-info {
+  font-size: 26rpx;
+  color: #333;
+  line-height: 2;
+  padding: 16rpx;
+  background: #f9f9f9;
+  border-radius: 8rpx;
+}

+ 24 - 0
project.config.json

@@ -0,0 +1,24 @@
+{
+  "setting": {
+    "es6": true,
+    "postcss": true,
+    "minified": true,
+    "uglifyFileName": false,
+    "enhance": true,
+    "packNpmRelationList": [],
+    "babelSetting": {
+      "ignore": [],
+      "disablePlugins": [],
+      "outputPath": ""
+    },
+    "useCompilerPlugins": false
+  },
+  "compileType": "miniprogram",
+  "simulatorPluginLibVersion": {},
+  "packOptions": {
+    "ignore": [],
+    "include": []
+  },
+  "appid": "wx98f5cf1c60f793b8",
+  "editorSetting": {}
+}

+ 14 - 0
project.private.config.json

@@ -0,0 +1,14 @@
+{
+  "libVersion": "3.8.12",
+  "projectname": "genealogy-min-program",
+  "setting": {
+    "urlCheck": false,
+    "coverView": true,
+    "lazyloadPlaceholderEnable": false,
+    "skylineRenderEnable": false,
+    "preloadBackgroundData": false,
+    "autoAudits": false,
+    "showShadowRootInWxmlPanel": true,
+    "compileHotReLoad": true
+  }
+}

+ 7 - 0
sitemap.json

@@ -0,0 +1,7 @@
+{
+  "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
+  "rules": [{
+    "action": "allow",
+    "page": "*"
+  }]
+}

+ 60 - 0
utils/helpers.wxs

@@ -0,0 +1,60 @@
+var relationMap = {
+  '1': '父',
+  '2': '母',
+  '3': '祖父',
+  '4': '祖母'
+};
+
+var maritalStatusMap = {
+  '0': '未知',
+  '1': '未婚',
+  '2': '已婚',
+  '3': '离异/丧偶'
+};
+
+var ancestorLabels = ['父/母', '祖父/祖母', '曾祖父', '高祖父', '天祖父', '烈祖父'];
+
+var childOrderNums = ['', '长', '次', '三', '四', '五', '六', '七', '八', '九', '十'];
+
+function getRelationText(relation) {
+  return relationMap['' + relation] || '';
+}
+
+function getSiblingType(relation) {
+  var r = '' + relation;
+  if (r === '11') return '兄';
+  if (r === '12') return '姐';
+  return '弟/妹';
+}
+
+function getMaritalStatusText(status) {
+  return maritalStatusMap['' + status] || '未知';
+}
+
+function getAncestorLabel(depth) {
+  var d = depth < 0 ? 0 : depth;
+  if (d < ancestorLabels.length) return ancestorLabels[d];
+  return '第' + (d + 1) + '代祖先';
+}
+
+function getChildOrderText(order) {
+  if (!order || order < 1) return '子';
+  if (order < childOrderNums.length) return childOrderNums[order] + '子';
+  return order + '子';
+}
+
+function shortName(name, simplified) {
+  if (simplified && simplified !== name) {
+    return name + '\n(' + simplified + ')';
+  }
+  return name;
+}
+
+module.exports = {
+  getRelationText: getRelationText,
+  getSiblingType: getSiblingType,
+  getMaritalStatusText: getMaritalStatusText,
+  getAncestorLabel: getAncestorLabel,
+  getChildOrderText: getChildOrderText,
+  shortName: shortName
+};