Просмотр исходного кода

feat(frontend): 改版v1 B类轻问卷(F16) + 首页定向内容(F17)

quick-survey.vue: B类5-8题轻问卷,getActiveTemplates加载+submitProblemSurvey提交(answers map格式兼容resultTags聚合)。pages.json注册quick-survey。index-home: B类导航改跳轻问卷;已有调研结果的用户按resultTags经getContentByTags加载定向科普内容模块。

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 месяц назад
Родитель
Сommit
3ed92da878

+ 6 - 0
cfc-frontend/pages.json

@@ -1597,6 +1597,12 @@
             "navigationBarTitleText": "专项调研"
           }
         },
+        {
+          "path": "quick-survey",
+          "style": {
+            "navigationBarTitleText": "快速了解你"
+          }
+        },
         {
           "path": "package-match",
           "style": {

+ 92 - 4
cfc-frontend/pages/index-home/index.vue

@@ -63,6 +63,20 @@
       :intent="userIntent"
       @action="handleJourneyAction" />
 
+    <!-- 定向科普内容 — F17 B类问卷结果定向展示 -->
+    <view v-if="targetedArticles.length > 0" class="section">
+      <view class="section-header">
+        <text class="section-title">📚 为你推荐</text>
+      </view>
+      <view class="targeted-list">
+        <view class="targeted-card" v-for="article in targetedArticles" :key="article.id" @click="goArticleDetail(article.id)">
+          <text class="targeted-category">{{ article.categoryName || article.category || '推荐' }}</text>
+          <text class="targeted-title">{{ article.title }}</text>
+          <text class="targeted-summary">{{ article.summary || '' }}</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 能量沙盘 — Canvas 五行星图 -->
     <WuxingSandbox
       v-if="sandboxData"
@@ -345,7 +359,7 @@ import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import IntentPicker from '../../components/intent-picker.vue'
 import JourneyCard from '../../components/journey-card.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getUserIntent, chooseUserIntent } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getUserIntent, chooseUserIntent, getContentByTags } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 
@@ -424,7 +438,8 @@ export default {
       // 改版v1 用户意图/旅程
       userIntent: null,
       showIntentPicker: false,
-      intentPickerDismissed: false
+      intentPickerDismissed: false,
+      targetedArticles: []
     }
   },
   computed: {
@@ -1031,6 +1046,8 @@ export default {
         var intent = res && res.data
         if (intent) {
           self.userIntent = intent
+          // F17: 已有调研结果的用户加载定向科普内容
+          self.loadTargetedContent(intent)
         } else {
           // 无意图且未跳过,弹选择层
           if (!self.intentPickerDismissed) {
@@ -1042,6 +1059,32 @@ export default {
         // 静默:接口异常时不弹
       })
     },
+    // F17: 按调研 resultTags 定向加载科普内容
+    loadTargetedContent: function(intent) {
+      var self = this
+      var tags = []
+      var resultJson = intent && intent.survey_result_json
+      if (resultJson) {
+        try {
+          var parsed = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson
+          if (parsed && parsed.resultTags && Array.isArray(parsed.resultTags)) {
+            tags = parsed.resultTags
+          }
+        } catch (e) {
+          tags = []
+        }
+      }
+      // B类无 tags 时用问题域 contentTags 或空数组
+      if (tags.length === 0 && intent && intent.intentType === 'C' && intent.problemDomainCode) {
+        tags = [intent.problemDomainCode]
+      }
+      getContentByTags({ tags: tags }).then(function(res) {
+        var list = res && res.data
+        self.targetedArticles = Array.isArray(list) ? list : []
+      }).catch(function() {
+        self.targetedArticles = []
+      })
+    },
     handleIntentSelect: function(payload) {
       var self = this
       self.showIntentPicker = false
@@ -1061,7 +1104,7 @@ export default {
           if (payload.intentType === 'A') {
             uni.navigateTo({ url: '/pages/assessment/purchase' })
           } else if (payload.intentType === 'B') {
-            uni.navigateTo({ url: '/pages/problem/domain-list' })
+            uni.navigateTo({ url: '/pages/problem/quick-survey' })
           } else if (payload.intentType === 'C') {
             uni.navigateTo({
               url: '/pages/problem/flow-intro?code=' + (payload.problemDomainCode || '')
@@ -1085,7 +1128,12 @@ export default {
       if (action === 'flow-intro') {
         uni.navigateTo({ url: '/pages/problem/flow-intro?code=' + (code || '') })
       } else if (action === 'survey') {
-        uni.navigateTo({ url: '/pages/problem/survey?code=' + (code || '') })
+        // B类轻问卷 / C类专项调研
+        if (code) {
+          uni.navigateTo({ url: '/pages/problem/survey?code=' + code })
+        } else {
+          uni.navigateTo({ url: '/pages/problem/quick-survey' })
+        }
       } else if (action === 'package') {
         uni.navigateTo({ url: '/pages/problem/package-match?code=' + (code || '') })
       } else if (action === 'logistics') {
@@ -1335,6 +1383,46 @@ export default {
   font-size: 24rpx;
   color: #5B9BD5;
 }
+.targeted-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.targeted-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.targeted-card:active {
+  opacity: 0.9;
+}
+.targeted-category {
+  display: inline-block;
+  font-size: 20rpx;
+  color: #F97316;
+  background: rgba(249, 115, 22, 0.1);
+  padding: 2rpx 14rpx;
+  border-radius: 16rpx;
+  margin-bottom: 10rpx;
+}
+.targeted-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.targeted-summary {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+}
 .empty-state {
   display: flex;
   justify-content: center;

+ 302 - 0
cfc-frontend/pages/problem/quick-survey.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="qs-wrap">
+    <view class="qs-header">
+      <text class="qs-title">快速了解你</text>
+      <text class="qs-sub">答几道小题,为你推荐最合适的内容和方案</text>
+    </view>
+
+    <view class="qs-loading" v-if="loading">加载中...</view>
+    <view class="qs-empty" v-else-if="!questions || questions.length === 0">暂无可用问卷</view>
+
+    <view class="qs-body" v-else>
+      <!-- 模板选择(多模板时可切换) -->
+      <view class="qs-template-row" v-if="templates.length > 1">
+        <text class="qs-template-label">选择话题:</text>
+        <view class="qs-template-chip" v-for="(t, i) in templates" :key="'t' + i"
+          :class="{ 'qs-template-chip-active': selectedTemplateId === t.id }"
+          @click="switchTemplate(t)">
+          <text>{{ t.title }}</text>
+        </view>
+      </view>
+
+      <view class="qs-q" v-for="(q, qi) in questions" :key="'q' + qi">
+        <text class="qs-q-label">{{ qi + 1 }}. {{ q.text }}</text>
+        <view class="qs-options">
+          <view class="qs-opt" v-for="(o, oi) in q.options" :key="'o' + qi + '_' + oi"
+            :class="{ 'qs-opt-active': selectedIdx(qi) === oi }"
+            @click="select(qi, oi)">
+            <text class="qs-opt-text">{{ o.text }}</text>
+            <text class="qs-opt-check" v-if="selectedIdx(qi) === oi">✓</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="qs-btn" @click="submit" v-if="!submitting">
+        <text class="qs-btn-text">提交,查看推荐</text>
+      </view>
+      <view class="qs-btn qs-btn-disabled" v-else>
+        <text class="qs-btn-text">提交中...</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getActiveTemplates } from '../../utils/api.js'
+import { submitProblemSurvey } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      templates: [],
+      selectedTemplateId: null,
+      templateTitle: '',
+      questions: [],
+      answers: [],
+      loading: false,
+      submitting: false
+    }
+  },
+  onLoad() {
+    this.loadTemplates()
+  },
+  methods: {
+    selectedIdx(qi) {
+      var arr = this.answers[qi]
+      return (arr && arr.length > 0) ? arr[0] : -1
+    },
+    select(qi, oi) {
+      this.answers[qi] = [oi]
+      this.$forceUpdate()
+    },
+    loadTemplates() {
+      var self = this
+      self.loading = true
+      uni.showLoading({ title: '加载中...', mask: true })
+      getActiveTemplates().then(function(res) {
+        uni.hideLoading()
+        self.loading = false
+        var list = res && res.data
+        var arr = Array.isArray(list) ? list : []
+        self.templates = arr
+        if (arr.length > 0) {
+          self.applyTemplate(arr[0])
+        } else {
+          self.questions = []
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.loading = false
+        self.templates = []
+        self.questions = []
+      })
+    },
+    applyTemplate(t) {
+      this.selectedTemplateId = t.id
+      this.templateTitle = t.title || ''
+      var parsed = []
+      if (t.questionsJson) {
+        try {
+          var p = typeof t.questionsJson === 'string' ? JSON.parse(t.questionsJson) : t.questionsJson
+          parsed = Array.isArray(p) ? p : []
+        } catch (e) {
+          parsed = []
+        }
+      }
+      this.questions = parsed
+      this.answers = []
+    },
+    switchTemplate(t) {
+      this.applyTemplate(t)
+    },
+    submit() {
+      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
+      }
+      self.submitting = true
+      uni.showLoading({ title: '提交中...', mask: true })
+
+      var answers = {}
+      for (var qi = 0; qi < this.questions.length; qi++) {
+        var selected = this.answers[qi] || []
+        if (selected.length === 0) continue
+        var q = this.questions[qi]
+        var opt = q.options && q.options[selected[0]]
+        if (opt && opt.id) {
+          answers[String(opt.id)] = opt.id
+        } else if (opt && opt.value) {
+          answers[String(opt.value)] = opt.value
+        } else {
+          answers['q' + qi] = opt ? opt.text : ''
+        }
+      }
+
+      submitProblemSurvey({
+        problemDomainCode: '',
+        surveyTemplateId: this.selectedTemplateId,
+        answers: answers
+      }).then(function(res) {
+        uni.hideLoading()
+        self.submitting = false
+        if (res && (res.code === 0 || res.code === 200)) {
+          uni.showToast({ title: '提交成功', icon: 'success' })
+          setTimeout(function() {
+            // 跳回首页展示定向内容
+            uni.switchTab({ url: '/pages/index-home/index' })
+          }, 800)
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.submitting = false
+        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.qs-wrap {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding: 40rpx 32rpx 120rpx;
+  box-sizing: border-box;
+}
+
+.qs-header {
+  margin-bottom: 32rpx;
+}
+
+.qs-title {
+  display: block;
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+}
+
+.qs-sub {
+  display: block;
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-top: 10rpx;
+}
+
+.qs-loading, .qs-empty {
+  text-align: center;
+  font-size: 28rpx;
+  color: #999;
+  padding: 80rpx 0;
+}
+
+.qs-template-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 16rpx 20rpx;
+  margin-bottom: 24rpx;
+}
+
+.qs-template-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-right: 8rpx;
+}
+
+.qs-template-chip {
+  font-size: 24rpx;
+  color: #666;
+  background: #F3F4F6;
+  border-radius: 24rpx;
+  padding: 6rpx 20rpx;
+  margin: 4rpx;
+}
+
+.qs-template-chip-active {
+  background: #F97316;
+  color: #fff;
+}
+
+.qs-q {
+  margin-bottom: 32rpx;
+}
+
+.qs-q-label {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.qs-options {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.qs-opt {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border: 2rpx solid #E5E7EB;
+  border-radius: 16rpx;
+  padding: 18rpx 24rpx;
+  margin-right: 16rpx;
+  margin-bottom: 12rpx;
+}
+
+.qs-opt-active {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+
+.qs-opt-text {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.qs-opt-check {
+  color: #F97316;
+  font-size: 28rpx;
+  font-weight: bold;
+  margin-left: 8rpx;
+}
+
+.qs-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 48rpx;
+  padding: 26rpx 0;
+  text-align: center;
+  margin-top: 32rpx;
+}
+
+.qs-btn:active {
+  opacity: 0.9;
+}
+
+.qs-btn-disabled {
+  opacity: 0.7;
+}
+
+.qs-btn-text {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+</style>