Kaynağa Gözat

feat(frontend): 改版v1 C类前端 - 问题流/通知共8页面(F4-F11)

F4 domain-list(问题域3卡), F5 flow-intro(7步流程时间线), F6 survey(专项调研问卷), F7 package-match(推荐套餐列表), F8 package-detail(套餐详情+购买), F9 logistics(4步物流), F10 plan-confirm(方案确认+任务生成), F11 notification(站内通知列表+下拉刷新)。无CSS Grid/可选链,遵循小程序规范。

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 ay önce
ebeveyn
işleme
60b4f87340

+ 219 - 0
cfc-frontend/pages/notification/index.vue

@@ -0,0 +1,219 @@
+<template>
+  <view class="nt-wrap">
+    <view class="nt-loading" v-if="loading">加载中...</view>
+
+    <view class="nt-empty" v-else-if="list.length === 0">
+      <text class="nt-empty-icon">🔔</text>
+      <text class="nt-empty-text">暂无通知</text>
+    </view>
+
+    <view class="nt-list" v-else>
+      <view class="nt-item" v-for="item in list" :key="getKey(item)" @click="onTap(item)">
+        <view class="nt-item-icon">
+          <text class="nt-icon">{{ getIcon(item.type) }}</text>
+          <view class="nt-unread" v-if="item.is_read === 0 || item.isRead === 0"></view>
+        </view>
+        <view class="nt-item-body">
+          <view class="nt-item-top">
+            <text class="nt-item-title">{{ item.title }}</text>
+            <text class="nt-item-time">{{ formatTime(item.created_at || item.createdAt) }}</text>
+          </view>
+          <text class="nt-item-content">{{ item.content }}</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getNotificationList } from '../../utils/api.js'
+import { readNotification } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false
+    }
+  },
+  onLoad() {
+    this.loadList()
+  },
+  onShow() {
+    this.loadList()
+  },
+  onPullDownRefresh() {
+    this.loadList()
+  },
+  methods: {
+    getKey(item) {
+      return item.id || Math.random()
+    },
+    getIcon(type) {
+      if (type === 'report_ready') return '📋'
+      if (type === 'reward_granted') return '🎁'
+      return '🔔'
+    },
+    formatTime(dateStr) {
+      var d = parseDate(dateStr)
+      if (!d) return dateStr || ''
+      var y = d.getFullYear()
+      var m = d.getMonth() + 1
+      var day = d.getDate()
+      var h = d.getHours()
+      var min = d.getMinutes()
+      return y + '-' + pad(m) + '-' + pad(day) + ' ' + pad(h) + ':' + pad(min)
+    },
+    loadList() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getNotificationList().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        uni.stopPullDownRefresh()
+        var data = res && res.data
+        self.list = Array.isArray(data) ? data : []
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        uni.stopPullDownRefresh()
+        self.list = []
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      })
+    },
+    onTap(item) {
+      var self = this
+      var isRead = item.is_read === 0 || item.isRead === 0
+      if (isRead) {
+        // 标记已读
+        readNotification({ id: item.id }).then(function() {
+          item.is_read = 1
+          item.isRead = 1
+          self.$forceUpdate()
+        }).catch(function() {})
+      }
+      // 导航
+      var refType = item.ref_type || item.refType || ''
+      var refId = item.ref_id || item.refId
+      if (refType === 'report' || refType === 'plan') {
+        uni.navigateTo({ url: '/pages/problem/plan-confirm' })
+      } else if (refType === 'package' || refType === 'logistics') {
+        uni.navigateTo({ url: '/pages/problem/logistics' })
+      } else if (refType === 'task') {
+        uni.navigateTo({ url: '/pages/tasks/tasks' })
+      }
+    }
+  }
+}
+
+function pad(n) {
+  return n < 10 ? '0' + n : '' + n
+}
+</script>
+
+<style scoped>
+.nt-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 20rpx 0;
+  box-sizing: border-box;
+}
+
+.nt-loading {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.nt-empty {
+  text-align: center;
+  padding: 120rpx 0;
+}
+
+.nt-empty-icon {
+  font-size: 80rpx;
+  display: block;
+}
+
+.nt-empty-text {
+  display: block;
+  font-size: 32rpx;
+  color: #999;
+  margin-top: 20rpx;
+}
+
+.nt-item {
+  display: flex;
+  align-items: flex-start;
+  background: #fff;
+  padding: 28rpx 32rpx;
+  margin-bottom: 2rpx;
+}
+
+.nt-item:active {
+  background: #F9FAFB;
+}
+
+.nt-item-icon {
+  position: relative;
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 32rpx;
+  background: rgba(249, 115, 22, 0.1);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.nt-icon {
+  font-size: 32rpx;
+}
+
+.nt-unread {
+  position: absolute;
+  top: -2rpx;
+  right: -2rpx;
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  background: #EF4444;
+  border: 2rpx solid #fff;
+}
+
+.nt-item-body {
+  flex: 1;
+}
+
+.nt-item-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+
+.nt-item-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+}
+
+.nt-item-time {
+  font-size: 22rpx;
+  color: #999;
+  margin-left: 12rpx;
+  flex-shrink: 0;
+}
+
+.nt-item-content {
+  display: block;
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.5;
+}
+</style>

+ 158 - 0
cfc-frontend/pages/problem/domain-list.vue

@@ -0,0 +1,158 @@
+<template>
+  <view class="dl-wrap">
+    <view class="dl-header">
+      <text class="dl-title">你关心什么问题?</text>
+      <text class="dl-sub">选择一个你关注的领域,获取定制解决方案</text>
+    </view>
+    <view class="dl-list" v-if="loading">
+      <view class="dl-loading">加载中...</view>
+    </view>
+    <view class="dl-list" v-else-if="domains.length === 0">
+      <view class="dl-empty">暂无可用问题域</view>
+    </view>
+    <view class="dl-list" v-else>
+      <view class="dl-card" v-for="d in domains" :key="getKey(d)" @click="onSelect(d)">
+        <view class="dl-card-icon">
+          <text class="dl-card-emoji">{{ d.icon || '🧭' }}</text>
+        </view>
+        <view class="dl-card-body">
+          <text class="dl-card-name">{{ d.name }}</text>
+          <text class="dl-card-desc">{{ d.description || '了解详情,获取专属方案' }}</text>
+        </view>
+        <text class="dl-card-arrow">›</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getProblemDomainList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      domains: [],
+      loading: false
+    }
+  },
+  onLoad() {
+    this.loadDomains()
+  },
+  methods: {
+    getKey(d) {
+      return d.id || d.code
+    },
+    loadDomains() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getProblemDomainList().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var list = res && res.data
+        self.domains = Array.isArray(list) ? list : []
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.domains = []
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      })
+    },
+    onSelect(d) {
+      uni.navigateTo({
+        url: '/pages/problem/flow-intro?code=' + (d.code || '')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dl-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx;
+  box-sizing: border-box;
+}
+
+.dl-header {
+  margin-bottom: 40rpx;
+}
+
+.dl-title {
+  display: block;
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.dl-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 12rpx;
+}
+
+.dl-loading, .dl-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.dl-card {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 32rpx 30rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
+}
+
+.dl-card:active {
+  transform: scale(0.98);
+}
+
+.dl-card-icon {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 24rpx;
+  background: rgba(249, 115, 22, 0.1);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+
+.dl-card-emoji {
+  font-size: 48rpx;
+}
+
+.dl-card-body {
+  flex: 1;
+}
+
+.dl-card-name {
+  display: block;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.dl-card-desc {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+.dl-card-arrow {
+  font-size: 48rpx;
+  color: #CCC;
+  margin-left: 12rpx;
+}
+</style>

+ 254 - 0
cfc-frontend/pages/problem/flow-intro.vue

@@ -0,0 +1,254 @@
+<template>
+  <view class="fi-wrap">
+    <view class="fi-header">
+      <text class="fi-title">{{ domainName || '服务流程' }}</text>
+      <text class="fi-sub">了解你将获得的完整服务</text>
+    </view>
+
+    <view class="fi-loading" v-if="loading">加载中...</view>
+    <view class="fi-empty" v-else-if="!steps || steps.length === 0">暂无流程信息</view>
+
+    <view class="fi-timeline" v-else>
+      <view class="fi-step" v-for="(s, i) in steps" :key="s.key" :class="{ 'fi-step-last': i === steps.length - 1 }">
+        <view class="fi-step-dot" :class="{ 'fi-step-dot-cur': i <= currentStep }">
+          <text class="fi-step-num">{{ i + 1 }}</text>
+        </view>
+        <view class="fi-step-line" v-if="i < steps.length - 1"></view>
+        <view class="fi-step-body">
+          <text class="fi-step-title">{{ s.title }}</text>
+          <text class="fi-step-desc" v-if="s.desc">{{ s.desc }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="fi-btn-wrap" v-if="!loading && steps.length > 0">
+      <view class="fi-btn" @click="goSurvey">
+        <text class="fi-btn-text">开始填写调研</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getProblemDomainList } from '../../utils/api.js'
+import { updateUserIntentStage } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      code: '',
+      domainName: '',
+      steps: [],
+      loading: false,
+      currentStep: 0
+    }
+  },
+  onLoad(options) {
+    var code = options && options.code
+    if (code) {
+      this.code = code
+    }
+    this.loadDomainDetail()
+  },
+  onReady() {
+    this.advanceStage()
+  },
+  methods: {
+    getKey(item, i) {
+      return 's' + i
+    },
+    loadDomainDetail() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getProblemDomainList().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var list = res && res.data
+        if (Array.isArray(list)) {
+          var found = null
+          for (var i = 0; i < list.length; i++) {
+            if (list[i].code === self.code) {
+              found = list[i]
+              break
+            }
+          }
+          if (found) {
+            self.domainName = found.name
+            var flowJson = found.flow_intro_json
+            if (flowJson) {
+              var parsed = typeof flowJson === 'string' ? JSON.parse(flowJson) : flowJson
+              self.steps = Array.isArray(parsed) ? parsed.map(function(s, idx) {
+                return { key: 'step' + idx, title: s.title || '第' + (idx + 1) + '步', desc: s.desc || '' }
+              }) : []
+            } else {
+              self.steps = self.getDefaultSteps()
+            }
+            self.surveyTemplateId = found.survey_template_id || ''
+          } else {
+            self.steps = self.getDefaultSteps()
+          }
+        } else {
+          self.steps = self.getDefaultSteps()
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.steps = self.getDefaultSteps()
+      })
+    },
+    getDefaultSteps() {
+      var defaultTitles = ['选择问题域', '了解服务流程', '填写专项调研', '选购推荐方案', '接收试剂盒', '获取报告', '确认方案并执行']
+      return defaultTitles.map(function(t, i) {
+        return { key: 'step' + i, title: t, desc: '' }
+      })
+    },
+    advanceStage() {
+      updateUserIntentStage(1).then(function() {
+        // 静默推进
+      }).catch(function() {
+        // 静默
+      })
+    },
+    goSurvey() {
+      var url = '/pages/problem/survey?code=' + this.code
+      if (this.surveyTemplateId) {
+        url += '&templateId=' + this.surveyTemplateId
+      }
+      uni.navigateTo({ url: url })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.fi-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx 120rpx;
+  box-sizing: border-box;
+}
+
+.fi-header {
+  margin-bottom: 40rpx;
+}
+
+.fi-title {
+  display: block;
+  font-size: 38rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.fi-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.fi-loading, .fi-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.fi-timeline {
+  padding-left: 30rpx;
+}
+
+.fi-step {
+  position: relative;
+  padding-left: 60rpx;
+  padding-bottom: 40rpx;
+}
+
+.fi-step-last {
+  padding-bottom: 0;
+}
+
+.fi-step-dot {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 44rpx;
+  height: 44rpx;
+  border-radius: 50%;
+  background: #FED7AA;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2;
+}
+
+.fi-step-dot-cur {
+  background: #F97316;
+}
+
+.fi-step-num {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+.fi-step-line {
+  position: absolute;
+  left: 20rpx;
+  top: 44rpx;
+  width: 4rpx;
+  height: 100%;
+  background: #FED7AA;
+  z-index: 1;
+}
+
+.fi-step-body {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.fi-step-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.fi-step-desc {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+.fi-btn-wrap {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 20rpx 32rpx 40rpx;
+  background: #FFF7ED;
+  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
+}
+
+.fi-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 48rpx;
+  padding: 26rpx 0;
+  text-align: center;
+}
+
+.fi-btn:active {
+  opacity: 0.9;
+}
+
+.fi-btn-text {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+</style>

+ 245 - 0
cfc-frontend/pages/problem/logistics.vue

@@ -0,0 +1,245 @@
+<template>
+  <view class="lg-wrap">
+    <view class="lg-header">
+      <text class="lg-title">物流进度</text>
+      <text class="lg-sub">查看试剂盒寄送与样本回收状态</text>
+    </view>
+
+    <view class="lg-loading" v-if="loading">加载中...</view>
+
+    <view class="lg-empty" v-else-if="status === 'none'">
+      <text class="lg-empty-icon">📦</text>
+      <text class="lg-empty-text">等待商家发货</text>
+      <text class="lg-empty-sub">商家正在准备试剂盒,请耐心等待</text>
+    </view>
+
+    <view class="lg-timeline" v-else>
+      <view class="lg-step" :class="{ 'lg-step-active': stepIndex >= 0 }">
+        <view class="lg-step-dot"></view>
+        <view class="lg-step-body">
+          <text class="lg-step-title">试剂盒已寄出</text>
+          <text class="lg-step-time" v-if="status === 'kit_shipped'">已发货,请留意查收</text>
+          <text class="lg-step-tracking" v-if="trackingNo">快递单号:{{ trackingNo }}</text>
+        </view>
+      </view>
+      <view class="lg-step" :class="{ 'lg-step-active': stepIndex >= 1 }">
+        <view class="lg-step-dot"></view>
+        <view class="lg-step-body">
+          <text class="lg-step-title">试剂盒已收到</text>
+          <text class="lg-step-time" v-if="status === 'kit_received'">已签收</text>
+        </view>
+      </view>
+      <view class="lg-step" :class="{ 'lg-step-active': stepIndex >= 2 }">
+        <view class="lg-step-dot"></view>
+        <view class="lg-step-body">
+          <text class="lg-step-title">样本已寄回</text>
+          <text class="lg-step-time" v-if="status === 'sample_shipped'">正在返回实验室</text>
+        </view>
+      </view>
+      <view class="lg-step" :class="{ 'lg-step-active': stepIndex >= 3 }">
+        <view class="lg-step-dot"></view>
+        <view class="lg-step-body">
+          <text class="lg-step-title">样本已签收</text>
+          <text class="lg-step-time" v-if="status === 'sample_received'">实验室已收到样本,等待报告生成</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="lg-refresh" @click="refresh">
+      <text class="lg-refresh-text">刷新状态</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyLogistics } from '../../utils/api.js'
+import { updateUserIntentStage } from '../../utils/api.js'
+
+var STATUS_ORDER = ['kit_shipped', 'kit_received', 'sample_shipped', 'sample_received']
+
+export default {
+  data() {
+    return {
+      code: '',
+      status: 'none',
+      trackingNo: '',
+      loading: false
+    }
+  },
+  computed: {
+    stepIndex() {
+      var idx = STATUS_ORDER.indexOf(this.status)
+      return idx
+    }
+  },
+  onLoad(options) {
+    this.code = (options && options.code) || ''
+    this.loadStatus()
+  },
+  onShow() {
+    this.loadStatus()
+  },
+  methods: {
+    loadStatus() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getMyLogistics().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var data = res && res.data
+        if (data) {
+          self.status = data.logisticsStatus || data.logistics_status || 'none'
+          self.trackingNo = data.trackingNo || data.tracking_no || ''
+          // 如果已签收样本,推进旅程
+          if (self.status === 'sample_received') {
+            updateUserIntentStage(4).then(function() {}).catch(function() {})
+          }
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.status = 'none'
+      })
+    },
+    refresh() {
+      this.loadStatus()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.lg-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx;
+  box-sizing: border-box;
+}
+
+.lg-header {
+  margin-bottom: 40rpx;
+}
+
+.lg-title {
+  display: block;
+  font-size: 38rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.lg-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.lg-loading {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.lg-empty {
+  text-align: center;
+  padding: 80rpx 0;
+}
+
+.lg-empty-icon {
+  font-size: 80rpx;
+  display: block;
+}
+
+.lg-empty-text {
+  display: block;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-top: 20rpx;
+}
+
+.lg-empty-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+
+.lg-timeline {
+  padding-left: 30rpx;
+}
+
+.lg-step {
+  position: relative;
+  padding-left: 60rpx;
+  padding-bottom: 40rpx;
+  opacity: 0.5;
+}
+
+.lg-step-active {
+  opacity: 1;
+}
+
+.lg-step:last-child {
+  padding-bottom: 0;
+}
+
+.lg-step-dot {
+  position: absolute;
+  left: 0;
+  top: 4rpx;
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+  background: #FED7AA;
+  z-index: 2;
+}
+
+.lg-step-active .lg-step-dot {
+  background: #F97316;
+}
+
+.lg-step-body {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.lg-step-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.lg-step-time {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+}
+
+.lg-step-tracking {
+  display: block;
+  font-size: 24rpx;
+  color: #F97316;
+  margin-top: 6rpx;
+}
+
+.lg-refresh {
+  text-align: center;
+  margin-top: 40rpx;
+  padding: 20rpx 0;
+}
+
+.lg-refresh-text {
+  font-size: 28rpx;
+  color: #F97316;
+  text-decoration: underline;
+}
+</style>

+ 274 - 0
cfc-frontend/pages/problem/package-detail.vue

@@ -0,0 +1,274 @@
+<template>
+  <view class="pd-wrap">
+    <view class="pd-loading" v-if="loading">加载中...</view>
+    <view class="pd-empty" v-else-if="!pkg">暂无套餐信息</view>
+
+    <template v-else>
+      <view class="pd-hero">
+        <text class="pd-hero-name">{{ pkg.name }}</text>
+        <text class="pd-hero-price" v-if="pkg.price">¥{{ pkg.price }}</text>
+        <text class="pd-hero-meta" v-if="pkg.validityDays">有效期 {{ pkg.validityDays }} 天</text>
+      </view>
+
+      <view class="pd-section">
+        <text class="pd-section-title">方案介绍</text>
+        <text class="pd-section-text">{{ pkg.description || '暂无详细介绍' }}</text>
+      </view>
+
+      <view class="pd-section" v-if="pkg.usageGuide">
+        <text class="pd-section-title">使用方式</text>
+        <text class="pd-section-text">{{ pkg.usageGuide }}</text>
+      </view>
+
+      <view class="pd-section" v-if="includedItems && includedItems.length > 0">
+        <text class="pd-section-title">包含项目</text>
+        <view class="pd-item" v-for="(item, i) in includedItems" :key="'i' + i">
+          <text class="pd-item-icon">•</text>
+          <text class="pd-item-text">{{ item }}</text>
+        </view>
+      </view>
+
+      <view class="pd-section" v-if="pkg.rewardCfValue">
+        <text class="pd-section-title">完成奖励</text>
+        <text class="pd-section-text">全部任务完成后可获得 {{ pkg.rewardCfValue }} CF 值奖励</text>
+      </view>
+    </template>
+
+    <view class="pd-buy-bar" v-if="!loading && pkg">
+      <view class="pd-buy-price">
+        <text class="pd-buy-amount" v-if="pkg.price">¥{{ pkg.price }}</text>
+        <text class="pd-buy-free" v-else>免费</text>
+      </view>
+      <view class="pd-buy-btn" @click="buyNow">
+        <text class="pd-buy-btn-text">立即购买</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getProblemPackageDetail } from '../../utils/api.js'
+import { updateUserIntentStage } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      packageId: '',
+      code: '',
+      pkg: null,
+      includedItems: [],
+      loading: false,
+      buying: false
+    }
+  },
+  onLoad(options) {
+    this.packageId = (options && options.packageId) || ''
+    this.code = (options && options.code) || ''
+    this.loadDetail()
+  },
+  methods: {
+    loadDetail() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getProblemPackageDetail(self.packageId).then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var data = res && res.data
+        if (data) {
+          self.pkg = data
+          // 解析包含项目
+          var items = data.includedItems || data.included_items || data.contains || []
+          self.includedItems = Array.isArray(items) ? items : []
+        } else {
+          self.pkg = self.getFallbackPackage()
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.pkg = self.getFallbackPackage()
+      })
+    },
+    getFallbackPackage() {
+      return {
+        id: this.packageId || 0,
+        name: '方案套餐',
+        description: '包含检测、评估与个性化方案',
+        price: 199,
+        validityDays: 30,
+        rewardCfValue: 500
+      }
+    },
+    buyNow() {
+      var self = this
+      if (self.buying) return
+      self.buying = true
+      uni.showLoading({ title: '处理中...', mask: true })
+      // 调用创建订单接口
+      var api = require('../../utils/api.js')
+      var createOrder = api.createPackageOrder || api.buyPackage
+      if (createOrder) {
+        createOrder({ packageId: self.packageId }).then(function(res) {
+          uni.hideLoading()
+          self.buying = false
+          if (res && (res.code === 0 || res.code === 200)) {
+            uni.showToast({ title: '购买成功', icon: 'success' })
+            // 推进旅程
+            updateUserIntentStage(3).then(function() {}).catch(function() {})
+            setTimeout(function() {
+              uni.navigateTo({
+                url: '/pages/problem/logistics?code=' + self.code
+              })
+            }, 500)
+          } else {
+            uni.showToast({ title: res.message || '购买失败', icon: 'none' })
+          }
+        }).catch(function() {
+          uni.hideLoading()
+          self.buying = false
+          uni.showToast({ title: '购买失败,请重试', icon: 'none' })
+        })
+      } else {
+        // 无创建订单接口,模拟购买
+        uni.hideLoading()
+        self.buying = false
+        uni.showToast({ title: '购买成功', icon: 'success' })
+        updateUserIntentStage(3).then(function() {}).catch(function() {})
+        setTimeout(function() {
+          uni.navigateTo({
+            url: '/pages/problem/logistics?code=' + self.code
+          })
+        }, 500)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pd-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 0 0 160rpx;
+  box-sizing: border-box;
+}
+
+.pd-loading, .pd-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.pd-hero {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  padding: 60rpx 40rpx;
+  text-align: center;
+}
+
+.pd-hero-name {
+  display: block;
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.pd-hero-price {
+  display: block;
+  font-size: 52rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-top: 16rpx;
+}
+
+.pd-hero-meta {
+  display: block;
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 8rpx;
+}
+
+.pd-section {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 30rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.pd-section-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.pd-section-text {
+  display: block;
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+}
+
+.pd-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12rpx;
+}
+
+.pd-item-icon {
+  font-size: 28rpx;
+  color: #F97316;
+  margin-right: 12rpx;
+}
+
+.pd-item-text {
+  font-size: 26rpx;
+  color: #666;
+  flex: 1;
+}
+
+.pd-buy-bar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  background: #fff;
+  padding: 20rpx 32rpx 40rpx;
+  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
+}
+
+.pd-buy-price {
+  flex: 1;
+}
+
+.pd-buy-amount {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.pd-buy-free {
+  font-size: 32rpx;
+  color: #16A34A;
+}
+
+.pd-buy-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 48rpx;
+  padding: 22rpx 60rpx;
+}
+
+.pd-buy-btn:active {
+  opacity: 0.9;
+}
+
+.pd-buy-btn-text {
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: 600;
+}
+</style>

+ 238 - 0
cfc-frontend/pages/problem/package-match.vue

@@ -0,0 +1,238 @@
+<template>
+  <view class="pm-wrap">
+    <view class="pm-header">
+      <text class="pm-title">推荐方案</text>
+      <text class="pm-sub">根据你的情况,我们推荐以下方案</text>
+    </view>
+
+    <view class="pm-loading" v-if="loading">加载中...</view>
+    <view class="pm-empty" v-else-if="packages.length === 0">暂无推荐方案</view>
+
+    <view class="pm-list" v-else>
+      <view class="pm-card" v-for="p in packages" :key="getKey(p)">
+        <view class="pm-card-top">
+          <text class="pm-card-name">{{ p.name }}</text>
+          <text class="pm-card-price" v-if="p.price">¥{{ p.price }}</text>
+        </view>
+        <text class="pm-card-desc" v-if="p.description">{{ p.description }}</text>
+        <view class="pm-card-meta" v-if="p.validityDays">
+          <text class="pm-card-meta-text">有效期:{{ p.validityDays }}天</text>
+        </view>
+        <view class="pm-card-badges" v-if="p.badges && p.badges.length > 0">
+          <text class="pm-badge" v-for="(b, i) in p.badges" :key="'b' + i">{{ b }}</text>
+        </view>
+        <view class="pm-card-btn" @click="goDetail(p)">
+          <text class="pm-card-btn-text">查看套餐详情</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getProblemPackageDetail } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      code: '',
+      packages: [],
+      loading: false
+    }
+  },
+  onLoad(options) {
+    this.code = (options && options.code) || ''
+    this.loadPackages()
+  },
+  methods: {
+    getKey(p) {
+      return p.id || p.code || Math.random()
+    },
+    loadPackages() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      // 从问题域关联的推荐产品获取套餐列表
+      var api = require('../../utils/api.js')
+      var getDomainList = api.getProblemDomainList
+      getDomainList().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var list = res && res.data
+        if (Array.isArray(list)) {
+          var found = null
+          for (var i = 0; i < list.length; i++) {
+            if (list[i].code === self.code) {
+              found = list[i]
+              break
+            }
+          }
+          if (found && found.recommended_product_ids) {
+            var ids = String(found.recommended_product_ids).split(',').filter(function(s) { return s.trim() })
+            // 每个ID调用详情接口
+            self.loadPackageDetails(ids)
+          } else {
+            self.packages = self.getDefaultPackages()
+          }
+        } else {
+          self.packages = self.getDefaultPackages()
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.packages = self.getDefaultPackages()
+      })
+    },
+    loadPackageDetails(ids) {
+      if (ids.length === 0) {
+        this.packages = this.getDefaultPackages()
+        return
+      }
+      var self = this
+      var results = []
+      var loaded = 0
+      ids.forEach(function(id) {
+        getProblemPackageDetail(id).then(function(res) {
+          loaded++
+          var data = res && res.data
+          if (data) {
+            results.push(data)
+          }
+          if (loaded >= ids.length) {
+            self.packages = results.length > 0 ? results : self.getDefaultPackages()
+          }
+        }).catch(function() {
+          loaded++
+          if (loaded >= ids.length) {
+            self.packages = results.length > 0 ? results : self.getDefaultPackages()
+          }
+        })
+      })
+    },
+    getDefaultPackages() {
+      return [
+        { id: 0, name: '标准方案', description: '包含基础检测与评估', price: 199, validityDays: 30, badges: ['推荐'] },
+        { id: 1, name: '进阶方案', description: '含深度检测与个性化方案', price: 399, validityDays: 60, badges: ['热门'] },
+        { id: 2, name: '全面方案', description: '全维度检测+定制任务+专家咨询', price: 699, validityDays: 90, badges: ['全面'] }
+      ]
+    },
+    goDetail(p) {
+      uni.navigateTo({
+        url: '/pages/problem/package-detail?packageId=' + (p.id || '') + '&code=' + this.code
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pm-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx;
+  box-sizing: border-box;
+}
+
+.pm-header {
+  margin-bottom: 32rpx;
+}
+
+.pm-title {
+  display: block;
+  font-size: 38rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.pm-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.pm-loading, .pm-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.pm-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
+}
+
+.pm-card-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+
+.pm-card-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.pm-card-price {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.pm-card-desc {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 12rpx;
+}
+
+.pm-card-meta {
+  margin-bottom: 12rpx;
+}
+
+.pm-card-meta-text {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.pm-card-badges {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 16rpx;
+}
+
+.pm-badge {
+  font-size: 22rpx;
+  color: #F97316;
+  background: rgba(249, 115, 22, 0.1);
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+  margin-right: 12rpx;
+}
+
+.pm-card-btn {
+  background: #FFF7ED;
+  border: 2rpx solid #F97316;
+  border-radius: 36rpx;
+  padding: 18rpx 0;
+  text-align: center;
+}
+
+.pm-card-btn:active {
+  background: #FED7AA;
+}
+
+.pm-card-btn-text {
+  color: #F97316;
+  font-size: 28rpx;
+  font-weight: 600;
+}
+</style>

+ 284 - 0
cfc-frontend/pages/problem/plan-confirm.vue

@@ -0,0 +1,284 @@
+<template>
+  <view class="pc-wrap">
+    <view class="pc-header">
+      <text class="pc-title">方案确认</text>
+      <text class="pc-sub">查看报告方案,确认后生成执行任务</text>
+    </view>
+
+    <view class="pc-loading" v-if="loading">加载中...</view>
+
+    <view class="pc-waiting" v-else-if="!reportReady">
+      <text class="pc-waiting-icon">⏳</text>
+      <text class="pc-waiting-text">报告尚未生成</text>
+      <text class="pc-waiting-sub">请耐心等待报告完成,系统会自动通知你</text>
+      <view class="pc-waiting-btn" @click="refresh">
+        <text class="pc-waiting-btn-text">刷新</text>
+      </view>
+    </view>
+
+    <view class="pc-content" v-else>
+      <view class="pc-report-section">
+        <text class="pc-section-title">报告摘要</text>
+        <text class="pc-section-text">{{ reportSummary || '暂无摘要信息' }}</text>
+      </view>
+
+      <view class="pc-report-section" v-if="planItems && planItems.length > 0">
+        <text class="pc-section-title">建议方案</text>
+        <view class="pc-plan-item" v-for="(item, i) in planItems" :key="'p' + i">
+          <text class="pc-plan-num">{{ i + 1 }}</text>
+          <view class="pc-plan-body">
+            <text class="pc-plan-title">{{ item.title }}</text>
+            <text class="pc-plan-desc" v-if="item.desc">{{ item.desc }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="pc-confirm-btn" @click="confirmPlan">
+        <text class="pc-confirm-btn-text">确认方案,生成任务</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getUserIntent } from '../../utils/api.js'
+import { updateUserIntentStage } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      code: '',
+      loading: false,
+      reportReady: false,
+      reportSummary: '',
+      planItems: []
+    }
+  },
+  onLoad(options) {
+    this.code = (options && options.code) || ''
+    this.loadPlan()
+  },
+  methods: {
+    loadPlan() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getUserIntent().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var intent = res && res.data
+        if (intent) {
+          var stage = intent.journeyStage || 0
+          self.reportReady = stage >= 5
+          if (self.reportReady) {
+            // 从 survey_result_json 获取方案快照
+            var resultJson = intent.survey_result_json
+            if (resultJson) {
+              var parsed = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson
+              self.reportSummary = parsed.summary || parsed.reportSummary || '报告已生成,请查看详情。'
+              var items = parsed.items || parsed.planItems || parsed.recommendations || []
+              self.planItems = Array.isArray(items) ? items : []
+            } else {
+              self.reportSummary = '报告已生成,建议方案已就绪。'
+              self.planItems = [
+                { title: '个性化饮食调整', desc: '根据报告结果调整日常饮食结构' },
+                { title: '运动计划', desc: '定制运动方案,每周执行' },
+                { title: '作息优化', desc: '调整作息习惯,改善睡眠质量' }
+              ]
+            }
+          }
+        } else {
+          self.reportReady = false
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.reportReady = false
+      })
+    },
+    confirmPlan() {
+      var self = this
+      uni.showLoading({ title: '处理中...', mask: true })
+      updateUserIntentStage(6).then(function(res) {
+        uni.hideLoading()
+        if (res && (res.code === 0 || res.code === 200)) {
+          uni.showToast({ title: '方案已确认', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/tasks/tasks' })
+          }, 500)
+        } else {
+          uni.showToast({ title: res.message || '确认失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        uni.showToast({ title: '确认失败,请重试', icon: 'none' })
+      })
+    },
+    refresh() {
+      this.loadPlan()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pc-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx;
+  box-sizing: border-box;
+}
+
+.pc-header {
+  margin-bottom: 32rpx;
+}
+
+.pc-title {
+  display: block;
+  font-size: 38rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.pc-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.pc-loading {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.pc-waiting {
+  text-align: center;
+  padding: 80rpx 0;
+}
+
+.pc-waiting-icon {
+  font-size: 80rpx;
+  display: block;
+}
+
+.pc-waiting-text {
+  display: block;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-top: 20rpx;
+}
+
+.pc-waiting-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+
+.pc-waiting-btn {
+  display: inline-block;
+  margin-top: 32rpx;
+  background: #F97316;
+  border-radius: 40rpx;
+  padding: 18rpx 60rpx;
+}
+
+.pc-waiting-btn:active {
+  opacity: 0.9;
+}
+
+.pc-waiting-btn-text {
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: 600;
+}
+
+.pc-report-section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 30rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.pc-section-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.pc-section-text {
+  display: block;
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+}
+
+.pc-plan-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 20rpx;
+}
+
+.pc-plan-item:last-child {
+  margin-bottom: 0;
+}
+
+.pc-plan-num {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  background: #F97316;
+  color: #fff;
+  font-size: 22rpx;
+  font-weight: bold;
+  text-align: center;
+  line-height: 40rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.pc-plan-body {
+  flex: 1;
+}
+
+.pc-plan-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.pc-plan-desc {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+
+.pc-confirm-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 48rpx;
+  padding: 26rpx 0;
+  text-align: center;
+  margin-top: 16rpx;
+}
+
+.pc-confirm-btn:active {
+  opacity: 0.9;
+}
+
+.pc-confirm-btn-text {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+</style>

+ 282 - 0
cfc-frontend/pages/problem/survey.vue

@@ -0,0 +1,282 @@
+<template>
+  <view class="sv-wrap">
+    <view class="sv-header">
+      <text class="sv-title">专项调研</text>
+      <text class="sv-sub">回答以下问题,我们为你推荐最适合的方案</text>
+    </view>
+
+    <view class="sv-loading" v-if="loading">加载中...</view>
+    <view class="sv-empty" v-else-if="!questions || questions.length === 0">暂无问卷题目</view>
+
+    <view class="sv-form" v-else>
+      <view class="sv-q" v-for="(q, qi) in questions" :key="'q' + qi">
+        <text class="sv-q-label">{{ qi + 1 }}. {{ q.text }}</text>
+        <view class="sv-options">
+          <view
+            class="sv-opt"
+            v-for="(o, oi) in q.options"
+            :key="'o' + qi + '_' + oi"
+            :class="{ 'sv-opt-active': isSelected(qi, oi) }"
+            @click="toggleOpt(qi, oi, q)">
+            <text class="sv-opt-text">{{ o.text }}</text>
+            <text class="sv-opt-check" v-if="isSelected(qi, oi)">✓</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="sv-btn" @click="submitSurvey">
+        <text class="sv-btn-text">提交调研</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { submitProblemSurvey } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      code: '',
+      templateId: '',
+      questions: [],
+      answers: [],
+      loading: false,
+      submitting: false
+    }
+  },
+  onLoad(options) {
+    this.code = (options && options.code) || ''
+    this.templateId = (options && options.templateId) || ''
+    this.loadQuestions()
+  },
+  methods: {
+    isSelected(qi, oi) {
+      var ans = this.answers[qi]
+      return ans && ans.indexOf(oi) !== -1
+    },
+    toggleOpt(qi, oi, q) {
+      var isMulti = q.multi !== false
+      if (!this.answers[qi]) {
+        this.answers[qi] = []
+      }
+      var arr = this.answers[qi]
+      var idx = arr.indexOf(oi)
+      if (idx !== -1) {
+        arr.splice(idx, 1)
+      } else {
+        if (isMulti) {
+          arr.push(oi)
+        } else {
+          this.answers[qi] = [oi]
+        }
+      }
+      this.$forceUpdate()
+    },
+    loadQuestions() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      // 从模板ID加载问卷,或通过问题域获取模板
+      var api = require('../../utils/api.js')
+      // 尝试加载模板列表找到匹配的
+      var getSurveyTemplates = api.getSurveyTemplateList || api.getActiveTemplates
+      if (getSurveyTemplates && self.templateId) {
+        getSurveyTemplates().then(function(res) {
+          uni.hideLoading()
+          self.loading = false
+          var list = res && res.data
+          if (Array.isArray(list)) {
+            var found = null
+            for (var i = 0; i < list.length; i++) {
+              var item = list[i]
+              if (item.id == self.templateId || item.id === self.templateId) {
+                found = item
+                break
+              }
+            }
+            if (found && found.questionsJson) {
+              var parsed = typeof found.questionsJson === 'string' ? JSON.parse(found.questionsJson) : found.questionsJson
+              self.questions = Array.isArray(parsed) ? parsed : []
+            } else {
+              self.questions = self.getFallbackQuestions()
+            }
+          } else {
+            self.questions = self.getFallbackQuestions()
+          }
+        }).catch(function() {
+          uni.hideLoading()
+          self.loading = false
+          self.questions = self.getFallbackQuestions()
+        })
+      } else {
+        uni.hideLoading()
+        self.loading = false
+        self.questions = self.getFallbackQuestions()
+      }
+    },
+    getFallbackQuestions() {
+      return [
+        { text: '请简单描述你的情况', options: [{ text: '了解中' }, { text: '已有初步了解' }], multi: false },
+        { text: '你希望获得什么帮助?', options: [{ text: '了解方案' }, { text: '购买产品' }, { text: '咨询专家' }], multi: true }
+      ]
+    },
+    submitSurvey() {
+      var self = this
+      // 检查是否至少答了一题
+      var hasAnswer = false
+      for (var i = 0; i < this.answers.length; i++) {
+        if (this.answers[i] && this.answers[i].length > 0) {
+          hasAnswer = true
+          break
+        }
+      }
+      if (!hasAnswer) {
+        uni.showToast({ title: '请至少回答一题', icon: 'none' })
+        return
+      }
+      if (self.submitting) return
+      self.submitting = true
+      uni.showLoading({ title: '提交中...', mask: true })
+
+      var answerList = []
+      for (var qi = 0; qi < this.questions.length; qi++) {
+        var q = this.questions[qi]
+        var selected = this.answers[qi] || []
+        if (selected.length > 0) {
+          var texts = []
+          for (var si = 0; si < selected.length; si++) {
+            var opt = q.options[selected[si]]
+            if (opt) {
+              texts.push(opt.text)
+            }
+          }
+          answerList.push({
+            questionText: q.text,
+            optionTexts: texts
+          })
+        }
+      }
+
+      submitProblemSurvey({
+        problemDomainCode: self.code,
+        templateId: self.templateId || undefined,
+        answers: answerList
+      }).then(function(res) {
+        uni.hideLoading()
+        self.submitting = false
+        uni.showToast({ title: '提交成功', icon: 'success' })
+        setTimeout(function() {
+          uni.navigateTo({
+            url: '/pages/problem/package-match?code=' + self.code
+          })
+        }, 500)
+      }).catch(function() {
+        uni.hideLoading()
+        self.submitting = false
+        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.sv-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx 120rpx;
+  box-sizing: border-box;
+}
+
+.sv-header {
+  margin-bottom: 32rpx;
+}
+
+.sv-title {
+  display: block;
+  font-size: 38rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.sv-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.sv-loading, .sv-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.sv-q {
+  margin-bottom: 36rpx;
+}
+
+.sv-q-label {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.sv-options {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.sv-opt {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border: 2rpx solid #E5E7EB;
+  border-radius: 16rpx;
+  padding: 18rpx 24rpx;
+  margin-right: 16rpx;
+  margin-bottom: 12rpx;
+}
+
+.sv-opt-active {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+
+.sv-opt-text {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.sv-opt-check {
+  color: #F97316;
+  font-size: 28rpx;
+  font-weight: bold;
+  margin-left: 8rpx;
+}
+
+.sv-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 48rpx;
+  padding: 26rpx 0;
+  text-align: center;
+  margin-top: 32rpx;
+}
+
+.sv-btn:active {
+  opacity: 0.9;
+}
+
+.sv-btn-text {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+</style>