963 lines
24 KiB
Plaintext
963 lines
24 KiB
Plaintext
<template>
|
||
<scroll-view direction="vertical" class="analysis-container">
|
||
<!-- 头部导航 -->
|
||
<view class="header">
|
||
<text class="title">数据分析</text>
|
||
<view class="header-actions">
|
||
<button class="export-btn" @click="exportReport">导出报告</button>
|
||
<button class="refresh-btn" @click="refreshAnalysis">刷新</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间范围选择 -->
|
||
<view class="time-range-card">
|
||
<text class="card-title">分析时间范围</text>
|
||
<view class="time-range-tabs">
|
||
<button class="time-tab" :class="{ active: activeTimeRange == range }"
|
||
v-for="(range, index) in timeRanges" :key="index" @click="selectTimeRange(range)">
|
||
{{ getTimeRangeLabel(range) }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 总体健康评分 -->
|
||
<view class="health-score-card">
|
||
<text class="card-title">健康评分</text>
|
||
<view class="score-display">
|
||
<view class="score-circle">
|
||
<text class="score-value">{{ overallScore }}</text>
|
||
<text class="score-label">分</text>
|
||
</view>
|
||
<view class="score-details">
|
||
<view class="score-item" v-for="(item, index) in scoreBreakdown" :key="index">
|
||
<text class="score-category">{{ item.category }}</text>
|
||
<view class="score-bar">
|
||
<view class="score-progress" :style="{ width: item.percentage + '%' }"></view>
|
||
</view>
|
||
<text class="score-number">{{ item.score }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 指标趋势分析 -->
|
||
<view class="trends-card">
|
||
<text class="card-title">指标趋势</text>
|
||
<view class="trend-tabs">
|
||
<button class="trend-tab" :class="{ active: activeTrendType == type }"
|
||
v-for="(type, index) in trendTypes" :key="index" @click="selectTrendType(type)">
|
||
{{ getTrendTypeLabel(type) }}
|
||
</button>
|
||
</view>
|
||
<ak-charts :option="trendChartOption" :canvas-id="'trend-analysis-chart'" class="trend-chart" />
|
||
<view class="trend-summary">
|
||
<text class="trend-text">{{ trendSummary }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 异常检测 -->
|
||
<view class="anomaly-card" v-if="anomalies.length > 0">
|
||
<text class="card-title">异常检测</text>
|
||
<view class="anomaly-list">
|
||
<view class="anomaly-item" v-for="(anomaly, index) in anomalies" :key="index">
|
||
<view class="anomaly-header">
|
||
<text class="anomaly-type">{{ anomaly.type }}</text>
|
||
<text class="anomaly-severity" :class="getSeverityClass(anomaly.severity)">
|
||
{{ anomaly.severity }}
|
||
</text>
|
||
</view>
|
||
<text class="anomaly-description">{{ anomaly.description }}</text>
|
||
<text class="anomaly-time">{{ formatTime(anomaly.detected_at) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI分析报告 -->
|
||
<view class="ai-report-card">
|
||
<text class="card-title">AI分析报告</text>
|
||
<view class="report-status" v-if="isAnalyzing">
|
||
<text class="analyzing-text">AI正在分析您的数据...</text>
|
||
<view class="analyzing-indicator"></view>
|
||
</view>
|
||
<view class="report-content" v-else-if="aiReport !== null">
|
||
<view class="report-section">
|
||
<text class="section-title">健康状况评估</text>
|
||
<text class="section-content">{{ aiReport?.health_assessment ?? '' }}</text>
|
||
</view>
|
||
<view class="report-section">
|
||
<text class="section-title">趋势分析</text>
|
||
<text class="section-content">{{ aiReport?.trend_analysis ?? '' }}</text>
|
||
</view>
|
||
<view class="report-section">
|
||
<text class="section-title">个性化建议</text>
|
||
<view class="recommendations">
|
||
<view class="recommendation-item" v-for="(rec, index) in (aiReport?.recommendations ?? [])"
|
||
:key="index">
|
||
<text class="rec-icon">💡</text>
|
||
<text class="rec-text">{{ rec }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<button class="analyze-btn" v-else @click="startAIAnalysis">开始AI分析</button>
|
||
</view>
|
||
|
||
<!-- 数据对比 -->
|
||
<view class="comparison-card">
|
||
<text class="card-title">数据对比</text>
|
||
<view class="comparison-options">
|
||
<view class="picker-view" @click="showComparisonPicker">
|
||
<text>与{{ comparisonPeriods[comparisonIndex] }}对比</text>
|
||
<text class="picker-arrow">▼</text>
|
||
</view>
|
||
</view>
|
||
<view class="comparison-results" v-if="comparisonData.length > 0">
|
||
<view class="comparison-item" v-for="(item, index) in comparisonData" :key="index">
|
||
<text class="comparison-metric">{{ item.metric_name }}</text>
|
||
<view class="comparison-values">
|
||
<text class="current-value">当前: {{ item.current_period }}</text>
|
||
<text class="previous-value">之前: {{ item.previous_period }}</text>
|
||
<text class="change-value" :class="getChangeClass(item.change_percentage)">
|
||
{{ item.change_percentage > 0 ? '+' : '' }}{{ item.change_percentage }}%
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import akCharts from '@/uni_modules/ak-charts/components/ak-charts.uvue'
|
||
|
||
import { ref, onMounted } from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { SensorMeasurement, HealthScoreBreakdown, HealthAnomaly, AIAnalysisReport, TrendComparison } from './types.uts'
|
||
import { setClipboardData, SetClipboardDataOption } from '@/uni_modules/lime-clipboard'
|
||
|
||
// 响应式数据
|
||
const activeTimeRange = ref<string>('7d')
|
||
const activeTrendType = ref<string>('heart_rate')
|
||
const overallScore = ref<number>(82)
|
||
const scoreBreakdown = ref<Array<HealthScoreBreakdown>>([])
|
||
const trendChartOption = ref<UTSJSONObject>(new UTSJSONObject())
|
||
const trendSummary = ref<string>('')
|
||
const anomalies = ref<Array<HealthAnomaly>>([])
|
||
const aiReport = ref<AIAnalysisReport | null>(null)
|
||
const isAnalyzing = ref<boolean>(false)
|
||
const comparisonIndex = ref<number>(0)
|
||
const comparisonData = ref<Array<TrendComparison>>([])
|
||
|
||
// 选项数据
|
||
const timeRanges = ['24h', '7d', '30d', '90d']
|
||
const trendTypes = ['heart_rate', 'steps', 'spo2', 'bp', 'temp']
|
||
const comparisonPeriods = ['上周', '上月', '上季度', '去年同期']
|
||
|
||
const userId = 'eed3824b-bba1-4309-8048-19d17367c084'
|
||
|
||
|
||
async function loadHealthScore() {
|
||
// 模拟健康评分数据
|
||
const mockScoreBreakdown : Array<HealthScoreBreakdown> = [
|
||
{
|
||
category: '心率',
|
||
score: 85,
|
||
percentage: 85,
|
||
trend: 'stable',
|
||
color: '#4CAF50'
|
||
},
|
||
{
|
||
category: '运动',
|
||
score: 78,
|
||
percentage: 78,
|
||
trend: 'up',
|
||
color: '#2196F3'
|
||
},
|
||
{
|
||
category: '睡眠',
|
||
score: 80,
|
||
percentage: 80,
|
||
trend: 'down',
|
||
color: '#9C27B0'
|
||
},
|
||
{
|
||
category: '血氧',
|
||
score: 88,
|
||
percentage: 88,
|
||
trend: 'stable',
|
||
color: '#FF9800'
|
||
}
|
||
]
|
||
|
||
scoreBreakdown.value = mockScoreBreakdown
|
||
}
|
||
|
||
|
||
|
||
function exportReport() {
|
||
// 构建报告数据
|
||
const reportData = new UTSJSONObject()
|
||
const currentAiReport = aiReport.value // 避免Smart cast问题
|
||
reportData.set('user_id', userId)
|
||
reportData.set('time_range', activeTimeRange.value)
|
||
reportData.set('overall_score', overallScore.value)
|
||
reportData.set('score_breakdown', scoreBreakdown.value)
|
||
reportData.set('anomalies', anomalies.value)
|
||
reportData.set('ai_report', currentAiReport)
|
||
reportData.set('generated_at', new Date().toISOString())
|
||
const jsonStr = JSON.stringify(reportData, null, 2)
|
||
setClipboardData({
|
||
data: jsonStr,
|
||
success: (res : UniError) => {
|
||
uni.showToast({
|
||
title: '报告已复制到剪贴板',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
} as SetClipboardDataOption)
|
||
}
|
||
|
||
// 工具函数
|
||
function getTimeRangeLabel(range : string) : string {
|
||
const labels = new Map<string, string>()
|
||
labels.set('24h', '24小时')
|
||
labels.set('7d', '7天')
|
||
labels.set('30d', '30天')
|
||
labels.set('90d', '90天')
|
||
|
||
return labels.get(range) ?? range
|
||
}
|
||
|
||
function getTrendTypeLabel(type : string) : string {
|
||
const labels = new Map<string, string>()
|
||
labels.set('heart_rate', '心率')
|
||
labels.set('steps', '步数')
|
||
labels.set('spo2', '血氧')
|
||
labels.set('bp', '血压')
|
||
labels.set('temp', '体温')
|
||
|
||
return labels.get(type) ?? type
|
||
}
|
||
|
||
function getTrendColor(type : string) : string {
|
||
const colors = new Map<string, string>()
|
||
colors.set('heart_rate', '#FF6B6B')
|
||
colors.set('steps', '#4ECDC4')
|
||
colors.set('spo2', '#45B7D1')
|
||
colors.set('bp', '#AB47BC')
|
||
colors.set('temp', '#FFA726')
|
||
|
||
return colors.get(type) ?? '#2196F3'
|
||
}
|
||
|
||
function getSeverityClass(severity : string) : string {
|
||
if (severity == '高') return 'severity-high'
|
||
if (severity == '中等') return 'severity-medium'
|
||
return 'severity-low'
|
||
}
|
||
|
||
function getChangeClass(change : number) : string {
|
||
if (change > 0) return 'change-positive'
|
||
if (change < 0) return 'change-negative'
|
||
return 'change-neutral'
|
||
}
|
||
|
||
function formatTime(timeStr : string) : string {
|
||
if (timeStr == '') return '--'
|
||
|
||
const time = new Date(timeStr)
|
||
const now = new Date()
|
||
const diff = now.getTime() - time.getTime()
|
||
|
||
if (diff < 3600000) { // 1小时内
|
||
const minutes = Math.floor(diff / 60000)
|
||
return `${minutes}分钟前`
|
||
} else if (diff < 86400000) { // 24小时内
|
||
const hours = Math.floor(diff / 3600000)
|
||
return `${hours}小时前`
|
||
} else {
|
||
const days = Math.floor(diff / 86400000)
|
||
return `${days}天前`
|
||
}
|
||
|
||
}
|
||
function generateTrendSummary(data : Array<SensorMeasurement>) {
|
||
if (data.length == 0) {
|
||
trendSummary.value = '暂无数据'
|
||
return
|
||
}
|
||
|
||
// 计算平均值和趋势
|
||
let sum = 0
|
||
let count = 0
|
||
const values : Array<number> = []
|
||
|
||
for (let i : Int = 0; i < data.length; i++) {
|
||
const item = data[i]
|
||
const rawData = item.raw_data
|
||
if (rawData !== null) {
|
||
let value : number = 0
|
||
|
||
if (activeTrendType.value == 'heart_rate') {
|
||
value = rawData.getNumber('bpm') ?? 0
|
||
} else if (activeTrendType.value == 'steps') {
|
||
value = rawData.getNumber('count') ?? 0
|
||
} else if (activeTrendType.value == 'spo2') {
|
||
value = rawData.getNumber('spo2') ?? 0
|
||
} else if (activeTrendType.value == 'bp') {
|
||
value = rawData.getNumber('systolic') ?? 0
|
||
} else if (activeTrendType.value == 'temp') {
|
||
value = rawData.getNumber('temp') ?? 0
|
||
}
|
||
|
||
if (value > 0) {
|
||
sum += value
|
||
count++
|
||
values.push(value)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (count > 0) {
|
||
const average = sum / count
|
||
const typeLabel = getTrendTypeLabel(activeTrendType.value)
|
||
const timeLabel = getTimeRangeLabel(activeTimeRange.value)
|
||
|
||
// 计算趋势方向
|
||
let trendDirection = '稳定'
|
||
if (values.length > 1) {
|
||
const firstHalf = values.slice(0, Math.floor(values.length / 2))
|
||
const secondHalf = values.slice(Math.floor(values.length / 2))
|
||
|
||
const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length
|
||
const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length
|
||
|
||
const change = ((secondAvg - firstAvg) / firstAvg) * 100
|
||
|
||
if (change > 5) {
|
||
trendDirection = '上升'
|
||
} else if (change < -5) {
|
||
trendDirection = '下降'
|
||
}
|
||
}
|
||
|
||
trendSummary.value = `${timeLabel}内${typeLabel}平均值为${average.toFixed(1)},整体趋势${trendDirection}`
|
||
}
|
||
}
|
||
|
||
function updateTrendChart(data : Array<SensorMeasurement>) {
|
||
const chartData : Array<number> = []
|
||
const chartLabels : Array<string> = []
|
||
|
||
for (let i : Int = 0; i < data.length; i++) {
|
||
const item = data[i]
|
||
const rawData = item.raw_data
|
||
if (rawData !== null) {
|
||
let value : number = 0
|
||
|
||
// 根据类型提取数值
|
||
if (activeTrendType.value == 'heart_rate') {
|
||
value = rawData.getNumber('bpm') ?? 0
|
||
} else if (activeTrendType.value == 'steps') {
|
||
value = rawData.getNumber('count') ?? 0
|
||
} else if (activeTrendType.value == 'spo2') {
|
||
value = rawData.getNumber('spo2') ?? 0
|
||
} else if (activeTrendType.value == 'bp') {
|
||
value = rawData.getNumber('systolic') ?? 0
|
||
} else if (activeTrendType.value == 'temp') {
|
||
value = rawData.getNumber('temp') ?? 0
|
||
}
|
||
|
||
chartData.push(value)
|
||
|
||
// 格式化时间标签
|
||
const timeStr = item.measured_at
|
||
const time = new Date(timeStr)
|
||
let label = ''
|
||
|
||
if (activeTimeRange.value == '24h') {
|
||
label = time.getHours().toString().padStart(2, '0') + ':' +
|
||
time.getMinutes().toString().padStart(2, '0')
|
||
} else {
|
||
label = (time.getMonth() + 1).toString() + '/' + time.getDate().toString()
|
||
}
|
||
|
||
chartLabels.push(label)
|
||
}
|
||
}
|
||
|
||
// 构建图表配置
|
||
const option = new UTSJSONObject()
|
||
option.set('type', 'line')
|
||
option.set('data', chartData)
|
||
option.set('labels', chartLabels)
|
||
option.set('color', getTrendColor(activeTrendType.value))
|
||
option.set('smooth', true)
|
||
|
||
trendChartOption.value = option
|
||
}
|
||
|
||
async function loadTrendData() {
|
||
if (supa == null) return
|
||
|
||
try {
|
||
// 根据时间范围计算开始时间
|
||
const endTime = new Date()
|
||
const startTime = new Date()
|
||
|
||
if (activeTimeRange.value == '24h') {
|
||
startTime.setDate(startTime.getDate() - 1)
|
||
} else if (activeTimeRange.value == '7d') {
|
||
startTime.setDate(startTime.getDate() - 7)
|
||
} else if (activeTimeRange.value == '30d') {
|
||
startTime.setDate(startTime.getDate() - 30)
|
||
} else if (activeTimeRange.value == '90d') {
|
||
startTime.setDate(startTime.getDate() - 90)
|
||
}
|
||
|
||
const result = await supa.from('ss_sensor_measurements')
|
||
.eq('user_id', userId)
|
||
.eq('measurement_type', activeTrendType.value)
|
||
.gte('measured_at', startTime.toISOString())
|
||
.lte('measured_at', endTime.toISOString())
|
||
.order('measured_at', { ascending: true })
|
||
.limit(100)
|
||
.executeAs<Array<SensorMeasurement>>()
|
||
if (result.data !== null) {
|
||
if (Array.isArray(result.data)) {
|
||
const dataArray = result.data as Array<any>
|
||
const trendData : Array<SensorMeasurement> = []
|
||
for (let i = 0; i < dataArray.length; i++) {
|
||
trendData.push(dataArray[i] as SensorMeasurement)
|
||
}
|
||
updateTrendChart(trendData)
|
||
generateTrendSummary(trendData)
|
||
} else {
|
||
updateTrendChart([])
|
||
generateTrendSummary([])
|
||
}
|
||
} else {
|
||
updateTrendChart([])
|
||
generateTrendSummary([])
|
||
}
|
||
} catch (e) {
|
||
console.log('加载趋势数据失败:', e)
|
||
}
|
||
}
|
||
|
||
|
||
async function loadAnomalies() {
|
||
// 模拟异常数据
|
||
const mockAnomalies : Array<HealthAnomaly> = [
|
||
{
|
||
id: '1',
|
||
type: '心率异常',
|
||
severity: 'medium',
|
||
description: '检测到心率持续偏高,建议关注',
|
||
detected_at: new Date(Date.now() - 3600000).toISOString(),
|
||
resolved: false
|
||
},
|
||
{
|
||
id: '2',
|
||
type: '血压波动',
|
||
severity: 'low',
|
||
description: '血压值有轻微波动,持续观察',
|
||
detected_at: new Date(Date.now() - 7200000).toISOString(),
|
||
resolved: true
|
||
}
|
||
]
|
||
|
||
anomalies.value = mockAnomalies
|
||
}
|
||
|
||
async function loadComparisonData() {
|
||
// 模拟对比数据
|
||
const mockComparison : Array<TrendComparison> = [
|
||
{
|
||
current_period: 72,
|
||
previous_period: 75,
|
||
change_percentage: -4.0,
|
||
change_direction: 'down',
|
||
metric_name: '平均心率',
|
||
time_range: activeTimeRange.value
|
||
},
|
||
{
|
||
current_period: 8500,
|
||
previous_period: 7800,
|
||
change_percentage: 9.0,
|
||
change_direction: 'up',
|
||
metric_name: '日均步数',
|
||
time_range: activeTimeRange.value
|
||
}]
|
||
|
||
comparisonData.value = mockComparison
|
||
}
|
||
|
||
async function startAIAnalysis() {
|
||
if (supa == null) return
|
||
|
||
isAnalyzing.value = true
|
||
|
||
try {
|
||
// 调用AI分析RPC
|
||
const analysisParams = new UTSJSONObject()
|
||
analysisParams.set('user_id', userId)
|
||
analysisParams.set('time_range', activeTimeRange.value)
|
||
analysisParams.set('analysis_types', ['health_assessment', 'trend_analysis', 'recommendations'])
|
||
const result = await supa.rpc('generate_health_analysis', analysisParams)
|
||
if (result.data !== null && Array.isArray(result.data)) {
|
||
const dataArray = result.data as Array<any>
|
||
if (dataArray.length > 0) {
|
||
const resultData = dataArray
|
||
const reportData = resultData[0] as UTSJSONObject
|
||
const mockReport : AIAnalysisReport = {
|
||
health_assessment: reportData.getString('health_assessment') ?? '健康评估数据获取失败',
|
||
trend_analysis: reportData.getString('trend_analysis') ?? '趋势分析数据获取失败',
|
||
recommendations: reportData.getArray('recommendations') as string[] ?? [],
|
||
risk_factors: reportData.getArray('risk_factors') as string[] ?? [],
|
||
generated_at: reportData.getString('generated_at') ?? new Date().toISOString()
|
||
|
||
}
|
||
aiReport.value = mockReport
|
||
} else {
|
||
// 模拟AI报告
|
||
const mockReport : AIAnalysisReport = {
|
||
health_assessment: '您的整体健康状况良好,各项指标基本正常。心率变异性较为稳定,运动量适中。',
|
||
trend_analysis: '近期心率呈轻微下降趋势,说明心血管健康状况有所改善。步数波动较大,建议保持规律运动。',
|
||
recommendations: [
|
||
'建议每天保持8000步以上的运动量',
|
||
'注意监测血压变化,定期检查',
|
||
'保持良好的睡眠习惯,每天7-8小时',
|
||
'饮食均衡,减少高盐高脂食物摄入'
|
||
],
|
||
risk_factors: ['血压偏高', '运动不规律'],
|
||
generated_at: new Date().toISOString()
|
||
|
||
}
|
||
aiReport.value = mockReport
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('AI分析失败:', e)
|
||
uni.showToast({
|
||
title: 'AI分析失败',
|
||
icon: 'error'
|
||
})
|
||
} finally {
|
||
isAnalyzing.value = false
|
||
}
|
||
}
|
||
|
||
|
||
async function initializeData() {
|
||
await loadHealthScore()
|
||
await loadTrendData()
|
||
await loadAnomalies()
|
||
await loadComparisonData()
|
||
}
|
||
|
||
function selectTimeRange(range : string) {
|
||
activeTimeRange.value = range
|
||
loadTrendData()
|
||
}
|
||
|
||
function selectTrendType(type : string) {
|
||
activeTrendType.value = type
|
||
loadTrendData()
|
||
}
|
||
function showComparisonPicker() {
|
||
uni.showActionSheet({
|
||
itemList: comparisonPeriods,
|
||
success: (res) => {
|
||
if (res.tapIndex >= 0) {
|
||
comparisonIndex.value = res.tapIndex
|
||
loadComparisonData()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function refreshAnalysis() {
|
||
initializeData()
|
||
}
|
||
onMounted(() => {
|
||
initializeData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.analysis-container {
|
||
flex: 1;
|
||
padding: 20rpx;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
padding: 20rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.header-actions {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.export-btn,
|
||
.refresh-btn {
|
||
padding: 16rpx 24rpx;
|
||
margin-left: 16rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
border: none;
|
||
}
|
||
|
||
.export-btn {
|
||
background-color: #67C23A;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.refresh-btn {
|
||
background-color: #409EFF;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.time-range-card,
|
||
.health-score-card,
|
||
.trends-card,
|
||
.anomaly-card,
|
||
.ai-report-card,
|
||
.comparison-card {
|
||
margin-bottom: 20rpx;
|
||
padding: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.time-range-tabs,
|
||
.trend-tabs {
|
||
flex-direction: row;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
.time-tab,
|
||
.trend-tab {
|
||
flex: 1;
|
||
padding: 16rpx;
|
||
margin: 0 8rpx;
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
border-radius: 8rpx;
|
||
font-size: 26rpx;
|
||
border: none;
|
||
text-align: center;
|
||
}
|
||
|
||
.time-tab.active,
|
||
.trend-tab.active {
|
||
background-color: #409EFF;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.score-display {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.score-circle {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: 100rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-right: 40rpx;
|
||
}
|
||
|
||
.score-value {
|
||
font-size: 64rpx;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 24rpx;
|
||
color: #ffffff;
|
||
margin-top: -8rpx;
|
||
}
|
||
|
||
.score-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.score-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.score-category {
|
||
width: 100rpx;
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
}
|
||
|
||
.score-bar {
|
||
flex: 1;
|
||
height: 16rpx;
|
||
background-color: #f0f0f0;
|
||
border-radius: 8rpx;
|
||
margin: 0 16rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.score-progress {
|
||
height: 100%;
|
||
background-color: #409EFF;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.score-number {
|
||
width: 60rpx;
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
text-align: right;
|
||
}
|
||
|
||
.trend-chart {
|
||
height: 400rpx;
|
||
margin: 20rpx 0;
|
||
}
|
||
|
||
.trend-summary {
|
||
padding: 20rpx;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.trend-text {
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.anomaly-list {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.anomaly-item {
|
||
padding: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
background-color: #fff5f5;
|
||
border-radius: 8rpx;
|
||
border-left: 8rpx solid #f56c6c;
|
||
}
|
||
|
||
.anomaly-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.anomaly-type {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.anomaly-severity {
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.severity-high {
|
||
background-color: #fde2e2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.severity-medium {
|
||
background-color: #fef3c7;
|
||
color: #d97706;
|
||
}
|
||
|
||
.severity-low {
|
||
background-color: #dcfce7;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.anomaly-description {
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.anomaly-time {
|
||
font-size: 22rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.report-status {
|
||
align-items: center;
|
||
padding: 40rpx 0;
|
||
}
|
||
|
||
.analyzing-text {
|
||
font-size: 28rpx;
|
||
color: #666666;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.analyzing-indicator {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border: 4rpx solid #f0f0f0;
|
||
border-top: 4rpx solid #409EFF;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.report-content {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.report-section {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.section-content {
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.recommendations {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.recommendation-item {
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.rec-icon {
|
||
font-size: 28rpx;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.rec-text {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.analyze-btn {
|
||
padding: 24rpx 48rpx;
|
||
background-color: #409EFF;
|
||
color: #ffffff;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
border: none;
|
||
align-self: center;
|
||
}
|
||
|
||
.comparison-options {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.picker-view {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.picker-arrow {
|
||
color: #999999;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.comparison-results {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.comparison-item {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.comparison-metric {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.comparison-values {
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.current-value,
|
||
.previous-value {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.change-value {
|
||
font-size: 26rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.change-positive {
|
||
color: #67C23A;
|
||
}
|
||
|
||
.change-negative {
|
||
color: #F56C6C;
|
||
}
|
||
|
||
.change-neutral {
|
||
color: #909399;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style> |