Initial commit of akmon project
This commit is contained in:
963
pages/sense/analysis.uvue
Normal file
963
pages/sense/analysis.uvue
Normal file
@@ -0,0 +1,963 @@
|
||||
<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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user