pet/pages/pets/health-charts.vue

958 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="health-charts-container page-container-with-bg">
<!-- 健康状态总览卡片 -->
<view class="health-overview-card">
<view class="overview-header">
<text class="pet-name">{{ petName }}的健康档案</text>
<text class="last-update">最后更新{{ lastUpdateTime }}</text>
</view>
<view class="health-summary">
<view class="health-score-section">
<view class="score-circle">
<view class="circle-progress" :style="{ background: `conic-gradient(${getScoreColor()} 0deg, ${getScoreColor()} ${healthScore * 3.6}deg, #E0E0E0 ${healthScore * 3.6}deg)` }">
<view class="circle-center">
<text class="score-value">{{ healthScore }}</text>
<text class="score-label">健康评分</text>
</view>
</view>
</view>
<view class="score-details">
<view class="score-item">
<text class="score-name">体重状态</text>
<text class="score-points" :style="{ color: getStatusColor(weightData.status) }">{{ weightData.weightScore }}</text>
</view>
<view class="score-item">
<text class="score-name">疫苗状态</text>
<text class="score-points" :style="{ color: getStatusColor(vaccineData.status) }">{{ vaccineData.vaccineScore }}</text>
</view>
<view class="score-item">
<text class="score-name">医疗记录</text>
<text class="score-points" :style="{ color: getStatusColor(medicalData.status) }">{{ medicalData.medicalScore }}</text>
</view>
<view class="score-item">
<text class="score-name">生长发育</text>
<text class="score-points" :style="{ color: getStatusColor(growthData.status) }">{{ growthData.growthScore }}</text>
</view>
</view>
</view>
<view class="health-trend">
<view class="trend-indicator" :class="healthTrend">
<text class="trend-icon">{{ getTrendIcon() }}</text>
<text class="trend-text">{{ getTrendText() }}</text>
</view>
</view>
<view class="risk-alerts" v-if="riskFactors.length > 0">
<text class="alerts-title">⚠️ 风险提醒</text>
<view class="alert-item" v-for="risk in riskFactors" :key="risk.type" :class="risk.level">
<text class="alert-text">{{ risk.description }}</text>
</view>
</view>
</view>
</view>
<!-- 时间维度切换 -->
<view class="time-selector">
<view class="selector-header">
<text class="selector-title">📊 数据视图</text>
</view>
<view class="time-tabs">
<view
class="time-tab"
:class="{ active: activeTimeRange === range.key }"
v-for="range in timeRanges"
:key="range.key"
@click="switchTimeRange(range.key)"
>
<text class="tab-text">{{ range.label }}</text>
</view>
</view>
</view>
<!-- 多维度数据展示 -->
<view class="data-charts-section">
<!-- 体重变化趋势图表 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">📈 体重变化趋势</text>
<view class="chart-status" :class="weightData.status">
<text class="status-text">{{ getStatusText(weightData.status) }}</text>
</view>
</view>
<qiun-data-charts
type="line"
:opts="weightChartOpts"
:chartData="weightChartData"
:canvas2d="true"
:canvasId="'weightChart'"
:canvas-id="'weightChart'"
/>
<view class="chart-summary">
<text class="summary-text">当前体重:{{ weightData.currentWeight }}kg较上周{{ weightData.weeklyChange.change >= 0 ? '增加' : '减少' }}{{ Math.abs(weightData.weeklyChange.change) }}kg</text>
</view>
</view>
<!-- 疫苗接种状态时间线 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">💉 疫苗接种状态</text>
<view class="chart-status" :class="vaccineData.status">
<text class="status-text">{{ getStatusText(vaccineData.status) }}</text>
</view>
</view>
<view class="vaccine-timeline">
<view class="timeline-summary">
<view class="summary-item">
<text class="summary-label">完成度</text>
<text class="summary-value">{{ vaccineData.statusSummary.completionRate }}%</text>
</view>
<view class="summary-item">
<text class="summary-label">已完成</text>
<text class="summary-value" style="color: #4CAF50;">{{ vaccineData.statusSummary.completed }}</text>
</view>
<view class="summary-item">
<text class="summary-label">即将到期</text>
<text class="summary-value" style="color: #FF9800;">{{ vaccineData.statusSummary.expiringSoon }}</text>
</view>
<view class="summary-item">
<text class="summary-label">已过期</text>
<text class="summary-value" style="color: #F44336;">{{ vaccineData.statusSummary.expired }}</text>
</view>
</view>
</view>
</view>
<!-- 健康记录统计 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">🏥 健康记录统计</text>
<view class="chart-status" :class="medicalData.status">
<text class="status-text">{{ getStatusText(medicalData.status) }}</text>
</view>
</view>
<view class="medical-stats">
<view class="stats-grid">
<view class="stat-card">
<text class="stat-number">{{ medicalData.totalRecords }}</text>
<text class="stat-label">总记录数</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ medicalData.medicalVisits }}</text>
<text class="stat-label">近期就医</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ medicalData.symptoms }}</text>
<text class="stat-label">异常症状</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ medicalData.recentRecords }}</text>
<text class="stat-label">近期记录</text>
</view>
</view>
</view>
</view>
<!-- 生长发育曲线 -->
<view class="chart-card" v-if="showGrowthChart">
<view class="chart-header">
<text class="chart-title">📏 生长发育曲线</text>
<view class="chart-status" :class="growthData.status">
<text class="status-text">{{ getStatusText(growthData.status) }}</text>
</view>
</view>
<view class="growth-info">
<view class="growth-stage">
<text class="stage-label">生长阶段:</text>
<text class="stage-value">{{ getGrowthStageText() }}</text>
</view>
<view class="growth-summary">
<text class="summary-text">{{ getGrowthSummary() }}</text>
</view>
</view>
</view>
</view>
<!-- AI健康分析卡片 -->
<view class="ai-health-analysis">
<view class="analysis-header">
<text class="analysis-title">🤖 AI健康分析</text>
<view class="analysis-badge">
<text class="badge-text">智能分析</text>
</view>
</view>
<view class="analysis-content">
<view class="analysis-section">
<text class="section-title">📊 综合评估</text>
<text class="section-content">{{ aiAnalysis.overallAssessment }}</text>
</view>
<view class="analysis-section">
<text class="section-title">📈 趋势分析</text>
<text class="section-content">{{ aiAnalysis.trendAnalysis }}</text>
</view>
<view class="analysis-section">
<text class="section-title">💡 个性化建议</text>
<text class="section-content">{{ aiAnalysis.recommendations }}</text>
</view>
<view class="analysis-section">
<text class="section-title">🛡️ 预防措施</text>
<text class="section-content">{{ aiAnalysis.preventiveMeasures }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
// 简化的健康管理器 - 从 utils/healthManager.js 和 weightManager.js 移入
const healthManager = {
getHealthScore(petId) {
// 简化的健康评分计算
return Math.floor(Math.random() * 20) + 80 // 80-100分
},
getHealthSummary(petId) {
return {
overall: '良好',
weight: '正常',
vaccine: '已完成',
lastCheckup: '2024-01-15'
}
}
}
const weightManager = {
storageKey: 'pet_weight_records',
getWeightRecords(petId) {
try {
const allRecords = uni.getStorageSync(this.storageKey) || {}
let records = allRecords[petId] || []
if (records.length === 0) {
records = this.initializeTestData(petId)
allRecords[petId] = records
uni.setStorageSync(this.storageKey, allRecords)
}
return records
} catch (error) {
console.error('获取体重记录失败:', error)
return this.initializeTestData(petId)
}
},
initializeTestData(petId) {
const now = new Date()
const testData = []
const baseWeight = 3.8
for (let i = 90; i >= 0; i -= 7) {
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
const weight = baseWeight + (Math.random() - 0.5) * 0.3
testData.push({
id: Date.now() + i,
petId: petId,
weight: parseFloat(weight.toFixed(1)),
date: date.toISOString().split('T')[0],
note: i === 0 ? '最新记录' : ''
})
}
return testData.reverse()
},
addWeightRecord(petId, weight, date, note = '') {
try {
const allRecords = uni.getStorageSync(this.storageKey) || {}
if (!allRecords[petId]) {
allRecords[petId] = []
}
const newRecord = {
id: Date.now(),
petId: petId,
weight: parseFloat(weight),
date: date,
note: note
}
allRecords[petId].push(newRecord)
allRecords[petId].sort((a, b) => new Date(a.date) - new Date(b.date))
uni.setStorageSync(this.storageKey, allRecords)
return true
} catch (error) {
console.error('添加体重记录失败:', error)
return false
}
},
getWeightTrend(petId) {
const records = this.getWeightRecords(petId)
if (records.length < 2) return 'stable'
const recent = records.slice(-3)
const avg = recent.reduce((sum, r) => sum + r.weight, 0) / recent.length
const prev = records[records.length - 4]?.weight || avg
if (avg > prev + 0.2) return 'increasing'
if (avg < prev - 0.2) return 'decreasing'
return 'stable'
}
}
export default {
data() {
return {
petId: '',
petName: '',
petInfo: {},
lastUpdateTime: '',
// 健康数据
healthScore: 85,
healthTrend: 'stable',
weightData: {},
vaccineData: {},
medicalData: {},
growthData: {},
riskFactors: [],
// 时间范围
activeTimeRange: 'month',
timeRanges: [
{ key: 'week', label: '近一周' },
{ key: 'month', label: '近一月' },
{ key: 'year', label: '近一年' },
{ key: 'all', label: '全部历史' }
],
// 图表配置
weightChartOpts: {
color: ["#FF8A80", "#64B5F6"],
padding: [15, 15, 0, 15],
enableScroll: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
fontSize: 10,
fontColor: "#666666"
},
yAxis: {
gridType: "dash",
dashLength: 2,
fontSize: 10,
fontColor: "#666666"
},
extra: {
line: {
type: "curve",
width: 2,
activeType: "hollow"
}
}
},
// 图表数据
weightChartData: {},
// AI分析
aiAnalysis: {
overallAssessment: '',
trendAnalysis: '',
recommendations: '',
preventiveMeasures: ''
}
}
},
computed: {
showGrowthChart() {
return this.petInfo.age < 2 // 只为幼宠显示生长曲线
}
},
onLoad(options) {
this.petId = options.petId || ''
this.petName = options.petName || '宠物'
this.loadPetInfo()
this.loadHealthData()
this.generateChartData()
},
methods: {
loadPetInfo() {
try {
const pets = uni.getStorageSync('pets') || []
this.petInfo = pets.find(pet => pet.id == this.petId) || {
id: this.petId,
name: this.petName,
breed: '橘猫',
age: 2,
gender: '公'
}
} catch (error) {
console.error('加载宠物信息失败:', error)
}
},
loadHealthData() {
// 获取综合健康数据
const healthData = healthManager.getComprehensiveHealthData(this.petId, this.petInfo)
this.healthScore = healthData.healthScore
this.healthTrend = healthData.healthTrend
this.weightData = healthData.weightData
this.vaccineData = healthData.vaccineData
this.medicalData = healthData.medicalData
this.growthData = healthData.growthData
this.riskFactors = healthData.riskFactors
// 生成AI分析
this.aiAnalysis = healthManager.generateAIHealthAnalysis(this.petId, this.petInfo)
// 更新最后更新时间
this.lastUpdateTime = new Date().toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
},
switchTimeRange(timeKey) {
this.activeTimeRange = timeKey
this.generateChartData()
},
generateChartData() {
// 生成体重图表数据
this.weightChartData = weightManager.generateChartData(this.petId, this.activeTimeRange)
},
getScoreColor() {
if (this.healthScore >= 85) return '#4CAF50'
if (this.healthScore >= 70) return '#FF9800'
return '#F44336'
},
getStatusColor(status) {
const colors = {
'good': '#4CAF50',
'warning': '#FF9800',
'poor': '#F44336'
}
return colors[status] || '#666666'
},
getStatusText(status) {
const texts = {
'good': '良好',
'warning': '注意',
'poor': '需改善'
}
return texts[status] || '未知'
},
getTrendIcon() {
const icons = {
'improving': '📈',
'stable': '➡️',
'declining': '📉'
}
return icons[this.healthTrend] || '➡️'
},
getTrendText() {
const texts = {
'improving': '改善中',
'stable': '保持稳定',
'declining': '需关注'
}
return texts[this.healthTrend] || '稳定'
},
getGrowthStageText() {
const stages = {
'kitten': '幼猫期',
'young': '青年期',
'adult': '成年期',
'senior': '老年期'
}
return stages[this.growthData.growthStage] || '成年期'
},
getGrowthSummary() {
const stage = this.growthData.growthStage
const weight = this.growthData.currentWeight
if (stage === 'kitten') {
return `幼猫期体重${weight}kg生长发育${this.growthData.status === 'good' ? '正常' : '需关注'}`
} else if (stage === 'senior') {
return `老年期体重${weight}kg建议定期体检关注健康状况`
} else {
return `${this.getGrowthStageText()}体重${weight}kg整体发育状况良好`
}
}
}
}
</script>
<style lang="scss" scoped>
.health-charts-container {
padding: 20rpx;
}
/* 健康状态总览卡片 */
.health-overview-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 28rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.overview-header {
text-align: center;
margin-bottom: 32rpx;
.pet-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
display: block;
margin-bottom: 8rpx;
}
.last-update {
font-size: 22rpx;
color: #999999;
display: block;
}
}
.health-summary {
.health-score-section {
display: flex;
align-items: center;
gap: 32rpx;
margin-bottom: 32rpx;
.score-circle {
.circle-progress {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.circle-center {
width: 100rpx;
height: 100rpx;
background: white;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.score-value {
font-size: 32rpx;
font-weight: bold;
color: #333333;
line-height: 1;
}
.score-label {
font-size: 20rpx;
color: #999999;
margin-top: 6rpx;
}
}
}
}
.score-details {
flex: 1;
.score-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.score-name {
font-size: 24rpx;
color: #666666;
}
.score-points {
font-size: 24rpx;
font-weight: bold;
}
}
}
}
.health-trend {
text-align: center;
margin-bottom: 24rpx;
.trend-indicator {
display: inline-flex;
align-items: center;
gap: 12rpx;
padding: 12rpx 24rpx;
border-radius: 20rpx;
&.improving {
background: #E8F5E8;
color: #4CAF50;
}
&.stable {
background: #E3F2FD;
color: #2196F3;
}
&.declining {
background: #FFEBEE;
color: #F44336;
}
.trend-icon {
font-size: 24rpx;
}
.trend-text {
font-size: 24rpx;
font-weight: 500;
}
}
}
.risk-alerts {
.alerts-title {
font-size: 26rpx;
font-weight: bold;
color: #FF9800;
margin-bottom: 16rpx;
display: block;
}
.alert-item {
padding: 12rpx 16rpx;
border-radius: 12rpx;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
&.high {
background: #FFEBEE;
border-left: 4rpx solid #F44336;
}
&.medium {
background: #FFF3E0;
border-left: 4rpx solid #FF9800;
}
&.low {
background: #E8F5E8;
border-left: 4rpx solid #4CAF50;
}
.alert-text {
font-size: 22rpx;
color: #666666;
}
}
}
}
}
/* 时间选择器 */
.time-selector {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 28rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.selector-header {
margin-bottom: 28rpx;
.selector-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
}
.time-tabs {
display: flex;
gap: 12rpx;
.time-tab {
flex: 1;
text-align: center;
padding: 16rpx 12rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.1);
transition: all 0.3s ease;
&.active {
background: #FF8A80;
.tab-text {
color: white;
}
}
&:active {
transform: scale(0.95);
}
.tab-text {
font-size: 24rpx;
font-weight: 500;
color: #666666;
}
}
}
}
/* 数据图表区域 */
.data-charts-section {
.chart-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 28rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
.chart-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.chart-status {
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-size: 22rpx;
font-weight: 500;
&.good {
background: #E8F5E8;
color: #4CAF50;
}
&.warning {
background: #FFF3E0;
color: #FF9800;
}
&.poor {
background: #FFEBEE;
color: #F44336;
}
.status-text {
font-size: 22rpx;
}
}
}
.chart-summary {
margin-top: 20rpx;
text-align: center;
.summary-text {
font-size: 24rpx;
color: #666666;
}
}
}
}
/* 疫苗时间线 */
.vaccine-timeline {
.timeline-summary {
display: flex;
justify-content: space-around;
background: rgba(255, 138, 128, 0.05);
border-radius: 20rpx;
padding: 24rpx;
.summary-item {
text-align: center;
.summary-label {
font-size: 20rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.summary-value {
font-size: 28rpx;
font-weight: bold;
display: block;
}
}
}
}
/* 医疗统计 */
.medical-stats {
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
.stat-card {
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
padding: 20rpx;
text-align: center;
.stat-number {
font-size: 32rpx;
font-weight: bold;
color: #FF8A80;
display: block;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 20rpx;
color: #999999;
display: block;
}
}
}
}
/* 生长信息 */
.growth-info {
.growth-stage {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.stage-label {
font-size: 24rpx;
color: #999999;
}
.stage-value {
font-size: 24rpx;
font-weight: bold;
color: #FF8A80;
}
}
.growth-summary {
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
padding: 20rpx;
.summary-text {
font-size: 24rpx;
color: #666666;
line-height: 1.5;
}
}
}
/* AI健康分析 */
.ai-health-analysis {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 28rpx;
padding: 32rpx;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.analysis-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
.analysis-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.analysis-badge {
background: linear-gradient(135deg, #64B5F6, #81C784);
border-radius: 20rpx;
padding: 8rpx 16rpx;
.badge-text {
font-size: 20rpx;
color: white;
font-weight: 500;
}
}
}
.analysis-content {
.analysis-section {
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
margin-bottom: 12rpx;
display: block;
}
.section-content {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
display: block;
}
}
}
}
</style>