| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- <template>
- <view class="page">
- <view class="nav-bar">
- <view class="nav-back" @tap="goBack">
- <text class="back-text">‹ 返回</text>
- </view>
- <text class="nav-title">肠道菌群报告</text>
- </view>
- <scroll-view class="content" scroll-y>
- <!-- 报告头部 -->
- <view class="report-header">
- <text class="report-title">肠道菌群检测报告</text>
- <view class="report-meta">
- <text class="meta-item" v-if="report.personName">{{ report.personName }}</text>
- <text class="meta-item" v-if="report.gender">{{ report.gender }}</text>
- <text class="meta-item" v-if="report.age">{{ report.age }}岁</text>
- </view>
- <view class="report-meta">
- <text class="meta-item" v-if="report.reportDate">日期: {{ report.reportDate }}</text>
- <text class="meta-item" v-if="report.reportNumber">编号: {{ report.reportNumber }}</text>
- </view>
- <view class="badge-row" v-if="report.gutAge || report.gutType">
- <text class="badge" v-if="report.gutAge">肠龄: {{ report.gutAge }}</text>
- <text class="badge" v-if="report.gutType">肠型: {{ report.gutType }}</text>
- </view>
- <!-- 报告解读 -->
- <view class="interpretation-box" v-if="reportInterpretation && reportInterpretation.gutAgeAssessment">
- <text class="interp-text">{{ reportInterpretation.gutAgeAssessment }}</text>
- </view>
- </view>
- <!-- 评分圆环 -->
- <scroll-view class="scores-scroll" scroll-x>
- <view class="score-item" v-for="(score, idx) in scoreItems" :key="idx">
- <view class="score-ring" :style="'border-color: ' + score.color + '; color: ' + score.color">
- <text class="score-val">{{ score.value }}</text>
- </view>
- <text class="score-label">{{ score.label }}</text>
- </view>
- </scroll-view>
- <!-- 快速跳转 -->
- <view class="quick-nav">
- <view class="nav-card" @tap="goToSpecies">
- <text class="nav-icon">🦠</text>
- <text class="nav-label">菌群详情</text>
- <text class="nav-count">{{ floraCount }}种</text>
- </view>
- <view class="nav-card" @tap="goToPathogen">
- <text class="nav-icon">🔬</text>
- <text class="nav-label">病原菌</text>
- <text class="nav-count">{{ pathogenCount }}项</text>
- </view>
- <view class="nav-card" @tap="goToTaxonomy">
- <text class="nav-icon">📊</text>
- <text class="nav-label">菌群层级</text>
- <text class="nav-count">5级</text>
- </view>
- <view class="nav-card" @tap="goToRisks">
- <text class="nav-icon">⚠️</text>
- <text class="nav-label">疾病风险</text>
- <text class="nav-count">{{ riskCount }}项</text>
- </view>
- <view class="nav-card" @tap="goToFoods">
- <text class="nav-icon">🍽️</text>
- <text class="nav-label">饮食推荐</text>
- <text class="nav-count">{{ foodCount }}项</text>
- </view>
- <view class="nav-card" @tap="goToIndicators">
- <text class="nav-icon">📋</text>
- <text class="nav-label">全部指标</text>
- <text class="nav-count">{{ indicatorCount }}项</text>
- </view>
- </view>
- <!-- 肠道功能手风琴 -->
- <view class="accordion-section">
- <view class="accordion-header" @tap="toggleAccordion('gut')">
- <text class="accordion-title">肠道功能与代谢物</text>
- <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
- </view>
- <view class="accordion-body" v-if="accordionOpen.gut">
- <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
- <view class="indicator-row">
- <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
- {{ item.indicatorName }}
- <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
- <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
- <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
- </text>
- <view class="indicator-value" v-if="!isEditing">
- <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
- </view>
- <view class="indicator-edit" v-if="isEditing">
- <input class="edit-input" type="text" v-model="item.indicatorValue" />
- <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
- <text class="edit-picker">{{ item.status || '正常' }}</text>
- </picker>
- </view>
- </view>
- <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
- </view>
- <view class="empty-hint" v-if="gutIndicators.length === 0">暂无数据</view>
- </view>
- </view>
- <!-- 营养指标手风琴 -->
- <view class="accordion-section">
- <view class="accordion-header" @tap="toggleAccordion('nutri')">
- <text class="accordion-title">营养指标</text>
- <text class="accordion-arrow">{{ accordionOpen.nutri ? '收起' : '展开' }}</text>
- </view>
- <view class="accordion-body" v-if="accordionOpen.nutri">
- <view class="sub-group" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
- <text class="sub-group-title">{{ group.category }}</text>
- <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
- <view class="indicator-row">
- <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
- {{ item.indicatorName }}
- <text class="arrow-up" v-if="item.status === '偏高' || item.status === '过多'"> ↑</text>
- <text class="arrow-down" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'"> ↓</text>
- <text class="arrow-warn" v-else-if="item.status === '异常'"> ⚠</text>
- </text>
- <view class="indicator-value" v-if="!isEditing">
- <text :class="'val-text val-' + (item._cssClass || 'normal')">{{ item.indicatorValue || '--' }}</text>
- </view>
- <view class="indicator-edit" v-if="isEditing">
- <input class="edit-input" type="text" v-model="item.indicatorValue" />
- <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
- <text class="edit-picker">{{ item.status || '正常' }}</text>
- </picker>
- </view>
- </view>
- <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
- </view>
- </view>
- <view class="empty-hint" v-if="nutritionGroups.length === 0">暂无数据</view>
- </view>
- </view>
- <view class="feedback-section" @tap="openFeedback">
- <text class="feedback-icon">💬</text>
- <text class="feedback-text">对这个报告有疑问或建议?告诉我们</text>
- <text class="feedback-arrow">›</text>
- </view>
- <view class="bottom-spacer"></view>
- </scroll-view>
- <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
- <feedback-popup :visible="feedbackVisible" :report-id="reportId" @close="closeFeedback" />
- <view class="fab" v-if="!isEditing" @tap="toggleEdit">
- <text class="fab-icon">✏️</text>
- </view>
- <view class="bottom-bar" v-if="isEditing">
- <view class="btn-cancel" @tap="cancelEdit">取消</view>
- <view class="btn-save" @tap="saveEdit">保存</view>
- </view>
- </view>
- </template>
- <script>
- import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge } from '../../utils/api.js'
- import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
- import FeedbackPopup from '../../components/feedback-popup.vue'
- var INDICATOR_TYPE_MAP = {
- '维生素': 'vitamin',
- '氨基酸': 'amino_acid',
- '微量元素': 'indicator',
- '肠道屏障与代谢物': 'indicator',
- '短链脂肪酸': 'indicator',
- '神经递质与激素': 'indicator',
- '抗生素风险评估': 'indicator'
- }
- export default {
- components: { HealthKnowledgePopup, FeedbackPopup },
- data() {
- return {
- reportId: null,
- reportData: null,
- report: {},
- reportInterpretation: null,
- isEditing: false,
- accordionOpen: { gut: false, nutri: false },
- scoreItems: [],
- allIndicators: [],
- gutIndicators: [],
- nutritionGroups: [],
- floraCount: 0,
- riskCount: 0,
- foodCount: 0,
- indicatorCount: 0,
- pathogenCount: 0,
- knowledgeVisible: false,
- knowledgeData: {},
- feedbackVisible: false,
- indStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多', '异常']
- }
- },
- onLoad: function(options) {
- this.reportId = options.reportId ? parseInt(options.reportId) : null
- if (this.reportId) {
- this.loadData()
- }
- },
- methods: {
- goBack: function() { uni.navigateBack() },
- /** 中文状态 → CSS安全英文 */
- _cssForStatus: function(s) {
- var map = { '偏高': 'high', '过多': 'excess', '偏低': 'low', '缺乏': 'deficient', '不足': 'insufficient', '异常': 'abnormal', '注意': 'warning' }
- return map[s] || 'normal'
- },
- loadData: function() {
- var self = this
- getReportDetail(this.reportId).then(function(res) {
- if (res.code === 200 && res.data) {
- self.reportData = res.data
- self.report = res.data.report || res.data
- self.reportInterpretation = res.data.reportInterpretation || null
- self.buildScores(res.data)
- self.buildIndicators(res.data)
- self.floraCount = (res.data.gutFlora || []).length
- self.riskCount = (res.data.diseaseRisks || []).length
- self.foodCount = (res.data.foods || []).length
- self.indicatorCount = (res.data.indicators || []).length
- // 计算病原菌总数
- var flora = res.data.gutFlora || []
- self.pathogenCount = flora.filter(function(i) {
- return i.category === 'pathogen_genus' || i.category === 'pathogen_detection'
- }).length
- }
- })
- },
- buildScores: function(data) {
- var r = data.report || data
- var items = []
- var scoreFields = [
- { key: 'overallScore', label: '综合', color: '#4A9BD7' },
- { key: 'gutHealthScore', label: '菌群健康', color: '#10B981' },
- { key: 'chronicDiseaseScore', label: '慢病控制', color: '#F59E0B' },
- { key: 'nutritionScore', label: '营养均衡', color: '#FF8C42' },
- { key: 'gutBalanceScore', label: '平衡', color: '#6366F1' },
- { key: 'gutDiversityScore', label: '多样性', color: '#FF6B9D' },
- { key: 'beneficialBacteriaScore', label: '有益菌', color: '#10B981' },
- { key: 'harmfulBacteriaScore', label: '有害菌', color: '#C62828' },
- { key: 'coreSpeciesScore', label: '核心菌属', color: '#4A9BD7' }
- ]
- for (var i = 0; i < scoreFields.length; i++) {
- var f = scoreFields[i]
- if (r[f.key] != null) {
- items.push({ value: r[f.key], label: f.label, color: f.color })
- }
- }
- this.scoreItems = items
- },
- buildIndicators: function(data) {
- var indicators = data.indicators || []
- // 为每个指标添加_cssClass字段(wxss不支持中文类名)
- var self = this
- indicators.forEach(function(item) {
- item._cssClass = self._cssForStatus(item.status)
- })
- this.allIndicators = indicators
- var gutCategories = ['肠道屏障与代谢物', '短链脂肪酸', '神经递质与激素', '抗生素风险评估']
- this.gutIndicators = indicators.filter(function(item) {
- return gutCategories.indexOf(item.category) >= 0
- })
- var seen = {}
- var groups = []
- for (var i = 0; i < indicators.length; i++) {
- var item = indicators[i]
- if (gutCategories.indexOf(item.category) >= 0) continue
- if (!seen[item.category]) {
- seen[item.category] = true
- groups.push({ category: item.category, items: [] })
- }
- for (var g = 0; g < groups.length; g++) {
- if (groups[g].category === item.category) {
- groups[g].items.push(item)
- break
- }
- }
- }
- this.nutritionGroups = groups
- },
- toggleAccordion: function(key) {
- this.accordionOpen[key] = !this.accordionOpen[key]
- },
- indicatorType: function(item) {
- return INDICATOR_TYPE_MAP[item.category] || 'nutrient'
- },
- indStatusIndex: function(status) {
- var idx = this.indStatuses.indexOf(status)
- return idx >= 0 ? idx : 0
- },
- onIndStatusChange: function(e, item) {
- item.status = this.indStatuses[e.detail.value]
- },
- showKnowledge: function(type, name) {
- var self = this
- // 优先使用 KB v3 查询指标调整建议
- queryIndicatorKnowledge(name).then(function(res) {
- if (res.code === 200 && res.data) {
- var v3 = res.data
- var high = v3['偏高影响'] || ''
- var low = v3['偏低影响'] || ''
- self.knowledgeData = {
- itemName: v3['名称'] || name,
- category: v3['说明'] || '',
- description: (high ? '偏高影响:' + high + '\n' : '') + (low ? '偏低影响:' + low : ''),
- suggestion: v3['调整建议'] || ''
- }
- self.knowledgeVisible = true
- } else {
- // 回退旧版 DB 查询
- self.queryOldKnowledgeBase(type, name)
- }
- }).catch(function() {
- self.queryOldKnowledgeBase(type, name)
- })
- },
- queryOldKnowledgeBase: function(type, name) {
- var self = this
- queryKnowledgeBase(type, name).then(function(res) {
- if (res.code === 200 && res.data) {
- self.knowledgeData = res.data
- self.knowledgeVisible = true
- } else {
- self.knowledgeData = { itemName: name }
- self.knowledgeVisible = true
- }
- })
- },
- closeKnowledge: function() {
- this.knowledgeVisible = false
- this.knowledgeData = {}
- },
- openFeedback: function() {
- this.feedbackVisible = true
- },
- closeFeedback: function() {
- this.feedbackVisible = false
- },
- toggleEdit: function() {
- this.isEditing = true
- },
- cancelEdit: function() {
- this.isEditing = false
- this.loadData()
- },
- saveEdit: function() {
- var self = this
- var payload = {
- overallScore: this.report.overallScore,
- gutHealthScore: this.report.gutHealthScore,
- nutritionScore: this.report.nutritionScore,
- indicators: this.allIndicators
- }
- editHealthReport(this.reportId, payload, 0).then(function(res) {
- if (res.code === 200) {
- uni.showToast({ title: '已保存', icon: 'success' })
- self.isEditing = false
- self.loadData()
- } else {
- uni.showToast({ title: res.message || '保存失败', icon: 'none' })
- }
- })
- },
- goToSpecies: function() {
- uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
- },
- goToPathogen: function() {
- uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId + '&tab=pathogen_genus' })
- },
- goToTaxonomy: function() {
- uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId + '&tab=taxonomy_class' })
- },
- goToRisks: function() {
- uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
- },
- goToFoods: function() {
- uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
- },
- goToIndicators: function() {
- this.accordionOpen.nutri = true
- },
- // AI问答功能已屏蔽
- goAIChat: function() {}
- }
- }
- </script>
- <style scoped>
- .page { min-height: 100vh; background: #f8f8f8; }
- .nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
- .nav-back { padding: 10rpx 0; }
- .back-text { font-size: 28rpx; color: #4A9BD7; }
- .nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
- .content { height: calc(100vh - 100rpx); }
- .report-header { background: linear-gradient(135deg, #4A9BD7, #6366F1); padding: 40rpx 30rpx; color: #fff; }
- .report-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
- .report-meta { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
- .meta-item { font-size: 24rpx; opacity: 0.9; }
- .badge-row { display: flex; gap: 16rpx; margin-top: 12rpx; }
- .badge { padding: 6rpx 20rpx; background: rgba(255,255,255,0.2); border-radius: 20rpx; font-size: 22rpx; }
- .scores-scroll { padding: 20rpx 0; white-space: nowrap; background: #fff; }
- .score-item { display: inline-flex; flex-direction: column; align-items: center; margin: 0 20rpx; }
- .score-ring { width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; margin-bottom: 8rpx; }
- .score-label { font-size: 22rpx; color: #666; }
- .quick-nav { display: flex; padding: 20rpx; background: #fff; margin-bottom: 16rpx; }
- .nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
- .nav-icon { font-size: 40rpx; margin-bottom: 6rpx; }
- .nav-label { font-size: 24rpx; color: #333; }
- .nav-count { font-size: 20rpx; color: #999; }
- .accordion-section { background: #fff; margin-bottom: 16rpx; }
- .accordion-header { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
- .accordion-title { font-size: 30rpx; font-weight: 600; color: #333; }
- .accordion-arrow { font-size: 24rpx; color: #999; }
- .accordion-body { padding: 10rpx 30rpx 20rpx; }
- .sub-group { margin-bottom: 16rpx; }
- .sub-group-title { font-size: 26rpx; color: #4A9BD7; font-weight: 600; display: block; margin: 12rpx 0 8rpx; }
- .indicator-card { padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
- .indicator-row { display: flex; align-items: center; justify-content: space-between; }
- .indicator-name { font-size: 28rpx; color: #333; flex: 1; }
- .arrow-up { color: #C62828; font-size: 22rpx; }
- .arrow-down { color: #E65100; font-size: 22rpx; }
- .arrow-warn { color: #F59E0B; font-size: 22rpx; }
- .val-text { font-size: 28rpx; font-weight: 600; color: #333; }
- .val-high { color: #C62828; }
- .val-excess { color: #C62828; }
- .val-low { color: #E65100; }
- .val-deficient { color: #E65100; }
- .val-insufficient { color: #E65100; }
- .val-abnormal { color: #C62828; }
- .val-warning { color: #F59E0B; }
- .indicator-range { font-size: 22rpx; color: #bbb; display: block; margin-top: 4rpx; }
- .indicator-edit { display: flex; gap: 8rpx; align-items: center; }
- .edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 26rpx; width: 100rpx; }
- .edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 24rpx; color: #333; }
- .fab { position: fixed; right: 30rpx; bottom: 40rpx; width: 100rpx; height: 100rpx; background: #4A9BD7; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
- .fab-icon { font-size: 40rpx; color: #fff; }
- .bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; z-index: 100; }
- .btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
- .btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
- .feedback-section {
- display: flex;
- align-items: center;
- padding: 24rpx 30rpx;
- background: #fff;
- margin: 16rpx;
- border-radius: 12rpx;
- border: 1rpx solid #f0f0f0;
- }
- .feedback-icon { font-size: 32rpx; margin-right: 12rpx; }
- .feedback-text { flex: 1; font-size: 26rpx; color: #4A9BD7; }
- .feedback-arrow { font-size: 28rpx; color: #ccc; }
- .bottom-spacer { height: 40rpx; }
- /* 报告解读 */
- .interpretation-box {
- background: #FFF8E1;
- border-radius: 12rpx;
- padding: 16rpx 20rpx;
- margin: 16rpx 0 0;
- border-left: 6rpx solid #FFB300;
- }
- .interp-text {
- font-size: 24rpx;
- color: #795548;
- line-height: 1.6;
- }
- /* AI 解读入口 */
- .ai-fab {
- position: fixed;
- right: 30rpx;
- bottom: 100rpx;
- display: flex;
- flex-direction: row;
- align-items: center;
- background: linear-gradient(135deg, #6366F1, #8B5CF6);
- border-radius: 40rpx;
- padding: 16rpx 28rpx;
- box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
- z-index: 100;
- }
- .ai-fab .fab-icon { font-size: 32rpx; margin-right: 12rpx; }
- .ai-fab .fab-text { font-size: 26rpx; color: #fff; font-weight: 500; }
- </style>
|