Forráskód Böngészése

feat(US-4.1/6.6/3.1): 年费入口+AI解读 — 前端页面对接

Wave 2+3 frontend: payment with dynamic pricing, login redirect_to, profile vip cards, index upgrade banner, chart subscription card + interpret panel, subscribe page, TriangleChart updates, user store with vipType/share/profile fields, API client for all new endpoints.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 3 hónapja
szülő
commit
26ccc04d8a

+ 326 - 227
client/components/TriangleChart.vue

@@ -1,12 +1,10 @@
 <template>
   <view class="container">
     <view class="chart-inner">
-
       <view class="triangle-glow"></view>
       <view class="triangle-bg"></view>
 
       <view class="inner-matrix">
-        <!-- Zone background overlays -->
         <view class="zone-left-bg"></view>
         <view class="zone-right-bg"></view>
 
@@ -32,10 +30,8 @@
           </view>
         </view>
 
-        <!-- 行间横向分割线(区分上三区与下两区) -->
         <view class="row-sep"></view>
 
-        <!-- 底部4数左→右:I(日) J(月) K(年前半) L(年后半) -->
         <view class="row row-bot4">
           <view class="cell-bot4" @click="showCellDetail('I', chartNum('innerBottom', 0))">
             <text :class="numColorCls(chartNum('innerBottom', 0))">
@@ -64,67 +60,62 @@
         </view>
       </view>
 
-      <!-- 顶部外侧:三角排列 X / V W (41-60岁) -->
+      <!-- 外三区 -->
       <view class="outer-tri top-tri">
-        <view class="tri-main" :class="numColorCls(matrix.outerTop.main)" @click="showCellDetail('X', chartNum('innerTop'))">
+        <view class="tri-main" :class="numColorCls(matrix.outerTop.main)" @click="showCellDetail('X', matrix.outerTop.main)">
           {{ matrix.outerTop.main }}
           <text v-if="isMasterNum(matrix.outerTop.main)" class="master-star">★</text>
         </view>
         <view class="tri-subs">
-          <text class="tri-sub" :class="numColorCls(matrix.outerTop.sub[0])" @click.stop="showCellDetail('V', chartNum('innerTop'))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerTop.sub[0])" @click.stop="showCellDetail('V', matrix.outerTop.sub[0])">
             {{ matrix.outerTop.sub[0] }}
             <text v-if="isMasterNum(matrix.outerTop.sub[0])" class="master-star-sm">★</text>
           </text>
-          <text class="tri-sub" :class="numColorCls(matrix.outerTop.sub[1])" @click.stop="showCellDetail('W', chartNum('innerTop'))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerTop.sub[1])" @click.stop="showCellDetail('W', matrix.outerTop.sub[1])">
             {{ matrix.outerTop.sub[1] }}
             <text v-if="isMasterNum(matrix.outerTop.sub[1])" class="master-star-sm">★</text>
           </text>
         </view>
       </view>
 
-      <!-- 左侧外侧:三角排列 R / P Q (21-40岁) -->
       <view class="outer-tri left-tri">
-        <view class="tri-main" :class="numColorCls(matrix.outerLeft.main)" @click="showCellDetail('R', chartNum('innerMid', 0))">
+        <view class="tri-main" :class="numColorCls(matrix.outerLeft.main)" @click="showCellDetail('R', matrix.outerLeft.main)">
           {{ matrix.outerLeft.main }}
           <text v-if="isMasterNum(matrix.outerLeft.main)" class="master-star-sm">★</text>
         </view>
         <view class="tri-subs">
-          <text class="tri-sub" :class="numColorCls(matrix.outerLeft.sub[0])" @click.stop="showCellDetail('P', chartNum('innerBottom', 0))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerLeft.sub[0])" @click.stop="showCellDetail('P', matrix.outerLeft.sub[0])">
             {{ matrix.outerLeft.sub[0] }}
             <text v-if="isMasterNum(matrix.outerLeft.sub[0])" class="master-star-sm">★</text>
           </text>
-          <text class="tri-sub" :class="numColorCls(matrix.outerLeft.sub[1])" @click.stop="showCellDetail('Q', chartNum('innerBottom', 1))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerLeft.sub[1])" @click.stop="showCellDetail('Q', matrix.outerLeft.sub[1])">
             {{ matrix.outerLeft.sub[1] }}
             <text v-if="isMasterNum(matrix.outerLeft.sub[1])" class="master-star-sm">★</text>
           </text>
         </view>
       </view>
 
-      <!-- 右侧外侧:三角排列 U / S T (61+岁) -->
       <view class="outer-tri right-tri">
-        <view class="tri-main" :class="numColorCls(matrix.outerRight.main)" @click="showCellDetail('U', chartNum('innerMid', 1))">
+        <view class="tri-main" :class="numColorCls(matrix.outerRight.main)" @click="showCellDetail('U', matrix.outerRight.main)">
           {{ matrix.outerRight.main }}
           <text v-if="isMasterNum(matrix.outerRight.main)" class="master-star-sm">★</text>
         </view>
         <view class="tri-subs">
-          <text class="tri-sub" :class="numColorCls(matrix.outerRight.sub[0])" @click.stop="showCellDetail('S', chartNum('innerBottom', 2))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerRight.sub[0])" @click.stop="showCellDetail('S', matrix.outerRight.sub[0])">
             {{ matrix.outerRight.sub[0] }}
             <text v-if="isMasterNum(matrix.outerRight.sub[0])" class="master-star-sm">★</text>
           </text>
-          <text class="tri-sub" :class="numColorCls(matrix.outerRight.sub[1])" @click.stop="showCellDetail('T', chartNum('innerBottom', 3))">
+          <text class="tri-sub" :class="numColorCls(matrix.outerRight.sub[1])" @click.stop="showCellDetail('T', matrix.outerRight.sub[1])">
             {{ matrix.outerRight.sub[1] }}
             <text v-if="isMasterNum(matrix.outerRight.sub[1])" class="master-star-sm">★</text>
           </text>
         </view>
       </view>
 
-      <!-- 外三区之间的 V 形分隔线 -->
       <view class="sep-v sep-v-left"></view>
       <view class="sep-v sep-v-right"></view>
-
     </view>
 
-    <!-- 底部 日(2) 月(2) 年(4) -->
     <view class="date-footer">
       <view class="date-group">
         <view class="input-boxes">
@@ -148,7 +139,7 @@
       </view>
     </view>
 
-    <!-- 数字点击详情弹窗 -->
+    <!-- Detail panel with annotation support -->
     <view v-if="selectedCell" class="detail-overlay" @click="hideDetail">
       <view class="detail-panel" @click.stop>
         <view class="detail-close" @click="hideDetail">✕</view>
@@ -158,30 +149,85 @@
         <view class="detail-trait">{{ selectedCell.trait }}</view>
         <view class="detail-desc">{{ selectedCell.desc }}</view>
         <view v-if="selectedCell.isMaster" class="detail-master">✦ 卓越数 — 能量倍增,非同凡响 ✦</view>
+
+        <!-- Annotation section (US-7.1) -->
+        <view v-if="recordId" class="annotation-section">
+          <view class="anno-divider"></view>
+          <text class="anno-title">📝 批注</text>
+
+          <!-- Existing annotations for this position -->
+          <view v-if="positionAnnotations.length > 0" class="anno-list">
+            <view v-for="(a, idx) in positionAnnotations" :key="idx" class="anno-item">
+              <text class="anno-text">{{ a.content }}</text>
+              <view class="anno-item-row">
+                <text class="anno-date">{{ formatTime(a.createdAt) }}</text>
+                <text class="anno-tag" v-if="a.tagName" :style="{ color: a.tagColor }">#{{ a.tagName }}</text>
+                <text class="anno-del" @click="deleteAnnotation(a.id)">删除</text>
+              </view>
+            </view>
+          </view>
+
+          <!-- Add annotation -->
+          <view class="anno-add">
+            <input class="anno-input" v-model="annoText" placeholder="添加批注..." maxlength="200" />
+            <view class="anno-actions">
+              <view class="tag-selector">
+                <text
+                  v-for="t in allTags"
+                  :key="t.id"
+                  class="tag-option"
+                  :class="{ active: selectedTagId === t.id }"
+                  :style="{ borderColor: t.color || '#666' }"
+                  @click="selectedTagId = selectedTagId === t.id ? null : t.id"
+                >#{{ t.name }}</text>
+                <text class="tag-option tag-add" @click="showCreateTag">+</text>
+              </view>
+              <button class="anno-save" @click="saveAnnotation" :disabled="!annoText.trim()">保存</button>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- Create tag modal -->
+    <view v-if="showTagModal" class="detail-overlay" @click="showTagModal = false">
+      <view class="tag-modal" @click.stop>
+        <text class="tag-modal-title">新建标签</text>
+        <input class="tag-modal-input" v-model="newTagName" placeholder="标签名称" maxlength="20" />
+        <view class="tag-color-picker">
+          <text
+            v-for="c in tagColors"
+            :key="c"
+            class="color-dot"
+            :class="{ active: newTagColor === c }"
+            :style="{ background: c }"
+            @click="newTagColor = c"
+          ></text>
+        </view>
+        <view class="tag-modal-actions">
+          <button class="tag-cancel" @click="showTagModal = false">取消</button>
+          <button class="tag-confirm" @click="createTag" :disabled="!newTagName.trim()">创建</button>
+        </view>
       </view>
     </view>
   </view>
 </template>
 
 <script>
-// style-guide §2.4: 命盘数字色
 const NUM_COLORS = {
   1: '#D94A4A', 2: '#4A90D9', 3: '#E8A238',
   4: '#2D8B67', 5: '#D4A017', 6: '#7B6FA0',
   7: '#4A6FA5', 8: '#C9A84C', 9: '#B85C3A'
 }
 
-// 30位完整命盘:US-1.3 位置命名对照表
 const POSITION_INFO = {
-  // 内部层(7位)
   O: { label: '主性格', zone: 'main',  trait: '核心人格特质', desc: '代表你的核心性格、人生方向和主要能量特征,是整个命盘的中心。' },
   M: { label: '青年综合数', zone: 'left',  trait: '青年期自身力量的呈现', desc: '由月能量与日能量结合而成,代表青年期的内在动力与外在表现。' },
   N: { label: '晚年综合数', zone: 'right', trait: '晚年期自身力量的呈现', desc: '由年前位与年后位结合而成,代表晚年的人生经验积累与智慧沉淀。' },
-  I: { label: '日能量', zone: 'left',  trait: '出生日能量', desc: '代表你出生当天的能量特质,影响你的日常行为和表达方式。(原位置 G → I)' },
-  J: { label: '月能量', zone: 'father', trait: '父亲遗传与先天能量', desc: '代表来自父亲/父系的先天遗传能量,影响你的内在根基。(原位置 F → J)' },
-  K: { label: '年前位', zone: 'right', trait: '前半生积累', desc: '代表早年环境与成长经历,影响人生前期的运势走向。(原位置 H)' },
-  L: { label: '年后位', zone: 'mother', trait: '母亲遗传与后天能量', desc: '代表来自母亲/母系的遗传能量以及后天环境的影响。(原位置 I)' },
-  // 外部层(9位)
+  I: { label: '日能量', zone: 'left',  trait: '出生日能量', desc: '代表你出生当天的能量特质,影响你的日常行为和表达方式。' },
+  J: { label: '月能量', zone: 'father', trait: '父亲遗传与先天能量', desc: '代表来自父亲/父系的先天遗传能量,影响你的内在根基。' },
+  K: { label: '年前位', zone: 'right', trait: '前半生积累', desc: '代表早年环境与成长经历,影响人生前期的运势走向。' },
+  L: { label: '年后位', zone: 'mother', trait: '母亲遗传与后天能量', desc: '代表来自母亲/母系的遗传能量以及后天环境的影响。' },
   P: { label: '左侧左子', zone: 'left', trait: '外部左翼基础', desc: '由日能量(I)与青年综合数(M)结合,代表左翼外部能量的基础层面。' },
   Q: { label: '左侧右子', zone: 'left', trait: '外部左翼辅助', desc: '由月能量(J)与青年综合数(M)结合,代表左翼外部能量的辅助层面。' },
   R: { label: '左侧主数', zone: 'left', trait: '左翼外部能量总和(21-40岁)', desc: '左侧子数之和,代表21-40岁年龄段的综合外部能量。' },
@@ -193,16 +239,28 @@ const POSITION_INFO = {
   X: { label: '顶部主数', zone: 'main',  trait: '顶部外部能量总和(41-60岁)', desc: '顶部子数之和,代表41-60岁年龄段的综合外部能量。' }
 }
 
+const TAG_COLORS = ['#D94A4A', '#4A90D9', '#E8A238', '#2D8B67', '#7B6FA0', '#C9A84C', '#D4A017', '#B85C3A']
+
 export default {
   props: {
     chartData: { type: Object, default: null },
     birthYear: { type: Number, default: 0 },
     birthMonth: { type: Number, default: 0 },
-    birthDay: { type: Number, default: 0 }
+    birthDay: { type: Number, default: 0 },
+    recordId: { type: Number, default: null },
+    annotations: { type: Array, default: () => [] },
+    tags: { type: Array, default: () => [] }
   },
+  emits: ['annotation-add', 'annotation-delete', 'tag-create'],
   data() {
     return {
-      selectedCell: null
+      selectedCell: null,
+      annoText: '',
+      selectedTagId: null,
+      showTagModal: false,
+      newTagName: '',
+      newTagColor: TAG_COLORS[1],
+      tagColors: TAG_COLORS
     }
   },
   computed: {
@@ -240,6 +298,13 @@ export default {
         day: ['0', '1'],
         year: ['1', '9', '9', '0']
       }
+    },
+    positionAnnotations() {
+      if (!this.selectedCell || !this.annotations || !this.recordId) return []
+      return this.annotations.filter(a => a.position === this.selectedCell.position)
+    },
+    allTags() {
+      return this.tags || []
     }
   },
   methods: {
@@ -270,16 +335,42 @@ export default {
     },
     hideDetail() {
       this.selectedCell = null
+      this.annoText = ''
+      this.selectedTagId = null
     },
     zoneName(zone) {
-      const names = {
-        main: '主性格区',
-        left: '左区(月日能量)',
-        right: '右区(年份能量)',
-        father: '父源区',
-        mother: '母源区'
-      }
-      return names[zone] || ''
+      return { main: '主性格区', left: '左区(月日能量)', right: '右区(年份能量)', father: '父源区', mother: '母源区' }[zone] || ''
+    },
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      const d = new Date(dateStr)
+      return `${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
+    },
+    saveAnnotation() {
+      if (!this.annoText.trim() || !this.recordId || !this.selectedCell) return
+      this.$emit('annotation-add', {
+        recordId: this.recordId,
+        position: this.selectedCell.position,
+        content: this.annoText.trim(),
+        tagId: this.selectedTagId
+      })
+      this.annoText = ''
+    },
+    deleteAnnotation(annoId) {
+      this.$emit('annotation-delete', annoId)
+    },
+    showCreateTag() {
+      this.showTagModal = true
+      this.newTagName = ''
+      this.newTagColor = TAG_COLORS[1]
+    },
+    createTag() {
+      if (!this.newTagName.trim()) return
+      this.$emit('tag-create', {
+        name: this.newTagName.trim(),
+        color: this.newTagColor
+      })
+      this.showTagModal = false
     }
   }
 }
@@ -302,7 +393,6 @@ export default {
   overflow: hidden;
 }
 
-/* 星空粒子 */
 .container::before {
   content: '';
   position: absolute;
@@ -327,8 +417,7 @@ export default {
   z-index: 1;
 }
 
-/* 三角形光晕 */
-/* ---- 五区色块叠加 ---- */
+/* Zone backgrounds, layout, cells, outer tris, etc. — exactly the same as before */
 .zone-left-bg,
 .zone-right-bg {
   position: absolute;
@@ -353,10 +442,7 @@ export default {
   background: linear-gradient(135deg, #4A90D9, #4A6FA5);
 }
 
-/* 父源/母源指示标记 */
-.zone-father {
-  position: relative;
-}
+.zone-father { position: relative; }
 .zone-father::after {
   content: '';
   position: absolute;
@@ -369,9 +455,7 @@ export default {
   background: #4A90D9;
   opacity: 0.5;
 }
-.zone-mother {
-  position: relative;
-}
+.zone-mother { position: relative; }
 .zone-mother::after {
   content: '';
   position: absolute;
@@ -385,7 +469,6 @@ export default {
   opacity: 0.5;
 }
 
-/* 垂直中分割线(从 O/M-N 交汇处向下贯穿至底部,分隔左右半区) */
 .inner-matrix::after {
   content: '';
   position: absolute;
@@ -404,7 +487,6 @@ export default {
   z-index: 1;
 }
 
-/* ---- 数字 1-9 独立配色 ---- */
 .num-color-1 { color: #D94A4A !important; }
 .num-color-2 { color: #4A90D9 !important; }
 .num-color-3 { color: #E8A238 !important; }
@@ -415,7 +497,6 @@ export default {
 .num-color-8 { color: #C9A84C !important; }
 .num-color-9 { color: #B85C3A !important; }
 
-/* 卓越数 ★ 标记 */
 .master-star {
   font-size: 28rpx;
   color: #fbbf24;
@@ -448,7 +529,6 @@ export default {
   filter: blur(20rpx);
 }
 
-/* 三角形主体 */
 .triangle-bg {
   position: absolute;
   top: 104rpx;
@@ -469,7 +549,6 @@ export default {
   clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
 }
 
-/* ---- 内部数字矩阵 ---- */
 .inner-matrix {
   position: absolute;
   bottom: 14rpx;
@@ -482,14 +561,12 @@ export default {
 }
 
 .row { display: flex; }
-
 .row-top {
   height: 36%;
   justify-content: center;
   align-items: flex-end;
   padding-bottom: 4rpx;
 }
-
 .row-mid {
   height: 28%;
   justify-content: center;
@@ -497,7 +574,6 @@ export default {
   gap: 56rpx;
   position: relative;
 }
-/* 顶部 O 区与中间 M/N 区之间的横向分割线 */
 .row-mid::before {
   content: '';
   position: absolute;
@@ -509,8 +585,6 @@ export default {
   background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08) 20%, rgba(255, 255, 255, 0.08) 80%, transparent);
   pointer-events: none;
 }
-
-/* 底部4数紧凑排列 */
 .row-bot4 {
   height: 36%;
   display: flex;
@@ -520,8 +594,6 @@ export default {
   padding: 0 20rpx;
   position: relative;
 }
-
-/* 行间横向分割线(位于 row-mid 与 row-bot4 之间,区分上三区与下两区) */
 .row-sep {
   height: 2rpx;
   width: 62%;
@@ -529,8 +601,6 @@ export default {
   background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.12) 20%, rgba(255, 255, 255, 0.12) 80%, transparent);
   flex-shrink: 0;
 }
-
-/* 底部四数 J 与 K 之间的垂直分割线(左右两区边界) */
 .row-bot4::before {
   content: '';
   position: absolute;
@@ -551,7 +621,6 @@ export default {
   text-shadow: 0 0 24rpx rgba(251, 191, 36, 0.5), 0 0 80rpx rgba(251, 191, 36, 0.2);
   letter-spacing: 2rpx;
 }
-
 .cell-mid {
   flex: none;
   width: auto;
@@ -561,7 +630,6 @@ export default {
   text-shadow: 0 0 16rpx rgba(255, 255, 255, 0.25);
   padding: 0 6rpx;
 }
-
 .cell-bot4 {
   width: 64rpx;
   text-align: center;
@@ -570,12 +638,8 @@ export default {
   color: #ffffff;
   text-shadow: 0 0 12rpx rgba(255, 255, 255, 0.2);
 }
+.border-right { border-right: 1rpx solid rgba(255, 255, 255, 0.1); }
 
-.border-right {
-  border-right: 1rpx solid rgba(255, 255, 255, 0.1);
-}
-
-/* ---- 外侧三角排列 ---- */
 .outer-tri {
   position: absolute;
   display: flex;
@@ -583,7 +647,6 @@ export default {
   align-items: center;
   z-index: 3;
 }
-
 .tri-main {
   font-size: 44rpx;
   font-weight: 700;
@@ -591,50 +654,21 @@ export default {
   text-shadow: 0 0 18rpx rgba(255, 255, 255, 0.2);
   line-height: 1.1;
 }
-
 .tri-subs {
   display: flex;
   gap: 20rpx;
   margin-top: 4rpx;
 }
-
 .tri-sub {
   font-size: 44rpx;
   font-weight: 700;
   color: rgba(255, 255, 255, 0.75);
 }
-
-/* 顶部 */
-.top-tri {
-  top: -58rpx;
-  left: 50%;
-  transform: translateX(-50%);
-  position: absolute;
-}
-.top-tri .tri-main {
-  font-size: 52rpx;
-  color: #fbbf24;
-  text-shadow: 0 0 28rpx rgba(251, 191, 36, 0.4);
-}
-
-/* 左侧 */
-.left-tri {
-  top: 28%;
-  left: 58rpx;
-  position: absolute;
-}
-
-/* 右侧 */
-.right-tri {
-  top: 28%;
-  right: 58rpx;
-  position: absolute;
-}
-
-/* 顶部的两外侧数字之间的分隔线 */
-.top-tri .tri-subs {
-  position: relative;
-}
+.top-tri { top: -58rpx; left: 50%; transform: translateX(-50%); position: absolute; }
+.top-tri .tri-main { font-size: 52rpx; color: #fbbf24; text-shadow: 0 0 28rpx rgba(251, 191, 36, 0.4); }
+.left-tri { top: 28%; left: 58rpx; position: absolute; }
+.right-tri { top: 28%; right: 58rpx; position: absolute; }
+.top-tri .tri-subs { position: relative; }
 .top-tri .tri-subs::before {
   content: '';
   position: absolute;
@@ -646,7 +680,6 @@ export default {
   background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15) 15%, rgba(255, 255, 255, 0.15) 85%, transparent);
 }
 
-/* ---- 外三区之间的 V 形分隔线 ---- */
 .sep-v {
   position: absolute;
   z-index: 2;
@@ -654,22 +687,9 @@ export default {
   background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.05) 100%);
   pointer-events: none;
 }
-.sep-v-left {
-  top: 20rpx;
-  left: 50%;
-  height: 70rpx;
-  transform-origin: top center;
-  transform: rotate(-36deg);
-}
-.sep-v-right {
-  top: 20rpx;
-  left: 50%;
-  height: 70rpx;
-  transform-origin: top center;
-  transform: rotate(36deg);
-}
+.sep-v-left { top: 20rpx; left: 50%; height: 70rpx; transform-origin: top center; transform: rotate(-36deg); }
+.sep-v-right { top: 20rpx; left: 50%; height: 70rpx; transform-origin: top center; transform: rotate(36deg); }
 
-/* ---- 底部日期 ---- */
 .date-footer {
   display: flex;
   align-items: center;
@@ -677,43 +697,20 @@ export default {
   padding: 0 40rpx;
   z-index: 1;
 }
-
-.date-group {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.date-divider {
-  width: 1rpx;
-  height: 40rpx;
-  background: rgba(255, 255, 255, 0.08);
-  margin: 0 24rpx;
-}
-
+.date-group { display: flex; flex-direction: column; align-items: center; }
+.date-divider { width: 1rpx; height: 40rpx; background: rgba(255, 255, 255, 0.08); margin: 0 24rpx; }
 .input-boxes { display: flex; }
-
 .box {
-  width: 34rpx;
-  height: 42rpx;
+  width: 34rpx; height: 42rpx;
   border: 1rpx solid rgba(255, 255, 255, 0.25);
-  border-radius: 6rpx;
-  margin: 0 3rpx;
-  text-align: center;
-  line-height: 42rpx;
-  font-size: 22rpx;
-  color: #ffffff;
+  border-radius: 6rpx; margin: 0 3rpx;
+  text-align: center; line-height: 42rpx;
+  font-size: 22rpx; color: #ffffff;
   background: rgba(255, 255, 255, 0.08);
 }
+.label { font-size: 18rpx; color: rgba(255, 255, 255, 0.45); margin-top: 8rpx; letter-spacing: 2rpx; }
 
-.label {
-  font-size: 18rpx;
-  color: rgba(255, 255, 255, 0.45);
-  margin-top: 8rpx;
-  letter-spacing: 2rpx;
-}
-
-/* ---- 数字点击详情弹窗 ---- */
+/* Detail overlay (same as before, extended for annotations) */
 .detail-overlay {
   position: fixed;
   inset: 0;
@@ -724,10 +721,7 @@ export default {
   justify-content: center;
   animation: fadeIn 0.2s ease-out;
 }
-@keyframes fadeIn {
-  from { opacity: 0; }
-  to { opacity: 1; }
-}
+@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
 .detail-panel {
   width: 100%;
   background: linear-gradient(180deg, #1a1040 0%, #0f0c2e 100%);
@@ -736,96 +730,201 @@ export default {
   position: relative;
   animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
   border-top: 1rpx solid rgba(251, 191, 36, 0.2);
-  max-height: 60vh;
+  max-height: 70vh;
   overflow-y: auto;
 }
-@keyframes slideUp {
-  from { transform: translateY(100%); }
-  to { transform: translateY(0); }
-}
+@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
 .detail-close {
-  position: absolute;
-  top: 20rpx;
-  right: 28rpx;
-  font-size: 32rpx;
-  color: rgba(255, 255, 255, 0.3);
-  width: 50rpx;
-  height: 50rpx;
+  position: absolute; top: 20rpx; right: 28rpx;
+  font-size: 32rpx; color: rgba(255, 255, 255, 0.3);
+  width: 50rpx; height: 50rpx;
+  display: flex; align-items: center; justify-content: center;
+}
+.detail-num { font-size: 80rpx; font-weight: 700; text-align: center; text-shadow: 0 0 30rpx currentColor; margin-bottom: 8rpx; }
+.detail-name { font-size: 30rpx; font-weight: 600; color: #ffffff; text-align: center; margin-bottom: 16rpx; }
+.detail-zone-tag { display: inline-block; font-size: 20rpx; padding: 4rpx 16rpx; border-radius: 20rpx; text-align: center; margin: 0 auto 20rpx; width: fit-content; }
+.tag-main { background: rgba(251, 191, 36, 0.15); color: #fbbf24; border: 1rpx solid rgba(251, 191, 36, 0.2); }
+.tag-left { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1rpx solid rgba(245, 158, 11, 0.2); }
+.tag-right { background: rgba(74, 144, 217, 0.15); color: #4A90D9; border: 1rpx solid rgba(74, 144, 217, 0.2); }
+.tag-father { background: rgba(74, 144, 217, 0.15); color: #4A90D9; border: 1rpx solid rgba(74, 144, 217, 0.2); }
+.tag-mother { background: rgba(123, 111, 160, 0.15); color: #7B6FA0; border: 1rpx solid rgba(123, 111, 160, 0.2); }
+.detail-trait { font-size: 26rpx; font-weight: 500; color: rgba(255, 255, 255, 0.8); text-align: center; margin-bottom: 16rpx; }
+.detail-desc { font-size: 24rpx; color: rgba(255, 255, 255, 0.5); line-height: 1.8; text-align: center; padding: 0 20rpx; }
+.detail-master { margin-top: 24rpx; text-align: center; font-size: 22rpx; color: #fbbf24; background: rgba(251, 191, 36, 0.1); border: 1rpx solid rgba(251, 191, 36, 0.15); border-radius: 12rpx; padding: 12rpx; letter-spacing: 2rpx; }
+
+/* ===== Annotation section ===== */
+.annotation-section {
+  margin-top: 16rpx;
+}
+.anno-divider {
+  height: 1rpx;
+  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
+  margin: 20rpx 0 16rpx;
+}
+.anno-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: rgba(255,255,255,0.7);
+  display: block;
+  margin-bottom: 12rpx;
+}
+.anno-list {
+  margin-bottom: 12rpx;
+}
+.anno-item {
+  background: rgba(255,255,255,0.04);
+  border: 1rpx solid rgba(255,255,255,0.06);
+  border-radius: 10rpx;
+  padding: 12rpx 14rpx;
+  margin-bottom: 8rpx;
+}
+.anno-text {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.85);
+  display: block;
+  line-height: 1.5;
+}
+.anno-item-row {
   display: flex;
   align-items: center;
-  justify-content: center;
+  gap: 12rpx;
+  margin-top: 6rpx;
 }
-.detail-num {
-  font-size: 80rpx;
-  font-weight: 700;
-  text-align: center;
-  text-shadow: 0 0 30rpx currentColor;
-  margin-bottom: 8rpx;
+.anno-date {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.3);
 }
-.detail-name {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #ffffff;
-  text-align: center;
-  margin-bottom: 16rpx;
+.anno-tag {
+  font-size: 20rpx;
+  font-weight: 500;
 }
-.detail-zone-tag {
-  display: inline-block;
+.anno-del {
   font-size: 20rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 20rpx;
-  text-align: center;
-  margin: 0 auto 20rpx;
-  width: fit-content;
+  color: #D94A4A;
+  margin-left: auto;
 }
-.tag-main {
-  background: rgba(251, 191, 36, 0.15);
-  color: #fbbf24;
-  border: 1rpx solid rgba(251, 191, 36, 0.2);
+.anno-add {
+  margin-top: 8rpx;
 }
-.tag-left {
-  background: rgba(245, 158, 11, 0.15);
-  color: #fbbf24;
-  border: 1rpx solid rgba(245, 158, 11, 0.2);
+.anno-input {
+  width: 100%;
+  height: 72rpx;
+  border: 1rpx solid rgba(255,255,255,0.1);
+  border-radius: 10rpx;
+  padding: 0 14rpx;
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.85);
+  background: rgba(255,255,255,0.04);
+  box-sizing: border-box;
 }
-.tag-right {
-  background: rgba(74, 144, 217, 0.15);
-  color: #4A90D9;
-  border: 1rpx solid rgba(74, 144, 217, 0.2);
+.anno-actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 10rpx;
 }
-.tag-father {
-  background: rgba(74, 144, 217, 0.15);
-  color: #4A90D9;
-  border: 1rpx solid rgba(74, 144, 217, 0.2);
+.tag-selector {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8rpx;
+  flex: 1;
 }
-.tag-mother {
-  background: rgba(123, 111, 160, 0.15);
-  color: #7B6FA0;
-  border: 1rpx solid rgba(123, 111, 160, 0.2);
+.tag-option {
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid rgba(255,255,255,0.15);
+  color: rgba(255,255,255,0.5);
 }
-.detail-trait {
-  font-size: 26rpx;
-  font-weight: 500;
-  color: rgba(255, 255, 255, 0.8);
-  text-align: center;
-  margin-bottom: 16rpx;
+.tag-option.active {
+  background: rgba(255,255,255,0.08);
+  color: rgba(255,255,255,0.85);
 }
-.detail-desc {
+.tag-add {
   font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.5);
-  line-height: 1.8;
-  text-align: center;
-  padding: 0 20rpx;
+  font-weight: 700;
+  color: rgba(255,255,255,0.4);
+  border-style: dashed;
+  padding: 4rpx 14rpx;
 }
-.detail-master {
-  margin-top: 24rpx;
-  text-align: center;
+.anno-save {
   font-size: 22rpx;
-  color: #fbbf24;
-  background: rgba(251, 191, 36, 0.1);
-  border: 1rpx solid rgba(251, 191, 36, 0.15);
-  border-radius: 12rpx;
-  padding: 12rpx;
-  letter-spacing: 2rpx;
+  padding: 6rpx 24rpx;
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+  color: #fff;
+  border: none;
+  border-radius: 8rpx;
+  flex-shrink: 0;
+  margin-left: 8rpx;
+}
+.anno-save[disabled] { opacity: 0.3; }
+
+/* Tag creation modal */
+.tag-modal {
+  width: 70%;
+  background: #1a1040;
+  border-radius: 20rpx;
+  padding: 36rpx;
+  border: 1rpx solid rgba(251,191,36,0.15);
+  margin-bottom: 30vh;
+}
+.tag-modal-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: rgba(255,255,255,0.9);
+  display: block;
+  text-align: center;
+  margin-bottom: 20rpx;
+}
+.tag-modal-input {
+  width: 100%;
+  height: 72rpx;
+  border: 1rpx solid rgba(255,255,255,0.15);
+  border-radius: 10rpx;
+  padding: 0 14rpx;
+  font-size: 26rpx;
+  color: rgba(255,255,255,0.85);
+  background: rgba(255,255,255,0.04);
+  box-sizing: border-box;
+  margin-bottom: 16rpx;
+}
+.tag-color-picker {
+  display: flex;
+  gap: 12rpx;
+  justify-content: center;
+  margin-bottom: 24rpx;
+}
+.color-dot {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  border: 2rpx solid transparent;
+}
+.color-dot.active {
+  border-color: #fff;
+  box-shadow: 0 0 12rpx rgba(255,255,255,0.3);
+}
+.tag-modal-actions {
+  display: flex;
+  gap: 16rpx;
+}
+.tag-cancel {
+  flex: 1;
+  padding: 12rpx 0;
+  background: rgba(255,255,255,0.06);
+  border: 1rpx solid rgba(255,255,255,0.1);
+  border-radius: 10rpx;
+  color: rgba(255,255,255,0.6);
+  font-size: 26rpx;
+}
+.tag-confirm {
+  flex: 1;
+  padding: 12rpx 0;
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+  border: none;
+  border-radius: 10rpx;
+  color: #fff;
+  font-size: 26rpx;
 }
+.tag-confirm[disabled] { opacity: 0.3; }
 </style>

+ 3 - 1
client/pages.json

@@ -6,7 +6,9 @@
     {"path": "pages/payment/index", "style": {"navigationBarTitleText": "VIP 开通"}},
     {"path": "pages/chart/index", "style": {"navigationBarTitleText": "命盘分析"}},
     {"path": "pages/login/index", "style": {"navigationBarTitleText": "微信登录"}},
-    {"path": "pages/subscribe/index", "style": {"navigationBarTitleText": "开通能量师"}}
+    {"path": "pages/subscribe/index", "style": {"navigationBarTitleText": "开通能量师"}},
+    {"path": "pages/commission/index", "style": {"navigationBarTitleText": "我的推广", "enablePullDownRefresh": true, "backgroundColor": "#0c0a1a"}},
+    {"path": "pages/withdraw/index", "style": {"navigationBarTitleText": "提现"}}
   ],
   "globalStyle": {
     "navigationBarTextStyle": "black",

+ 606 - 6
client/pages/chart/index.vue

@@ -7,9 +7,63 @@
         :birthYear="store.currentBirthYear"
         :birthMonth="store.currentBirthMonth"
         :birthDay="store.currentBirthDay"
+        :recordId="store.currentRecordId"
+        :annotations="annotations"
+        :tags="allTags"
+        @annotation-add="onAnnotationAdd"
+        @annotation-delete="onAnnotationDelete"
+        @tag-create="onTagCreate"
       />
     </view>
 
+    <!-- Menu button (regenerate + share info) -->
+    <view class="menu-bar">
+      <text class="menu-btn" @click="showMenu">···</text>
+    </view>
+
+    <!-- US-3.1: AI Interpretation section -->
+    <view class="interpret-bar">
+      <button class="interpret-btn" :disabled="interpreting" @click="fetchInterpret">
+        {{ interpreting ? '解读生成中...' : '🔮 查看AI解读' }}
+      </button>
+    </view>
+
+    <!-- US-3.1: Interpretation panel -->
+    <view v-if="interpretData" id="interpret-section" class="interpret-panel">
+      <view
+        v-for="(section, key) in interpretData"
+        :key="key"
+        class="interpret-card"
+        :class="{ expanded: interpretExpanded[key] }"
+        @click="toggleInterpretSection(key)"
+      >
+        <view class="interpret-card-header">
+          <text class="interpret-num-badge">{{ section.number }}</text>
+          <text class="interpret-card-title">{{ section.title }}</text>
+          <text class="interpret-toggle">{{ interpretExpanded[key] ? '▾' : '▸' }}</text>
+        </view>
+        <view v-show="interpretExpanded[key]" class="interpret-card-body">
+          <text class="interpret-content">{{ section.content }}</text>
+        </view>
+      </view>
+      <!-- Disclaimer -->
+      <view class="interpret-disclaimer">
+        <text class="disclaimer-text">本解读由AI生成,仅供参考</text>
+      </view>
+    </view>
+
+    <!-- Loading skeleton -->
+    <view v-if="interpreting" class="interpret-skeleton">
+      <view v-for="n in 4" :key="n" class="skeleton-card">
+        <view class="skeleton-header"></view>
+        <view v-if="n === 1" class="skeleton-body">
+          <view class="skeleton-line"></view>
+          <view class="skeleton-line short"></view>
+          <view class="skeleton-line"></view>
+        </view>
+      </view>
+    </view>
+
     <!-- AI Chat -->
     <view class="chat-section">
       <view class="section-title">
@@ -32,8 +86,16 @@
       <!-- Quota exceeded prompt for non-VIP -->
       <view v-if="quotaExceeded" class="upgrade-prompt">
         <text class="upgrade-text">今日 AI 解读次数已用完</text>
-        <text class="upgrade-desc">开通能量师会员享无限次解读</text>
-        <button class="upgrade-btn" @click="goSubscribe">立即开通 →</button>
+        <text class="upgrade-desc">选择适合的会员方案,解锁无限次解读</text>
+        <view class="upgrade-actions">
+          <button class="upgrade-btn secondary" @click="goSubscribeC">C端会员 ¥131/年</button>
+          <button class="upgrade-btn" @click="goSubscribe">能量师 ¥1,314/年</button>
+        </view>
+      </view>
+
+      <!-- Share hint (non-intrusive) -->
+      <view v-if="shareRemaining > 0 && !userStore.isVipActive" class="share-hint">
+        <text class="share-hint-text">分享好友可解锁更多次数 · 今日剩余 {{ shareRemaining }} 次</text>
       </view>
 
       <!-- Input area (disabled when quota exceeded) -->
@@ -51,16 +113,70 @@
           </button>
         </view>
       </template>
+
+      <!-- US-6.6: Subscription card for free users / 已开通 badge for paid -->
+      <view v-if="!quotaExceeded && userStore.isLoggedIn" class="subscribe-section">
+        <!-- Annual paid user -->
+        <view v-if="userStore.vipType === 'annual'" class="vip-badge-bar">
+          <text class="vip-badge-text">✅ 已开通AI解读</text>
+          <text class="vip-badge-expiry" v-if="userStore.vipEndTime">有效期至 {{ userStore.vipEndTime.slice(0, 10) }}</text>
+          <text class="vip-poster-link" @click="goPoster">📱 生成推广海报</text>
+        </view>
+        <!-- Practitioner paid user -->
+        <view v-else-if="userStore.vipType === 'practitioner'" class="vip-badge-bar pro">
+          <text class="vip-badge-text">✅ 已开通能量师</text>
+          <text class="vip-badge-expiry" v-if="userStore.vipEndTime">有效期至 {{ userStore.vipEndTime.slice(0, 10) }}</text>
+        </view>
+        <!-- Free user -->
+        <view v-else class="subscribe-card">
+          <text class="subscribe-title">🔮 AI解读会员</text>
+          <text class="subscribe-desc">¥131/年 · 无限AI解读 · 个性化报告 · 推广赚佣金</text>
+          <view class="subscribe-actions">
+            <button class="subscribe-btn" @click="goSubscribeC">立即开通 ¥131</button>
+            <text class="subscribe-link" @click="goSubscribe">了解能量师专业版</text>
+          </view>
+        </view>
+      </view>
     </view>
   </view>
 </template>
 
+<script>
+// Page lifecycle hooks (needed for WeChat share)
+export default {
+  onShareAppMessage() {
+    const pages = getCurrentPages()
+    const page = pages[pages.length - 1]
+    const recordId = page?.options?.recordId || ''
+    const store = this.$scope?.store
+    const title = store?.currentChart?.O
+      ? `我的命盘主性格数字是${store.currentChart.O},快来测测你的!`
+      : '数字能量命盘分析'
+    return {
+      title,
+      path: `/pages/chart/index?recordId=${recordId}`,
+      imageUrl: '/static/share-card.png'
+    }
+  },
+  onShareTimeline() {
+    const pages = getCurrentPages()
+    const page = pages[pages.length - 1]
+    const recordId = page?.options?.recordId || ''
+    return {
+      title: '数字能量命盘分析',
+      query: `recordId=${recordId}`,
+      imageUrl: '/static/share-card.png'
+    }
+  }
+}
+</script>
+
 <script setup>
 import { ref, computed, onMounted } from 'vue'
 import { useChartStore } from '@/stores/chart'
 import { useUserStore } from '@/stores/user'
 import TriangleChart from '@/components/TriangleChart.vue'
-import { chatApi } from '@/utils/api'
+import { chartApi, chatApi, consultationApi, profileApi, annotationApi, exportApi } from '@/utils/api'
 
 const store = useChartStore()
 const userStore = useUserStore()
@@ -70,12 +186,29 @@ const messages = ref([])
 const loading = ref(false)
 const usedChats = ref(0)
 const maxChats = ref(0)
+const shareCount = ref(0)
+const shareLimit = ref(0)
+
+// Annotation / Tag state (US-7.1 / US-7.2)
+const annotations = ref([])
+const allTags = ref([])
+
+// US-3.1: AI interpretation state
+const interpretData = ref(null)
+const interpreting = ref(false)
+const interpretExpanded = ref({})
 
 const quotaExceeded = computed(() => {
   if (userStore.isVipActive) return false
   return maxChats.value > 0 && usedChats.value >= maxChats.value
 })
 
+const shareRemaining = computed(() => {
+  if (userStore.isVipActive) return -1
+  if (shareLimit.value <= 0) return 0
+  return Math.max(0, shareLimit.value - shareCount.value)
+})
+
 onMounted(async () => {
   try {
     await userStore.fetchQuota()
@@ -96,6 +229,13 @@ onMounted(async () => {
   if (store.currentChart) {
     maxChats.value = userStore.maxChats || 3
     usedChats.value = userStore.usedChats || 0
+    shareCount.value = userStore.shareCount || 0
+    shareLimit.value = userStore.shareLimit || 1
+
+    // Load annotations and tags for this record (US-7.1/7.2)
+    if (store.currentRecordId) {
+      loadAnnotations()
+    }
 
     // Use consultationMessages from store (set during startConsultation) if available
     if (store.consultationMessages && store.consultationMessages.length > 0) {
@@ -132,6 +272,45 @@ onMounted(async () => {
   }
 })
 
+// US-3.1: Fetch AI structured interpretation
+async function fetchInterpret() {
+  if (!store.currentRecordId) {
+    uni.showToast({ title: '暂无命盘数据', icon: 'none' })
+    return
+  }
+  // If already loaded, just scroll to it
+  if (interpretData.value) {
+    scrollToInterpret()
+    return
+  }
+  interpreting.value = true
+  try {
+    const data = await chartApi.interpret(store.currentRecordId)
+    interpretData.value = data
+    // Auto-expand the main character section
+    interpretExpanded.value = { mainCharacter: true }
+    uni.showToast({ title: '解读已生成', icon: 'success' })
+    scrollToInterpret()
+  } catch (e) {
+    uni.showToast({ title: e.message || '解读生成失败', icon: 'none' })
+  } finally {
+    interpreting.value = false
+  }
+}
+
+function scrollToInterpret() {
+  // Scroll to the interpret section after rendering
+  uni.createSelectorQuery().select('#interpret-section').boundingClientRect(res => {
+    if (res) {
+      uni.pageScrollTo({ scrollTop: res.top - 20, duration: 300 })
+    }
+  }).exec()
+}
+
+function toggleInterpretSection(key) {
+  interpretExpanded.value[key] = !interpretExpanded.value[key]
+}
+
 async function sendQuestion(q) {
   if (!q.trim() || loading.value) return
   if (quotaExceeded.value) return
@@ -170,9 +349,181 @@ async function sendQuestion(q) {
   scrollTop.value = 99999
 }
 
+// US-3.3: Regenerate interpretation via action sheet menu
+function showMenu() {
+  const items = ['🔄 重新生成解读']
+  // US-2.2: PDF export for paid users only
+  if (userStore.isVipActive) {
+    items.push('📄 导出 PDF')
+  }
+  if (!userStore.isVipActive && shareRemaining.value > 0) {
+    items.push('📤 分享给好友')
+  }
+  items.push('❌ 取消')
+  uni.showActionSheet({
+    itemList: items,
+    success: async (res) => {
+      if (res.tapIndex === 0) {
+        await confirmRegenerate()
+      } else if (res.tapIndex === 1) {
+        if (items[1] === '📄 导出 PDF') {
+          await exportPdf()
+        } else if (items[1] === '📤 分享给好友') {
+          await triggerShare()
+        }
+      } else if (res.tapIndex === 2 && items[2] === '📤 分享给好友') {
+        await triggerShare()
+      }
+    }
+  })
+}
+
+async function confirmRegenerate() {
+  uni.showModal({
+    title: '重新生成解读',
+    content: '将清除当前对话记录并生成全新的AI解读,确定吗?',
+    success: async (confirm) => {
+      if (!confirm.confirm) return
+      if (!store.currentRecordId) {
+        uni.showToast({ title: '暂无命盘数据', icon: 'none' })
+        return
+      }
+      try {
+        uni.showLoading({ title: '重新生成中...' })
+        const newMessages = await consultationApi.regenerate(store.currentRecordId)
+        messages.value = (newMessages || []).map(m => ({
+          role: m.role === 'ai' ? 'ai' : 'user',
+          content: m.content
+        }))
+        // Reset chat counters
+        usedChats.value = 0
+        uni.hideLoading()
+        uni.showToast({ title: '解读已重新生成', icon: 'success' })
+        scrollTop.value = 99999
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: '重新生成失败,请重试', icon: 'none' })
+      }
+    }
+  })
+}
+
+// US-2.1: Track share and update quota
+async function triggerShare() {
+  uni.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline'],
+    success: async () => {
+      // Call backend to track share
+      try {
+        const result = await profileApi.trackShare()
+        shareCount.value = result.dailyShareCount || shareCount.value + 1
+      } catch (e) { /* best-effort */ }
+    }
+  })
+}
+
+// US-2.2: Export PDF report
+async function exportPdf() {
+  if (!store.currentRecordId) {
+    uni.showToast({ title: '暂无命盘数据', icon: 'none' })
+    return
+  }
+  uni.showLoading({ title: '正在生成 PDF...' })
+  try {
+    // The API returns a PDF binary. We use uni.downloadFile to get it as a temp file.
+    const token = uni.getStorageSync('token')
+    const url = 'http://127.0.0.1:8186/api/export/pdf'
+    const downloadRes = await new Promise((resolve, reject) => {
+      uni.downloadFile({
+        url,
+        header: { Authorization: `Bearer ${token}` },
+        method: 'POST',
+        data: { recordId: store.currentRecordId },
+        success: resolve,
+        fail: reject
+      })
+    })
+    if (downloadRes.statusCode !== 200) {
+      throw new Error('下载失败')
+    }
+    uni.hideLoading()
+    // Open the PDF file
+    uni.openDocument({
+      filePath: downloadRes.tempFilePath,
+      success: () => {
+        uni.showToast({ title: 'PDF 已生成', icon: 'success' })
+      },
+      fail: () => {
+        uni.showToast({ title: '打开文件失败', icon: 'none' })
+      }
+    })
+  } catch (e) {
+    uni.hideLoading()
+    uni.showToast({ title: e.message || '导出失败', icon: 'none' })
+  }
+}
+
 function goSubscribe() {
   uni.navigateTo({ url: '/pages/subscribe/index' })
 }
+function goSubscribeC() {
+  uni.navigateTo({ url: '/pages/payment/index?product=annual' })
+}
+function goPoster() {
+  uni.navigateTo({ url: '/pages/subscribe/index' })
+}
+
+// === Annotation / Tag handlers (US-7.1 / US-7.2) ===
+
+async function loadAnnotations() {
+  if (!store.currentRecordId) return
+  try {
+    const [annoList, tagList] = await Promise.all([
+      annotationApi.list(store.currentRecordId),
+      annotationApi.listTags(store.currentRecordId)
+    ])
+    annotations.value = annoList || []
+    allTags.value = tagList || []
+  } catch (e) { /* best-effort */ }
+}
+
+async function onAnnotationAdd(data) {
+  try {
+    const saved = await annotationApi.add(data)
+    // Reload annotations to include the new one with server-generated ID
+    if (store.currentRecordId) {
+      const annoList = await annotationApi.list(store.currentRecordId)
+      annotations.value = annoList || []
+    }
+    uni.showToast({ title: '批注已保存', icon: 'success' })
+  } catch (e) {
+    uni.showToast({ title: '保存失败', icon: 'none' })
+  }
+}
+
+async function onAnnotationDelete(annoId) {
+  try {
+    await annotationApi.delete(annoId)
+    annotations.value = annotations.value.filter(a => a.id !== annoId)
+    uni.showToast({ title: '批注已删除', icon: 'success' })
+  } catch (e) {
+    uni.showToast({ title: '删除失败', icon: 'none' })
+  }
+}
+
+async function onTagCreate(tagData) {
+  try {
+    const saved = await annotationApi.addTag({ ...tagData, recordId: store.currentRecordId })
+    if (store.currentRecordId) {
+      const tagList = await annotationApi.listTags(store.currentRecordId)
+      allTags.value = tagList || []
+    }
+    uni.showToast({ title: '标签已创建', icon: 'success' })
+  } catch (e) {
+    uni.showToast({ title: '创建失败', icon: 'none' })
+  }
+}
 </script>
 
 <style scoped lang="scss">
@@ -189,6 +540,149 @@ function goSubscribe() {
   margin: 0 14px;
 }
 
+/* Menu bar — three-dot button */
+.menu-bar {
+  display: flex;
+  justify-content: flex-end;
+  padding: 0 14px;
+  margin: -8px 0 0;
+}
+.menu-btn {
+  width: 32px;
+  height: 28px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  font-weight: 700;
+  color: rgba(255, 255, 255, 0.4);
+  letter-spacing: 2px;
+  border-radius: 6px;
+  background: rgba(255, 255, 255, 0.04);
+}
+.menu-btn:active {
+  background: rgba(255, 255, 255, 0.08);
+}
+
+/* US-3.1: Interpret button bar */
+.interpret-bar {
+  flex-shrink: 0;
+  margin: 0 14px 10px;
+}
+.interpret-btn {
+  width: 100%;
+  height: 44px;
+  background: linear-gradient(135deg, #7c3aed, #6366f1);
+  color: #fff;
+  border: none;
+  border-radius: 12px;
+  line-height: 44px;
+  font-size: 15px;
+  font-weight: 600;
+  letter-spacing: 1px;
+}
+.interpret-btn[disabled] {
+  opacity: 0.5;
+}
+
+/* Interpretation panel */
+.interpret-panel {
+  flex-shrink: 0;
+  margin: 0 14px 16px;
+}
+.interpret-card {
+  background: rgba(124, 58, 237, 0.06);
+  border: 1px solid rgba(124, 58, 237, 0.12);
+  border-radius: 12px;
+  margin-bottom: 10px;
+  overflow: hidden;
+  transition: border-color 0.2s;
+}
+.interpret-card.expanded {
+  border-color: rgba(124, 58, 237, 0.25);
+  background: rgba(124, 58, 237, 0.08);
+}
+.interpret-card-header {
+  display: flex;
+  align-items: center;
+  padding: 12px 14px;
+  gap: 10px;
+}
+.interpret-num-badge {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #7c3aed, #6366f1);
+  color: #fff;
+  font-size: 13px;
+  font-weight: 700;
+  line-height: 28px;
+  text-align: center;
+  flex-shrink: 0;
+}
+.interpret-card-title {
+  flex: 1;
+  font-size: 14px;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.85);
+}
+.interpret-toggle {
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.35);
+}
+.interpret-card-body {
+  padding: 0 14px 14px 52px;
+}
+.interpret-content {
+  display: block;
+  font-size: 13px;
+  line-height: 1.7;
+  color: rgba(255, 255, 255, 0.65);
+  white-space: pre-wrap;
+}
+
+/* Disclaimer */
+.interpret-disclaimer {
+  text-align: center;
+  padding: 6px 0 2px;
+}
+.disclaimer-text {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.2);
+}
+
+/* Loading skeleton */
+.interpret-skeleton {
+  flex-shrink: 0;
+  margin: 0 14px 16px;
+}
+.skeleton-card {
+  background: rgba(255, 255, 255, 0.03);
+  border: 1px solid rgba(255, 255, 255, 0.05);
+  border-radius: 12px;
+  margin-bottom: 10px;
+  overflow: hidden;
+}
+.skeleton-header {
+  height: 24px;
+  margin: 12px 14px;
+  background: rgba(255, 255, 255, 0.06);
+  border-radius: 6px;
+  width: 60%;
+}
+.skeleton-body {
+  padding: 0 14px 14px;
+}
+.skeleton-line {
+  height: 12px;
+  background: rgba(255, 255, 255, 0.04);
+  border-radius: 4px;
+  margin-bottom: 8px;
+}
+.skeleton-line.short {
+  width: 70%;
+}
+
 .chat-section {
   flex: 1;
   display: flex;
@@ -202,7 +696,7 @@ function goSubscribe() {
   font-size: 17px;
   font-weight: 600;
   color: rgba(255, 255, 255, 0.9);
-  margin: 20px 0 14px;
+  margin: 12px 0 14px;
   display: flex;
   align-items: center;
   gap: 8px;
@@ -299,6 +793,16 @@ function goSubscribe() {
   opacity: 0.4;
 }
 
+/* Share hint */
+.share-hint {
+  padding: 8px 0;
+  text-align: center;
+}
+.share-hint-text {
+  font-size: 11px;
+  color: rgba(255, 255, 255, 0.3);
+}
+
 /* Upgrade prompt */
 .upgrade-prompt {
   background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(217, 119, 6, 0.06));
@@ -323,13 +827,109 @@ function goSubscribe() {
   margin: 8px 0 16px;
 }
 
+.upgrade-actions {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+}
+
 .upgrade-btn {
+  flex: 1;
   background: linear-gradient(135deg, #f59e0b, #d97706);
   color: #fff;
   border: none;
   border-radius: 10px;
-  padding: 10px 36px;
-  font-size: 15px;
+  padding: 10px 0;
+  font-size: 14px;
   font-weight: 500;
+  line-height: 1.4;
+}
+
+.upgrade-btn.secondary {
+  background: rgba(255, 255, 255, 0.06);
+  border: 1px solid rgba(251, 191, 36, 0.2);
+  color: #fbbf24;
+}
+
+/* US-6.6: Subscribe section */
+.subscribe-section {
+  margin-top: 10px;
+}
+
+.vip-badge-bar {
+  background: rgba(52, 211, 153, 0.06);
+  border: 1px solid rgba(52, 211, 153, 0.15);
+  border-radius: 10px;
+  padding: 12px 14px;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 6px;
+}
+.vip-badge-bar.pro {
+  background: rgba(167, 139, 250, 0.06);
+  border-color: rgba(167, 139, 250, 0.15);
+}
+.vip-badge-text {
+  font-size: 13px;
+  font-weight: 600;
+  color: #34d399;
+}
+.vip-badge-bar.pro .vip-badge-text {
+  color: #a78bfa;
+}
+.vip-badge-expiry {
+  font-size: 11px;
+  color: rgba(255,255,255,0.4);
+}
+.vip-poster-link {
+  font-size: 12px;
+  color: #fbbf24;
+  margin-left: auto;
+}
+
+.subscribe-card {
+  background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(217, 119, 6, 0.05));
+  border: 1px solid rgba(251, 191, 36, 0.15);
+  border-radius: 12px;
+  padding: 16px;
+  text-align: center;
+}
+.subscribe-title {
+  display: block;
+  font-size: 15px;
+  font-weight: 600;
+  color: #fbbf24;
+  margin-bottom: 6px;
+}
+.subscribe-desc {
+  display: block;
+  font-size: 12px;
+  color: rgba(255,255,255,0.45);
+  margin-bottom: 14px;
+  line-height: 1.5;
+}
+.subscribe-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  align-items: center;
+}
+.subscribe-btn {
+  width: 100%;
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+  color: #fff;
+  border: none;
+  border-radius: 10px;
+  padding: 12px 0;
+  font-size: 15px;
+  font-weight: 600;
+}
+.subscribe-btn:active {
+  opacity: 0.85;
+}
+.subscribe-link {
+  font-size: 12px;
+  color: rgba(255,255,255,0.35);
 }
 </style>

+ 65 - 70
client/pages/index/index.vue

@@ -15,36 +15,13 @@
           v-model="name"
           placeholder="请输入姓名"
         />
-        <view class="row-inputs">
-          <view class="third-input">
-            <text class="label">年份</text>
-            <input
-              class="input-field"
-              type="digit"
-              maxlength="4"
-              v-model="birthYear"
-              placeholder="1990"
-            />
+        <text class="label mt-16">出生日期</text>
+        <picker class="date-picker" mode="date" :value="birthday" :start="minDate" :end="maxDate" @change="onBirthdayChange">
+          <view class="input-field date-field">
+            <text class="date-val">{{ birthday }}</text>
+            <text class="picker-arrow">▼</text>
           </view>
-          <view class="third-input">
-            <text class="label">月份</text>
-            <picker class="picker-wrap" mode="selector" :range="monthRange" @change="onMonthChange">
-              <view class="input-field picker-field">
-                <text :class="birthMonth ? 'picker-val' : 'picker-ph'">{{ birthMonth || '选择' }}</text>
-                <text class="picker-arrow">▼</text>
-              </view>
-            </picker>
-          </view>
-          <view class="third-input">
-            <text class="label">日期</text>
-            <picker class="picker-wrap" mode="selector" :range="dayRange" @change="onDayChange">
-              <view class="input-field picker-field">
-                <text :class="birthDay ? 'picker-val' : 'picker-ph'">{{ birthDay || '选择' }}</text>
-                <text class="picker-arrow">▼</text>
-              </view>
-            </picker>
-          </view>
-        </view>
+        </picker>
         <text class="label mt-16">想了解的问题(选填)</text>
         <view class="question-wrap">
           <textarea
@@ -67,6 +44,11 @@
           </view>
         </view>
         <button class="analyze-btn" @click="onAnalyze">开始咨询</button>
+
+        <!-- US-4.1: Upgrade guide for free users -->
+        <view v-if="!userStore.isVipActive" class="upgrade-banner" @click="goSubscribe">
+          <text class="upgrade-banner-text">解锁无限解读 · 开通会员仅 ¥131/年 ›</text>
+        </view>
       </view>
     </view>
   </view>
@@ -75,43 +57,50 @@
 <script setup>
 import { ref, computed } from 'vue'
 import { useChartStore } from '@/stores/chart'
+import { useUserStore } from '@/stores/user'
 import { consultationApi } from '@/utils/api'
 
 const store = useChartStore()
+const userStore = useUserStore()
 const name = ref('')
-const birthYear = ref('')
-const birthMonth = ref('')
-const birthDay = ref('')
+const birthday = ref('1990-01-01')
 const questions = ref('')
 const confirming = ref(false) // prevent double-click
 const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
 
+// Date picker range: 1900-01-01 ~ today (US-1.2 §6)
+const minDate = '1900-01-01'
+const maxDate = computed(() => {
+  const d = new Date()
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  return `${y}-${m}-${day}`
+})
+
 function selectQuick(q) {
   questions.value = questions.value === q ? '' : q
 }
 
-const monthRange = Array.from({ length: 12 }, (_, i) => String(i + 1))
-const dayRange = Array.from({ length: 31 }, (_, i) => String(i + 1))
-
-function onMonthChange(e) {
-  birthMonth.value = monthRange[e.detail.value]
-}
-function onDayChange(e) {
-  birthDay.value = dayRange[e.detail.value]
+function onBirthdayChange(e) {
+  birthday.value = e.detail.value
 }
 
-function formatBirthday(year, month, day) {
-  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
+function parseBirthday(str) {
+  const parts = str.split('-')
+  return {
+    year: parseInt(parts[0], 10),
+    month: parseInt(parts[1], 10),
+    day: parseInt(parts[2], 10)
+  }
 }
 
 async function onAnalyze() {
   if (confirming.value) return
-  const year = parseInt(birthYear.value, 10)
-  const month = parseInt(birthMonth.value, 10)
-  const day = parseInt(birthDay.value, 10)
+  const { year, month, day } = parseBirthday(birthday.value)
 
   if (!year || !month || !day) {
-    uni.showToast({ title: '请输入完整的出生年月日', icon: 'none' })
+    uni.showToast({ title: '请选择出生日期', icon: 'none' })
     return
   }
   if (year < 1900 || year > new Date().getFullYear()) {
@@ -127,16 +116,16 @@ async function onAnalyze() {
     return
   }
 
-  const birthday = formatBirthday(year, month, day)
+  const birthdayStr = birthday.value
   confirming.value = true
 
   try {
     // Check if this birthday has existing records — US-1.1 §7-9
-    const isNew = await checkNewBirthday(birthday)
+    const isNew = await checkNewBirthday(birthdayStr)
     if (!isNew) {
       // Existing birthday — proceed directly (US-1.1 §7-8: returns existing record)
       confirming.value = false
-      await proceedConsultation(year, month, day, birthday)
+      await proceedConsultation(year, month, day, birthdayStr)
       return
     }
   } catch (_e) {
@@ -152,7 +141,7 @@ async function onAnalyze() {
     success: async (res) => {
       confirming.value = false
       if (res.confirm) {
-        await proceedConsultation(year, month, day, birthday)
+        await proceedConsultation(year, month, day, birthdayStr)
       }
       // res.cancel → stay on page, do nothing (US-1.1 §11)
     },
@@ -186,6 +175,10 @@ async function proceedConsultation(year, month, day, birthday) {
     : '/pages/chart/index'
   uni.navigateTo({ url })
 }
+
+function goSubscribe() {
+  uni.navigateTo({ url: '/pages/subscribe/index' })
+}
 </script>
 
 <style scoped lang="scss">
@@ -255,37 +248,24 @@ async function proceedConsultation(year, month, day, birthday) {
   display: block;
 }
 
-.row-inputs {
-  display: flex;
-  gap: 10rpx;
-}
-
-.third-input {
-  flex: 1;
-  min-width: 0;
-}
-
-/* Picker */
-.picker-wrap {
+/* Date picker */
+.date-picker {
   width: 100%;
+  display: block;
 }
-.picker-field {
+.date-field {
   display: flex;
   align-items: center;
   justify-content: space-between;
 }
-.picker-val {
+.date-val {
+  font-size: 18px;
   color: rgba(255, 255, 255, 0.85);
-  font-size: 16px;
-}
-.picker-ph {
-  color: rgba(255, 255, 255, 0.25);
-  font-size: 16px;
+  letter-spacing: 2px;
 }
 .picker-arrow {
   font-size: 10px;
   color: rgba(255, 255, 255, 0.3);
-  margin-left: 4px;
 }
 
 .question-wrap {
@@ -346,4 +326,19 @@ async function proceedConsultation(year, month, day, birthday) {
   letter-spacing: 2px;
   margin-top: 4px;
 }
+
+/* US-4.1: Upgrade guide banner */
+.upgrade-banner {
+  margin-top: 16px;
+  text-align: center;
+  padding: 12px;
+  background: rgba(251, 191, 36, 0.06);
+  border: 1px solid rgba(251, 191, 36, 0.1);
+  border-radius: 10px;
+}
+.upgrade-banner-text {
+  font-size: 13px;
+  color: #fbbf24;
+  font-weight: 500;
+}
 </style>

+ 21 - 4
client/pages/login/index.vue

@@ -74,10 +74,27 @@ async function onWechatLogin() {
 
     uni.hideLoading()
     uni.showToast({ title: '登录成功', icon: 'success' })
-    setTimeout(() => {
-      // 使用 switchTab 跳转到首页(reLaunch 清空了页面栈)
-      uni.switchTab({ url: '/pages/index/index' })
-    }, 500)
+
+    // Check redirect_to param (US-6.6: "登录后开通" flow)
+    const pages = getCurrentPages()
+    const page = pages[pages.length - 1]
+    const params = page?.options || {}
+    const redirectTo = params.redirect_to || ''
+
+    // US-5.1 §8: Redirect to registration completion if profile incomplete
+    if (userStore.profileIncomplete) {
+      setTimeout(() => {
+        uni.redirectTo({ url: '/pages/register/index' })
+      }, 500)
+    } else if (redirectTo) {
+      setTimeout(() => {
+        uni.redirectTo({ url: redirectTo })
+      }, 500)
+    } else {
+      setTimeout(() => {
+        uni.switchTab({ url: '/pages/index/index' })
+      }, 500)
+    }
   } catch (e) {
     uni.hideLoading()
     uni.showToast({ title: e.message || '登录失败', icon: 'none' })

+ 272 - 73
client/pages/payment/index.vue

@@ -1,104 +1,303 @@
 <template>
   <view class="page-payment">
-    <view class="plan-card">
-      <view class="plan-header">
-        <text class="plan-name">VIP 会员</text>
-        <text class="plan-price">¥398</text>
-        <text class="plan-period">/ 年</text>
-      </view>
-      <view class="plan-features">
-        <text class="feature">✓ 无限次数字能量分析</text>
-        <text class="feature">✓ AI 深度解读报告</text>
-        <text class="feature">✓ 号码标注与标签管理</text>
-        <text class="feature">✓ 历史记录永久保存</text>
-        <text class="feature">✓ 推广赚取佣金</text>
-      </view>
+    <view class="header">
+      <text class="header-title">{{ pageTitle }}</text>
+      <text class="header-desc">{{ pageDesc }}</text>
     </view>
 
-    <view class="order-summary">
-      <text class="summary-title">订单摘要</text>
-      <view class="summary-row">
-        <text>VIP 会员年费</text>
-        <text>¥398</text>
+    <template v-if="loading">
+      <view class="loading-state"><text class="loading-text">加载中...</text></view>
+    </template>
+
+    <template v-else-if="product === 'annual'">
+      <!-- C端年费 ¥131 -->
+      <view class="plan-card selected">
+        <view class="plan-badge popular">推荐</view>
+        <view class="plan-top">
+          <text class="plan-name">C端会员</text>
+          <view class="plan-price-row">
+            <text class="price-symbol">¥</text>
+            <text class="price">{{ formatYuan(pricing.annualFee) }}</text>
+            <text class="per">/年</text>
+          </view>
+          <text class="price-daily">每天仅 ¥{{ (pricing.annualFee / 100 / 365).toFixed(2) }}</text>
+        </view>
+        <view class="plan-features">
+          <text class="feature">✦ AI 互动问答 — 无限次</text>
+          <text class="feature">✦ 获得推广权限,赚取佣金</text>
+          <text class="feature">✦ 一级 40% + 二级 5% 佣金比例</text>
+          <text class="feature">✦ VIP 标识</text>
+        </view>
       </view>
-      <view class="summary-row total">
-        <text>合计</text>
-        <text>¥398</text>
+
+      <button class="pay-btn" @click="onPay" :disabled="paying">
+        {{ paying ? '处理中...' : `立即开通 · C端会员 ¥${formatYuan(pricing.annualFee)}/年` }}
+      </button>
+    </template>
+
+    <template v-else-if="product === 'practitioner'">
+      <!-- 能量师 种子价/标准价 -->
+      <view class="plan-card plan-pro selected">
+        <view class="plan-badge pro">专业</view>
+        <view class="plan-top">
+          <text class="plan-name">能量师</text>
+          <view class="plan-price-row">
+            <text class="price-symbol">¥</text>
+            <text class="price">{{ formatYuan(currentPrice) }}</text>
+            <text class="per">/年</text>
+          </view>
+          <view v-if="pricing.isSeedPrice" class="seed-badge">
+            种子价 · 仅剩 {{ pricing.remainingSeats }}/{{ pricing.seedLimit }} 个名额
+          </view>
+          <text v-else class="standard-label">标准定价</text>
+          <text class="price-daily">每天仅 ¥{{ (currentPrice / 100 / 365).toFixed(2) }}</text>
+        </view>
+        <view class="plan-features">
+          <text class="feature">✦ 包含 C 端全部权益</text>
+          <text class="feature">✦ 无限命盘生成</text>
+          <text class="feature">✦ AI 五区完整解读,随问随答</text>
+          <text class="feature">✦ 全部历史记录永久保存</text>
+          <text class="feature">✦ 推广佣金 — 固定 ¥500/人 + 二级 ¥100/人</text>
+        </view>
       </view>
-    </view>
 
-    <button class="pay-btn" @click="handlePay" :disabled="paying">
-      {{ paying ? '支付中...' : '立即支付 ¥398' }}
-    </button>
+      <button class="pay-btn pro-btn" @click="onPay" :disabled="paying">
+        {{ paying ? '处理中...' : `立即开通 · 能量师 ${priceLabel} ¥${formatYuan(currentPrice)}/年` }}
+      </button>
+    </template>
+
+    <text class="note">支付即表示同意《订阅协议》</text>
   </view>
 </template>
 
 <script setup>
-import { ref } from 'vue'
-import { payApi } from '@/utils/api'
+import { ref, computed, onMounted } from 'vue'
+import { payApi, pricingApi } from '@/utils/api'
 
 const paying = ref(false)
+const loading = ref(true)
+const product = ref('annual')
+const pricing = ref({
+  seedPrice: 131400,
+  standardPrice: 198600,
+  annualFee: 13100,
+  isSeedPrice: true,
+  remainingSeats: 300,
+  seedLimit: 300
+})
+
+const pageTitle = computed(() => {
+  return product.value === 'annual' ? 'C端会员' : '能量师专业版'
+})
+const pageDesc = computed(() => {
+  return product.value === 'annual' ? '解锁无限AI解读,获取推广佣金' : '获取完整命盘分析、AI解读与分销推广权限'
+})
+
+const currentPrice = computed(() => {
+  if (product.value === 'annual') return pricing.value.annualFee
+  return pricing.value.isSeedPrice ? pricing.value.seedPrice : pricing.value.standardPrice
+})
+const priceLabel = computed(() => {
+  return pricing.value.isSeedPrice ? '种子价' : '标准价'
+})
+
+function formatYuan(cents) {
+  if (cents == null) return '0'
+  return (cents / 100).toLocaleString()
+}
+
+onMounted(async () => {
+  // Read product param from URL
+  const pages = getCurrentPages()
+  const page = pages[pages.length - 1]
+  const params = page?.options || {}
+  product.value = params.product || 'annual'
 
-async function handlePay() {
+  try {
+    const res = await pricingApi.current()
+    if (res) pricing.value = res
+  } catch (e) {
+    // Use defaults if API fails
+  }
+  loading.value = false
+})
+
+async function onPay() {
   paying.value = true
   try {
-    const order = await payApi.create({ totalFee: 39800, payType: 'wxpay' })
-    // Dev mode — WxPay returns mock prepay params directly
-    if (order.mock) {
-      // In dev/mock mode, simulate payment success
-      uni.showToast({ title: '支付成功!', icon: 'success' })
-      setTimeout(() => uni.navigateBack(), 1500)
-    } else if (order.prepayId) {
-      // Real WeChat Pay — invoke payment SDK
-      uni.requestPayment({
-        provider: 'wxpay',
-        timeStamp: order.timeStamp,
-        nonceStr: order.nonceStr,
-        package: order.package,
-        signType: order.signType || 'MD5',
-        paySign: order.paySign,
-        success: () => {
-          uni.showToast({ title: '支付成功!', icon: 'success' })
-          setTimeout(() => uni.navigateBack(), 1500)
-        },
-        fail: () => {
-          uni.showToast({ title: '支付取消', icon: 'none' })
-        }
-      })
-    }
+    const totalFee = currentPrice.value
+    const productType = product.value
+    await payApi.create({ totalFee, payType: 'wxpay', productType })
+    uni.showToast({ title: '开通成功!', icon: 'success' })
+    setTimeout(() => uni.navigateBack(), 1500)
   } catch (e) {
-    uni.showToast({ title: '支付失败: ' + (e.message || ''), icon: 'none' })
-  } finally {
-    paying.value = false
+    uni.showToast({ title: '支付失败,请重试', icon: 'none' })
   }
+  paying.value = false
 }
 </script>
 
 <style scoped lang="scss">
-.page-payment { padding: 16px; }
+.page-payment {
+  padding: 20px;
+  min-height: 100vh;
+  background: linear-gradient(180deg, #0c0a1a 0%, #120d28 60%, #0f0c1e 100%);
+}
+
+.header {
+  text-align: center;
+  margin-bottom: 20px;
+}
+.header-title {
+  font-size: 22px;
+  font-weight: 700;
+  color: rgba(255,255,255,0.9);
+  display: block;
+}
+.header-desc {
+  font-size: 13px;
+  color: rgba(255,255,255,0.4);
+  margin-top: 6px;
+  display: block;
+}
+
+.loading-state {
+  padding: 60px 0;
+  text-align: center;
+}
+.loading-text {
+  font-size: 14px;
+  color: rgba(255,255,255,0.3);
+}
+
+/* Plan Cards */
 .plan-card {
-  background: #fff; border-radius: 16px; padding: 24px;
-  text-align: center; margin-bottom: 16px;
+  position: relative;
+  background: rgba(255,255,255,0.03);
+  border: 1px solid rgba(255,255,255,0.08);
+  border-radius: 16px;
+  padding: 22px 18px 18px;
+  margin-bottom: 12px;
+}
+.plan-card.selected {
+  border-color: #fbbf24;
+  background: rgba(251,191,36,0.06);
+}
+.plan-card.plan-pro.selected {
+  border-color: #a78bfa;
+  background: rgba(167,139,250,0.06);
+}
+
+.plan-badge {
+  position: absolute;
+  top: -1px;
+  right: 20px;
+  font-size: 11px;
+  font-weight: 600;
+  padding: 3px 14px;
+  border-radius: 0 0 8px 8px;
+}
+.plan-badge.popular {
+  background: #f59e0b;
+  color: #fff;
+}
+.plan-badge.pro {
+  background: #7c3aed;
+  color: #fff;
+}
+
+.plan-top { text-align: center; }
+.plan-name {
+  font-size: 17px;
+  font-weight: 600;
+  color: rgba(255,255,255,0.85);
+  display: block;
+  margin-bottom: 10px;
+}
+.plan-price-row {
+  display: flex;
+  align-items: baseline;
+  justify-content: center;
+}
+.price-symbol {
+  font-size: 20px;
+  color: #fbbf24;
+  font-weight: 600;
 }
-.plan-header { margin-bottom: 20px; }
-.plan-name { font-size: 20px; font-weight: 700; color: #1F2937; display: block; }
-.plan-price {
-  font-size: 36px; font-weight: 800; color: #3B82F6; display: inline; margin-top: 8px;
+.price {
+  font-size: 38px;
+  font-weight: 700;
+  color: #fbbf24;
+  margin: 0 2px;
+}
+.plan-pro .price-symbol,
+.plan-pro .price {
+  color: #a78bfa;
+}
+.per {
+  font-size: 14px;
+  color: rgba(255,255,255,0.4);
+}
+.price-daily {
+  display: block;
+  font-size: 12px;
+  color: rgba(255,255,255,0.3);
+  margin-top: 4px;
+}
+.seed-badge {
+  display: inline-block;
+  margin-top: 6px;
+  font-size: 11px;
+  color: #fbbf24;
+  background: rgba(251,191,36,0.12);
+  padding: 2px 12px;
+  border-radius: 4px;
+}
+.standard-label {
+  display: inline-block;
+  margin-top: 6px;
+  font-size: 11px;
+  color: rgba(255,255,255,0.35);
+  padding: 2px 12px;
+  border-radius: 4px;
+  border: 1px solid rgba(255,255,255,0.1);
+}
+
+.plan-features {
+  margin-top: 16px;
 }
-.plan-period { font-size: 14px; color: #6B7280; }
-.plan-features { text-align: left; }
 .feature {
-  display: block; padding: 6px 0; color: #4B5563; font-size: 14px;
+  display: block;
+  padding: 5px 0;
+  font-size: 13px;
+  color: rgba(255,255,255,0.6);
+  line-height: 1.5;
 }
-.order-summary {
-  background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 24px;
+.plan-card.selected .feature {
+  color: rgba(255,255,255,0.8);
 }
-.summary-title { font-size: 16px; font-weight: 600; margin-bottom: 12px; display: block; }
-.summary-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 14px; }
-.summary-row.total { border-top: 1px solid #E5E7EB; margin-top: 8px; padding-top: 12px; font-weight: 700; }
+
 .pay-btn {
-  width: 100%; height: 48px; background: #3B82F6; color: #fff;
-  border: none; border-radius: 12px; font-size: 18px; line-height: 48px;
+  width: 100%;
+  padding: 16px;
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+  color: #fff;
+  border: none;
+  border-radius: 14px;
+  font-size: 17px;
+  font-weight: 600;
+  letter-spacing: 2px;
+  margin-bottom: 8px;
+}
+.pay-btn.pro-btn {
+  background: linear-gradient(135deg, #7c3aed, #6d28d9);
+}
+.pay-btn[disabled] { opacity: 0.4; }
+
+.note {
+  display: block;
+  text-align: center;
+  font-size: 12px;
+  color: rgba(255,255,255,0.25);
 }
-.pay-btn[disabled] { opacity: 0.6; }
 </style>

+ 63 - 5
client/pages/profile/index.vue

@@ -1,5 +1,11 @@
 <template>
   <view class="page-profile">
+    <!-- US-5.1 §13: Profile incomplete banner -->
+    <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
+      <text class="banner-text">📝 请完善个人资料,开启能量匹配</text>
+      <text class="banner-arrow">›</text>
+    </view>
+
     <!-- User info card -->
     <view class="user-card" @click="goLogin">
       <view class="avatar">
@@ -11,14 +17,32 @@
     </view>
 
     <!-- VIP status -->
-    <view class="vip-card" @click="goPayment">
+    <view v-if="userStore.vipType === 'practitioner'" class="vip-card pro" @click="goPayment">
+      <view class="vip-info">
+        <text class="vip-title">能量师会员</text>
+        <text class="vip-desc">
+          有效期至: {{ userStore.vipEndTime?.slice(0, 10) }}
+        </text>
+      </view>
+      <text class="vip-action">续费</text>
+    </view>
+    <view v-else-if="userStore.vipType === 'annual'" class="vip-card annual" @click="goUpgradePractitioner">
+      <view class="vip-info">
+        <text class="vip-title">C端会员</text>
+        <text class="vip-desc">
+          有效期至: {{ userStore.vipEndTime?.slice(0, 10) }}
+        </text>
+      </view>
+      <text class="vip-action">升级能量师</text>
+    </view>
+    <view v-else class="vip-card" @click="goPayment">
       <view class="vip-info">
         <text class="vip-title">VIP 会员</text>
         <text class="vip-desc">
-          {{ userStore.isVip ? `有效期至: ${userStore.vipEndTime?.slice(0, 10)}` : '开通VIP获取完整分析' }}
+          开通VIP获取完整分析
         </text>
       </view>
-      <text class="vip-action">{{ userStore.isVip ? '续费' : '立即开通' }}</text>
+      <text class="vip-action">立即开通</text>
     </view>
 
     <!-- Daily quota (non-VIP) -->
@@ -36,6 +60,10 @@
 
     <!-- Menu list -->
     <view class="menu-section">
+      <view class="menu-item" @click="goEditProfile">
+        <text>✏️ 编辑资料</text>
+        <text class="menu-arrow">›</text>
+      </view>
       <view class="menu-item" @click="goRecords">
         <text>📋 我的分析</text>
         <text class="menu-arrow">›</text>
@@ -73,6 +101,14 @@ onShow(() => {
   }
 })
 
+function goEditProfile() {
+  if (!userStore.isLoggedIn) {
+    uni.navigateTo({ url: '/pages/login/index' })
+    return
+  }
+  uni.navigateTo({ url: '/pages/profile/edit' })
+}
+
 function goLogin() {
   if (!userStore.isLoggedIn) {
     uni.navigateTo({ url: '/pages/login/index' })
@@ -80,7 +116,10 @@ function goLogin() {
 }
 
 function goPayment() {
-  uni.navigateTo({ url: '/pages/payment/index' })
+  uni.navigateTo({ url: '/pages/payment/index?product=annual' })
+}
+function goUpgradePractitioner() {
+  uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
 }
 
 function goRecords() {
@@ -88,7 +127,11 @@ function goRecords() {
 }
 
 function goCommission() {
-  uni.showToast({ title: '推广功能开发中', icon: 'none' })
+  if (!userStore.isLoggedIn) {
+    uni.navigateTo({ url: '/pages/login/index' })
+    return
+  }
+  uni.navigateTo({ url: '/pages/commission/index' })
 }
 
 function copyReferral() {
@@ -136,6 +179,12 @@ function handleLogout() {
   padding: 16px; display: flex; justify-content: space-between;
   align-items: center; margin-bottom: 16px;
 }
+.vip-card.annual {
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+}
+.vip-card.pro {
+  background: linear-gradient(135deg, #7c3aed, #6d28d9);
+}
 .vip-title { color: #fff; font-weight: 600; font-size: 16px; display: block; }
 .vip-desc { color: rgba(255,255,255,0.8); font-size: 12px; display: block; margin-top: 4px; }
 .vip-action { color: #fff; font-size: 14px; font-weight: 500; }
@@ -156,4 +205,13 @@ function handleLogout() {
 .quota-label { font-size: 12px; color: #6B7280; display: block; margin-bottom: 4px; }
 .quota-value { font-size: 20px; font-weight: 700; color: #1F2937; }
 .quota-divider { width: 1px; height: 36px; background: #E5E7EB; }
+
+/* US-5.1 §13 profile incomplete banner */
+.banner {
+  background: #FEF3C7; border: 1px solid #FDE68A; border-radius: 10px;
+  padding: 10px 14px; margin-bottom: 12px;
+  display: flex; justify-content: space-between; align-items: center;
+}
+.banner-text { font-size: 13px; color: #92400E; flex: 1; }
+.banner-arrow { font-size: 18px; color: #92400E; margin-left: 8px; }
 </style>

+ 231 - 102
client/pages/subscribe/index.vue

@@ -1,18 +1,64 @@
 <template>
   <view class="page">
-    <!-- Hero area -->
-    <view class="hero">
-      <text class="hero-icon">✦</text>
-      <text class="price">¥398</text>
-      <text class="per">/年</text>
-      <text class="desc">每天仅 ¥1.09,让数字能量成为你的生产力</text>
+    <view class="header">
+      <text class="header-title">选择会员方案</text>
+      <text class="header-desc">解锁数字能量的全部潜力</text>
     </view>
 
-    <!-- Benefits -->
-    <view class="card">
-      <view class="benefit" v-for="item in benefits" :key="item">
-        <text class="check-icon">◆</text>
-        <text>{{ item }}</text>
+    <template v-if="loading">
+      <view class="loading-state"><text class="loading-text">加载中...</text></view>
+    </template>
+
+    <template v-else>
+    <!-- C端 ¥131 方案 -->
+    <view
+      class="plan-card"
+      :class="{ selected: selectedPlan === 'annual' }"
+      @click="selectedPlan = 'annual'"
+    >
+      <view class="plan-badge popular">推荐</view>
+      <view class="plan-top">
+        <text class="plan-name">C端会员</text>
+        <view class="plan-price-row">
+          <text class="price-symbol">¥</text>
+          <text class="price">{{ formatYuan(pricing.annualFee) }}</text>
+          <text class="per">/年</text>
+        </view>
+        <text class="price-daily">每天仅 ¥{{ (pricing.annualFee / 100 / 365).toFixed(2) }}</text>
+      </view>
+      <view class="plan-features">
+        <text class="feature">✦ AI 互动问答 — 无限次</text>
+        <text class="feature">✦ 获得推广权限,赚取佣金</text>
+        <text class="feature">✦ 一级 40% + 二级 5% 佣金比例</text>
+        <text class="feature">✦ VIP 标识</text>
+      </view>
+    </view>
+
+    <!-- B端 ¥1,314 方案 -->
+    <view
+      class="plan-card plan-pro"
+      :class="{ selected: selectedPlan === 'practitioner' }"
+      @click="selectedPlan = 'practitioner'"
+    >
+      <view class="plan-badge pro">专业</view>
+      <view class="plan-top">
+        <text class="plan-name">能量师</text>
+        <view class="plan-price-row">
+          <text class="price-symbol">¥</text>
+          <text class="price">{{ formatYuan(currentPractitionerPrice) }}</text>
+          <text class="per">/年</text>
+        </view>
+        <view v-if="pricing.isSeedPrice" class="seed-badge">
+          种子价 · 仅剩 {{ pricing.remainingSeats }}/{{ pricing.seedLimit }} 个名额
+        </view>
+        <text class="price-daily">每天仅 ¥{{ (currentPractitionerPrice / 100 / 365).toFixed(2) }}</text>
+      </view>
+      <view class="plan-features">
+        <text class="feature">✦ 包含 C 端全部权益</text>
+        <text class="feature">✦ 无限命盘生成</text>
+        <text class="feature">✦ AI 五区完整解读,随问随答</text>
+        <text class="feature">✦ 全部历史记录永久保存</text>
+        <text class="feature">✦ 推广佣金 — 固定 ¥500/人 + 二级 ¥100/人</text>
       </view>
     </view>
 
@@ -20,49 +66,85 @@
     <view class="compare-card">
       <view class="compare-header">
         <text class="col">功能</text>
-        <text class="col">普通用户</text>
+        <text class="col">普通</text>
+        <text class="col c">C端</text>
         <text class="col pro">能量师</text>
       </view>
       <view class="compare-row" v-for="row in compareData" :key="row.label">
         <text class="col">{{ row.label }}</text>
-        <text class="col free">{{ row.free }}</text>
+        <text class="col">{{ row.free }}</text>
+        <text class="col c">{{ row.c }}</text>
         <text class="col pro">{{ row.pro }}</text>
       </view>
     </view>
 
     <!-- Pay button -->
     <button class="pay-btn" @click="onPay" :disabled="paying">
-      {{ paying ? '处理中...' : '立即开通 · 能量师 ¥398/年' }}
+      {{ paying ? '处理中...' : payButtonText }}
     </button>
     <text class="note">支付即表示同意《订阅协议》</text>
+    </template>
   </view>
 </template>
 
 <script setup>
-import { ref } from 'vue'
-import { payApi } from '@/utils/api'
+import { ref, computed, onMounted } from 'vue'
+import { payApi, pricingApi } from '@/utils/api'
 
 const paying = ref(false)
-
-const benefits = [
-  '无限命盘生成,不限次数',
-  'AI 五区完整解读,随问随答',
-  '无限分享 & PDF 导出',
-  '全部历史记录',
-  '推广赚佣金,¥119.4/人'
-]
+const loading = ref(true)
+const selectedPlan = ref('annual')
+const pricing = ref({
+  seedPrice: 131400,
+  standardPrice: 198600,
+  annualFee: 13100,
+  isSeedPrice: true,
+  remainingSeats: 300,
+  seedLimit: 300
+})
 
 const compareData = [
-  { label: '命盘生成', free: '3次/天', pro: '不限' },
-  { label: 'AI 解读', free: '3轮/天', pro: '无限次' },
-  { label: '分享', free: '1次/天', pro: '不限' },
-  { label: '推广佣金', free: '--', pro: '最高¥159.2/人' },
+  { label: '命盘生成', free: '3次/天', c: '3次/天', pro: '不限' },
+  { label: 'AI 解读', free: '3轮/天', c: '无限次', pro: '无限次' },
+  { label: '历史记录', free: '7天', c: '永久', pro: '永久' },
+  { label: '推广佣金', free: '--', c: '40%+5%', pro: '¥500+¥100/人' },
+  { label: '分享导出', free: '1次/天', c: '不限', pro: '不限' },
 ]
 
+const currentPractitionerPrice = computed(() => {
+  return pricing.value.isSeedPrice ? pricing.value.seedPrice : pricing.value.standardPrice
+})
+
+const payButtonText = computed(() => {
+  if (selectedPlan.value === 'annual') {
+    return `立即开通 · C端会员 ¥${formatYuan(pricing.value.annualFee)}/年`
+  }
+  const price = currentPractitionerPrice.value
+  const label = pricing.value.isSeedPrice ? '种子价' : '标准价'
+  return `立即开通 · 能量师 ${label} ¥${formatYuan(price)}/年`
+})
+
+function formatYuan(cents) {
+  if (cents == null) return '0'
+  return (cents / 100).toLocaleString()
+}
+
+onMounted(async () => {
+  try {
+    const res = await pricingApi.current()
+    if (res) pricing.value = res
+  } catch (e) {
+    // Use defaults if API fails
+  }
+  loading.value = false
+})
+
 async function onPay() {
   paying.value = true
   try {
-    const res = await payApi.create({ totalFee: 39800, payType: 'wxpay' })
+    const totalFee = selectedPlan.value === 'annual' ? pricing.value.annualFee : currentPractitionerPrice.value
+    const productType = selectedPlan.value
+    await payApi.create({ totalFee, payType: 'wxpay', productType })
     uni.showToast({ title: '开通成功!', icon: 'success' })
     setTimeout(() => uni.navigateBack(), 1500)
   } catch (e) {
@@ -79,116 +161,166 @@ async function onPay() {
   background: linear-gradient(180deg, #0c0a1a 0%, #120d28 60%, #0f0c1e 100%);
 }
 
-.hero {
-  background: linear-gradient(135deg, rgba(251, 191, 36, 0.1), rgba(217, 119, 6, 0.06));
-  border: 1px solid rgba(251, 191, 36, 0.12);
-  border-radius: 18px;
-  padding: 36px 20px;
+.header {
   text-align: center;
+  margin-bottom: 20px;
+}
+.header-title {
+  font-size: 22px;
+  font-weight: 700;
+  color: rgba(255,255,255,0.9);
+  display: block;
+}
+.header-desc {
+  font-size: 13px;
+  color: rgba(255,255,255,0.4);
+  margin-top: 6px;
+  display: block;
+}
+
+.loading-state {
+  padding: 60px 0;
+  text-align: center;
+}
+.loading-text {
+  font-size: 14px;
+  color: rgba(255,255,255,0.3);
+}
+
+/* Plan Cards */
+.plan-card {
+  position: relative;
+  background: rgba(255,255,255,0.03);
+  border: 1px solid rgba(255,255,255,0.08);
+  border-radius: 16px;
+  padding: 22px 18px 18px;
+  margin-bottom: 12px;
+  transition: border-color 0.2s;
+}
+.plan-card.selected {
+  border-color: #fbbf24;
+  background: rgba(251,191,36,0.06);
+}
+.plan-card.plan-pro.selected {
+  border-color: #a78bfa;
+  background: rgba(167,139,250,0.06);
+}
+
+.plan-badge {
+  position: absolute;
+  top: -1px;
+  right: 20px;
+  font-size: 11px;
+  font-weight: 600;
+  padding: 3px 14px;
+  border-radius: 0 0 8px 8px;
+}
+.plan-badge.popular {
+  background: #f59e0b;
+  color: #fff;
+}
+.plan-badge.pro {
+  background: #7c3aed;
   color: #fff;
-  margin-bottom: 16px;
 }
 
-.hero-icon {
+.plan-top { text-align: center; }
+.plan-name {
+  font-size: 17px;
+  font-weight: 600;
+  color: rgba(255,255,255,0.85);
   display: block;
+  margin-bottom: 10px;
+}
+.plan-price-row {
+  display: flex;
+  align-items: baseline;
+  justify-content: center;
+}
+.price-symbol {
   font-size: 20px;
   color: #fbbf24;
-  margin-bottom: 12px;
-  opacity: 0.6;
+  font-weight: 600;
 }
-
 .price {
-  font-size: 48px;
+  font-size: 38px;
   font-weight: 700;
   color: #fbbf24;
-  text-shadow: 0 0 30px rgba(251, 191, 36, 0.2);
+  margin: 0 2px;
+}
+.plan-pro .price-symbol,
+.plan-pro .price {
+  color: #a78bfa;
 }
-
 .per {
-  font-size: 16px;
-  color: rgba(255, 255, 255, 0.5);
-  margin-left: 4px;
+  font-size: 14px;
+  color: rgba(255,255,255,0.4);
 }
-
-.desc {
+.price-daily {
   display: block;
-  font-size: 13px;
-  color: rgba(255, 255, 255, 0.4);
-  margin-top: 14px;
-  letter-spacing: 1px;
+  font-size: 12px;
+  color: rgba(255,255,255,0.3);
+  margin-top: 4px;
 }
-
-.card {
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.06);
-  border-radius: 14px;
-  padding: 20px;
-  margin-bottom: 16px;
+.seed-badge {
+  display: inline-block;
+  margin-top: 6px;
+  font-size: 11px;
+  color: #fbbf24;
+  background: rgba(251,191,36,0.12);
+  padding: 2px 12px;
+  border-radius: 4px;
 }
 
-.benefit {
-  padding: 8px 0;
-  font-size: 14px;
-  color: rgba(255, 255, 255, 0.7);
-  display: flex;
-  align-items: center;
+.plan-features {
+  margin-top: 16px;
 }
-
-.check-icon {
-  color: #fbbf24;
-  margin-right: 12px;
-  font-size: 10px;
-  opacity: 0.6;
+.feature {
+  display: block;
+  padding: 5px 0;
+  font-size: 13px;
+  color: rgba(255,255,255,0.6);
+  line-height: 1.5;
+}
+.plan-card.selected .feature {
+  color: rgba(255,255,255,0.8);
 }
 
+/* Compare table */
 .compare-card {
-  background: rgba(255, 255, 255, 0.03);
-  border: 1px solid rgba(255, 255, 255, 0.06);
+  background: rgba(255,255,255,0.03);
+  border: 1px solid rgba(255,255,255,0.06);
   border-radius: 14px;
   overflow: hidden;
   margin-bottom: 20px;
 }
-
 .compare-header,
 .compare-row {
   display: flex;
-  padding: 14px 16px;
-  font-size: 13px;
+  padding: 12px 10px;
+  font-size: 12px;
 }
-
 .compare-header {
-  background: rgba(251, 191, 36, 0.06);
+  background: rgba(251,191,36,0.06);
   font-weight: 600;
-  color: rgba(255, 255, 255, 0.7);
+  color: rgba(255,255,255,0.7);
 }
-
 .compare-row {
-  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
-}
-
-.compare-row:last-child {
-  border-bottom: none;
+  border-bottom: 1px solid rgba(255,255,255,0.04);
+  color: rgba(255,255,255,0.5);
 }
-
+.compare-row:last-child { border-bottom: none; }
 .col {
   flex: 1;
   text-align: center;
-  color: rgba(255, 255, 255, 0.5);
 }
-
 .col:first-child {
   text-align: left;
-  flex: 1.5;
-  color: rgba(255, 255, 255, 0.7);
-}
-
-.free {
-  color: rgba(255, 255, 255, 0.3);
-}
-
-.pro {
-  color: #fbbf24;
+  flex: 1.3;
+  color: rgba(255,255,255,0.7);
 }
+.col.c { color: #fbbf24; }
+.col.pro { color: #a78bfa; }
 
 .pay-btn {
   width: 100%;
@@ -202,15 +334,12 @@ async function onPay() {
   letter-spacing: 2px;
   margin-bottom: 8px;
 }
-
-.pay-btn[disabled] {
-  opacity: 0.4;
-}
+.pay-btn[disabled] { opacity: 0.4; }
 
 .note {
   display: block;
   text-align: center;
   font-size: 12px;
-  color: rgba(255, 255, 255, 0.25);
+  color: rgba(255,255,255,0.25);
 }
 </style>

+ 49 - 2
client/stores/user.js

@@ -7,6 +7,7 @@ export const useUserStore = defineStore('user', {
     nickname: '',
     avatarUrl: '',
     isVip: false,
+    vipType: null,
     vipEndTime: null,
     referralCode: '',
     // Daily quota
@@ -14,9 +15,23 @@ export const useUserStore = defineStore('user', {
     maxCharts: 3,
     usedChats: 0,
     maxChats: 3,
+    shareCount: 0,
+    shareLimit: 1,
     // Commission
     totalCommission: 0,
-    availableCommission: 0
+    availableCommission: 0,
+    // US-5.1 Profile fields
+    profileIncomplete: false,
+    gender: 0,
+    birthDate: null,
+    bio: '',
+    city: '',
+    tags: '',
+    lookingFor: 0,
+    profileComplete: false,
+    birthYear: 0,
+    birthMonth: 0,
+    birthDay: 0
   }),
   getters: {
     isLoggedIn: (state) => !!state.token,
@@ -35,6 +50,8 @@ export const useUserStore = defineStore('user', {
       uni.setStorageSync('token', tokenStr)
       this.nickname = nickname
       this.avatarUrl = avatarUrl
+      // US-5.1 §5-7: Check profile completion
+      this.profileIncomplete = res.profileIncomplete === true
     },
     async wechatLogin(code, nickname, avatarUrl) {
       const data = { code, nickname, avatarUrl }
@@ -42,8 +59,10 @@ export const useUserStore = defineStore('user', {
       const tokenStr = res.token || res
       this.token = tokenStr
       uni.setStorageSync('token', tokenStr)
-      this.nickname = nickname || `微信用户`
+      this.nickname = nickname || '微信用户'
       this.avatarUrl = avatarUrl || ''
+      // US-5.1 §5-7: Check profile completion
+      this.profileIncomplete = res.profileIncomplete === true
       // Fetch full profile after login
       try { await this.fetchProfile() } catch (e) {}
     },
@@ -53,7 +72,20 @@ export const useUserStore = defineStore('user', {
       this.avatarUrl = user.avatarUrl
       this.referralCode = user.referralCode
       this.isVip = !!user.vipEndTime && new Date(user.vipEndTime) > new Date()
+      this.vipType = user.vipType || null
       this.vipEndTime = user.vipEndTime
+      // US-5.1 profile fields
+      this.gender = user.gender || 0
+      this.birthDate = user.birthDate || null
+      this.bio = user.bio || ''
+      this.city = user.city || ''
+      this.tags = user.tags || ''
+      this.lookingFor = user.lookingFor || 0
+      this.profileComplete = !!user.profileComplete
+      this.birthYear = user.birthYear || 0
+      this.birthMonth = user.birthMonth || 0
+      this.birthDay = user.birthDay || 0
+      this.profileIncomplete = !this.profileComplete
     },
     async fetchQuota() {
       try {
@@ -62,6 +94,8 @@ export const useUserStore = defineStore('user', {
         this.maxCharts = quota.chartLimit > 0 ? quota.chartLimit : 999
         this.usedChats = quota.dailyChatCount || 0
         this.maxChats = quota.chatLimit > 0 ? quota.chatLimit : 999
+        this.shareCount = quota.dailyShareCount || 0
+        this.shareLimit = quota.shareLimit > 0 ? quota.shareLimit : 1
       } catch (e) { /* ignore */ }
     },
     async fetchCommissionSummary() {
@@ -77,8 +111,21 @@ export const useUserStore = defineStore('user', {
       this.avatarUrl = ''
       this.usedCharts = 0
       this.usedChats = 0
+      this.shareCount = 0
+      this.shareLimit = 1
       this.totalCommission = 0
       this.availableCommission = 0
+      this.profileIncomplete = false
+      this.gender = 0
+      this.birthDate = null
+      this.bio = ''
+      this.city = ''
+      this.tags = ''
+      this.lookingFor = 0
+      this.profileComplete = false
+      this.birthYear = 0
+      this.birthMonth = 0
+      this.birthDay = 0
       uni.removeStorageSync('token')
     }
   }

+ 25 - 4
client/utils/api.js

@@ -58,14 +58,16 @@ export const authApi = {
 
 export const consultationApi = {
   start: (data) => requestInstance.post('/consultation/start', data),
-  check: (data) => requestInstance.post('/consultation/check', data)
+  check: (data) => requestInstance.post('/consultation/check', data),
+  regenerate: (chartRecordId) => requestInstance.post('/consultation/regenerate', { chartRecordId })
 }
 
 export const chartApi = {
   create: (data) => requestInstance.post('/chart/create', data),
   list: () => requestInstance.post('/chart/list', {}),
   detail: (id) => requestInstance.post('/chart/detail', { id }),
-  delete: (id) => requestInstance.post('/chart/delete', { id })
+  delete: (id) => requestInstance.post('/chart/delete', { id }),
+  interpret: (chartRecordId) => requestInstance.post('/chart/interpret', { chartRecordId })
 }
 
 export const chatApi = {
@@ -77,12 +79,19 @@ export const payApi = {
   create: (data) => requestInstance.post('/pay/create', data)
 }
 
+export const pricingApi = {
+  current: () => requestInstance.post('/pricing/current', {})
+}
+
 export const profileApi = {
   info: () => requestInstance.post('/profile/info', {}),
   vipStatus: () => requestInstance.post('/profile/vip-status', {}),
   quota: () => requestInstance.post('/profile/quota', {}),
   commissionSummary: () => requestInstance.post('/profile/commission-summary', {}),
-  chartCount: () => requestInstance.post('/profile/chart-count', {})
+  chartCount: () => requestInstance.post('/profile/chart-count', {}),
+  trackShare: () => requestInstance.post('/profile/share/track', {}),
+  complete: (data) => requestInstance.post('/profile/complete', data),
+  update: (data) => requestInstance.post('/profile/update', data)
 }
 
 export const annotationApi = {
@@ -96,5 +105,17 @@ export const annotationApi = {
 
 export const commissionApi = {
   list: () => requestInstance.post('/commission/list', {}),
-  total: () => requestInstance.post('/commission/total', {})
+  total: () => requestInstance.post('/commission/total', {}),
+  team: () => requestInstance.post('/commission/team', {}),
+  stats: () => requestInstance.post('/commission/stats', {})
+}
+
+export const withdrawApi = {
+  apply: (amount) => requestInstance.post('/withdraw/apply', { amount }),
+  list: () => requestInstance.post('/withdraw/list', {}),
+  balance: () => requestInstance.post('/withdraw/balance', {})
+}
+
+export const exportApi = {
+  pdf: (recordId) => requestInstance.post('/export/pdf', { recordId })
 }