Spaces:
Sleeping
Sleeping
| /** | |
| * SarcoAdvisor-BSU 前端交互脚本 | |
| * 处理用户输入、API调用和结果显示 | |
| */ | |
| // 全局变量 | |
| let currentAssessment = null; | |
| let currentLanguage = 'zh'; // 默认中文 | |
| // DOM加载完成后初始化 | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('SarcoAdvisor-BSU 前端已加载'); | |
| // 初始化表单 | |
| initializeForm(); | |
| // 检查系统健康状态 | |
| checkSystemHealth(); | |
| // 添加输入验证 | |
| addInputValidation(); | |
| // 初始化语言 | |
| initializeLanguage(); | |
| // 初始化自动计算 | |
| initializeAutoCalculation(); | |
| // 初始化PAQ问卷逻辑 | |
| initializePAQLogic(); | |
| }); | |
| /** | |
| * 初始化表单 | |
| */ | |
| function initializeForm() { | |
| const form = document.getElementById('assessmentForm'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| // 添加表单提交事件监听 | |
| form.addEventListener('submit', handleFormSubmit); | |
| // 添加实时验证 | |
| const inputs = form.querySelectorAll('input[required], select[required]'); | |
| inputs.forEach(input => { | |
| input.addEventListener('blur', validateInput); | |
| input.addEventListener('input', updateSubmitButton); | |
| }); | |
| console.log('表单初始化完成'); | |
| } | |
| /** | |
| * 检查系统健康状态 | |
| */ | |
| async function checkSystemHealth() { | |
| try { | |
| const response = await fetch('/health'); | |
| const health = await response.json(); | |
| if (health.status === 'healthy') { | |
| console.log('系统状态正常:', health); | |
| } else { | |
| console.warn('系统状态异常:', health); | |
| const warningMsg = currentLanguage === 'zh' ? | |
| '系统部分功能可能不可用,但基础评估功能正常' : | |
| 'Some system functions may be unavailable, but basic assessment functions are normal'; | |
| showAlert(warningMsg, 'warning'); | |
| } | |
| } catch (error) { | |
| console.error('健康检查失败:', error); | |
| const errorMsg = currentLanguage === 'zh' ? | |
| '无法连接到服务器,请稍后重试' : | |
| 'Unable to connect to server, please try again later'; | |
| showAlert(errorMsg, 'danger'); | |
| } | |
| } | |
| /** | |
| * 处理表单提交 | |
| */ | |
| async function handleFormSubmit(event) { | |
| event.preventDefault(); | |
| console.log('开始处理表单提交'); | |
| // 验证表单 | |
| if (!validateForm()) { | |
| return; | |
| } | |
| // 收集表单数据 | |
| const formData = collectFormData(); | |
| // 显示加载状态 | |
| showLoading(); | |
| try { | |
| // 调用完整评估API | |
| const result = await performFullAssessment(formData); | |
| // 显示结果 | |
| displayResults(result); | |
| // 保存当前评估 | |
| currentAssessment = result; | |
| } catch (error) { | |
| console.error('评估失败:', error); | |
| hideLoading(); | |
| // 显示详细错误信息 | |
| let errorMessage = currentLanguage === 'zh' ? '评估过程中出现错误' : 'An error occurred during assessment'; | |
| if (error.message) { | |
| errorMessage += currentLanguage === 'zh' ? ':' + error.message : ': ' + error.message; | |
| } | |
| errorMessage += currentLanguage === 'zh' ? '。请检查输入数据后重试。' : '. Please check your input data and try again.'; | |
| showAlert(errorMessage, 'danger'); | |
| // 同时在控制台输出用户数据用于调试 | |
| console.error('失败的用户数据:', formData); | |
| } | |
| } | |
| /** | |
| * 验证表单 | |
| */ | |
| function validateForm() { | |
| const form = document.getElementById('assessmentForm'); | |
| const inputs = form.querySelectorAll('input[required], select[required]'); | |
| let isValid = true; | |
| inputs.forEach(input => { | |
| if (!validateInput({ target: input })) { | |
| isValid = false; | |
| } | |
| }); | |
| return isValid; | |
| } | |
| /** | |
| * 验证单个输入 | |
| */ | |
| function validateInput(event) { | |
| const input = event.target; | |
| const value = input.value; | |
| const type = input.type; | |
| const min = parseFloat(input.min); | |
| const max = parseFloat(input.max); | |
| let isValid = true; | |
| let errorMessage = ''; | |
| // 必填验证 | |
| if (input.required && !value) { | |
| isValid = false; | |
| errorMessage = currentLanguage === 'zh' ? '此字段为必填项' : 'This field is required'; | |
| } | |
| // 数值范围验证 | |
| if (type === 'number' && value) { | |
| const numValue = parseFloat(value); | |
| if (isNaN(numValue)) { | |
| isValid = false; | |
| errorMessage = currentLanguage === 'zh' ? '请输入有效数字' : 'Please enter a valid number'; | |
| } else if (!isNaN(min) && numValue < min) { | |
| isValid = false; | |
| errorMessage = currentLanguage === 'zh' ? `值不能小于 ${min}` : `Value cannot be less than ${min}`; | |
| } else if (!isNaN(max) && numValue > max) { | |
| isValid = false; | |
| errorMessage = currentLanguage === 'zh' ? `值不能大于 ${max}` : `Value cannot be greater than ${max}`; | |
| } | |
| } | |
| // 显示验证结果 | |
| showInputValidation(input, isValid, errorMessage); | |
| return isValid; | |
| } | |
| /** | |
| * 显示输入验证结果 | |
| */ | |
| function showInputValidation(input, isValid, errorMessage) { | |
| // 移除旧的验证状态 | |
| input.classList.remove('is-valid', 'is-invalid'); | |
| // 移除旧的错误信息 | |
| const oldFeedback = input.parentNode.querySelector('.invalid-feedback'); | |
| if (oldFeedback) { | |
| oldFeedback.remove(); | |
| } | |
| if (!isValid && errorMessage) { | |
| // 添加错误状态 | |
| input.classList.add('is-invalid'); | |
| // 添加错误信息 | |
| const feedback = document.createElement('div'); | |
| feedback.className = 'invalid-feedback'; | |
| feedback.textContent = errorMessage; | |
| input.parentNode.appendChild(feedback); | |
| } else if (input.value) { | |
| // 添加成功状态 | |
| input.classList.add('is-valid'); | |
| } | |
| } | |
| /** | |
| * 更新提交按钮状态 | |
| */ | |
| function updateSubmitButton() { | |
| const form = document.getElementById('assessmentForm'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const requiredInputs = form.querySelectorAll('input[required], select[required]'); | |
| let allValid = true; | |
| requiredInputs.forEach(input => { | |
| if (!input.value || input.classList.contains('is-invalid')) { | |
| allValid = false; | |
| } | |
| }); | |
| submitBtn.disabled = !allValid; | |
| } | |
| /** | |
| * 收集表单数据并计算衍生特征 | |
| */ | |
| function collectFormData() { | |
| const rawData = {}; | |
| const form = document.getElementById('assessmentForm'); | |
| const formElements = form.elements; | |
| // 收集原始表单数据 | |
| for (let element of formElements) { | |
| if (element.name) { | |
| let value = element.value; | |
| // 处理空值 | |
| if (value === '' || value === null || value === undefined) { | |
| // 为必填字段提供默认值 | |
| if (element.hasAttribute('required')) { | |
| if (element.name === 'arthritis' || element.name === 'diabetes') { | |
| value = '0'; // 默认无疾病史 | |
| } else if (element.type === 'select-one' && element.name.startsWith('PAQ')) { | |
| value = '2'; // PAQ问题默认选择"否" | |
| } | |
| } | |
| } | |
| if (value !== '' && value !== null && value !== undefined) { | |
| // 转换数字类型 | |
| if (element.type === 'number') { | |
| value = parseFloat(value); | |
| } else if (element.type === 'select-one') { | |
| value = parseInt(value); | |
| } | |
| rawData[element.name] = value; | |
| } | |
| } | |
| } | |
| // 确保BMI和WWI有值(从自动计算获取) | |
| if (!rawData.body_mass_index) { | |
| const height = parseFloat(document.getElementById('height').value); | |
| const weight = parseFloat(document.getElementById('weight').value); | |
| if (height && weight) { | |
| const heightInMeters = height / 100; | |
| rawData.body_mass_index = parseFloat((weight / (heightInMeters * heightInMeters)).toFixed(1)); | |
| } | |
| } | |
| if (!rawData.WWI) { | |
| const waist = parseFloat(document.getElementById('waist').value); | |
| const weight = parseFloat(document.getElementById('weight').value); | |
| if (waist && weight) { | |
| rawData.WWI = parseFloat((waist / Math.sqrt(weight)).toFixed(2)); | |
| } | |
| } | |
| // 确保所有PAQ字段都有默认值(避免Pydantic验证错误) | |
| const requiredPAQFields = { | |
| // 必需的选择字段 | |
| 'PAQ605': 2, 'PAQ620': 2, 'PAQ635': 2, 'PAQ650': 2, 'PAQ665': 2, | |
| // 可选的天数字段(默认0) | |
| 'PAQ610': 0, 'PAQ625': 0, 'PAQ640': 0, 'PAQ655': 0, 'PAQ670': 0, | |
| // 可选的时长字段(默认0) | |
| 'PAD615': 0, 'PAD630': 0, 'PAD645': 0, 'PAD660': 0, 'PAD675': 0, | |
| // 久坐时间(默认8小时) | |
| 'PAD680': 480, | |
| // 医疗史字段(默认否) | |
| 'arthritis': 0, 'diabetes': 0 | |
| }; | |
| // 为缺失的字段设置默认值 | |
| for (const [field, defaultValue] of Object.entries(requiredPAQFields)) { | |
| if (!(field in rawData)) { | |
| rawData[field] = defaultValue; | |
| } | |
| } | |
| // 使用PAD680作为sedentary_minutes | |
| rawData.sedentary_minutes = rawData.PAD680 || 480; | |
| // 计算衍生特征 | |
| const derivedFeatures = calculateDerivedFeatures(rawData); | |
| // 合并原始数据和衍生特征 | |
| const formData = { ...rawData, ...derivedFeatures }; | |
| console.log('收集的表单数据(含默认值):', formData); | |
| return formData; | |
| } | |
| /** | |
| * 根据NHANES PAQ数据计算衍生特征 | |
| * 完全按照create_pa_derived_features.py的逻辑实现 | |
| */ | |
| function calculateDerivedFeatures(data) { | |
| const derived = {}; | |
| // ===================================================== | |
| // A. 活动总量/剂量特征 (Activity Volume/Dose) | |
| // ===================================================== | |
| // A1. 每周总MET-分钟 (Total MET-minutes/week) | |
| const vigorousWorkMETs = 8.0 * (data.PAD615 || 0) * (data.PAQ610 || 0); | |
| const moderateWorkMETs = 4.0 * (data.PAD630 || 0) * (data.PAQ625 || 0); | |
| const transportationMETs = 4.0 * (data.PAD645 || 0) * (data.PAQ640 || 0); | |
| const vigorousRecMETs = 8.0 * (data.PAD660 || 0) * (data.PAQ655 || 0); | |
| const moderateRecMETs = 4.0 * (data.PAD675 || 0) * (data.PAQ670 || 0); | |
| const totalMETMinutesWeek = vigorousWorkMETs + moderateWorkMETs + transportationMETs + vigorousRecMETs + moderateRecMETs; | |
| // A2. 每周高强度活动总分钟数 (Total Vigorous Minutes/week) | |
| const vigorousWorkMins = (data.PAD615 || 0) * (data.PAQ610 || 0); | |
| const vigorousRecMins = (data.PAD660 || 0) * (data.PAQ655 || 0); | |
| const totalVigorousMinutesWeek = vigorousWorkMins + vigorousRecMins; | |
| // A3. 每周中等强度活动总分钟数 (Total Moderate Minutes/week) | |
| const moderateWorkMins = (data.PAD630 || 0) * (data.PAQ625 || 0); | |
| const transportationMins = (data.PAD645 || 0) * (data.PAQ640 || 0); | |
| const moderateRecMins = (data.PAD675 || 0) * (data.PAQ670 || 0); | |
| const totalModerateMinutesWeek = moderateWorkMins + transportationMins + moderateRecMins; | |
| // ===================================================== | |
| // B. 活动模式/行为特征 (Activity Pattern/Behavior) | |
| // ===================================================== | |
| // B1. 平均每次高强度活动时长 | |
| const totalVigorousDays = (data.PAQ610 || 0) + (data.PAQ655 || 0); | |
| const avgVigorousDurationPerBout = totalVigorousDays > 0 ? totalVigorousMinutesWeek / totalVigorousDays : 0; | |
| // B2. 平均每次中等强度活动时长 | |
| const totalModerateDays = (data.PAQ625 || 0) + (data.PAQ640 || 0) + (data.PAQ670 || 0); | |
| const avgModerateDurationPerBout = totalModerateDays > 0 ? totalModerateMinutesWeek / totalModerateDays : 0; | |
| // B3. 活动多样性指数 | |
| let activityDiversityIndex = 0; | |
| if ((data.PAQ605 || 0) > 0) activityDiversityIndex++; // 工作高强度 | |
| if ((data.PAQ620 || 0) > 0) activityDiversityIndex++; // 工作中等强度 | |
| if ((data.PAQ635 || 0) > 0) activityDiversityIndex++; // 交通活动 | |
| if ((data.PAQ650 || 0) > 0) activityDiversityIndex++; // 休闲高强度 | |
| if ((data.PAQ665 || 0) > 0) activityDiversityIndex++; // 休闲中等强度 | |
| // ===================================================== | |
| // C. 活动比例/构成特征 (Activity Ratio/Composition) | |
| // ===================================================== | |
| // C1. 高强度活动MET-分钟占比 | |
| const vigorousMETTotal = vigorousWorkMETs + vigorousRecMETs; | |
| const vigorousMETRatio = totalMETMinutesWeek > 0 ? vigorousMETTotal / totalMETMinutesWeek : 0; | |
| // C2. 活动/久坐比 | |
| const totalActiveMinutesWeek = totalVigorousMinutesWeek + totalModerateMinutesWeek; | |
| const totalSedentaryMinutesWeek = (data.sedentary_minutes || data.PAD680 || 480) * 7; // 每日久坐 × 7天 | |
| const activitySedentaryRatio = totalSedentaryMinutesWeek > 0 ? totalActiveMinutesWeek / totalSedentaryMinutesWeek : 0; | |
| // ===================================================== | |
| // D. 指南达标特征 (Guideline Adherence) | |
| // ===================================================== | |
| // D1. 每周中等强度等效总分钟数 | |
| const totalModerateEquivalentMinutes = totalModerateMinutesWeek + (2 * totalVigorousMinutesWeek); | |
| // D2. 是否达到WHO体力活动推荐量 | |
| const guidelineAdherenceBinary = totalModerateEquivalentMinutes >= 150 ? 1 : 0; | |
| // D3. 体力活动水平分级 | |
| let activityLevelCategorical; | |
| if (totalModerateEquivalentMinutes >= 300) { | |
| activityLevelCategorical = 3; // 非常活跃 | |
| } else if (totalModerateEquivalentMinutes >= 150) { | |
| activityLevelCategorical = 2; // 活跃 | |
| } else if (totalModerateEquivalentMinutes > 0) { | |
| activityLevelCategorical = 1; // 低度活跃 | |
| } else { | |
| activityLevelCategorical = 0; // 不活跃 | |
| } | |
| // ===================================================== | |
| // 整理最终衍生特征 | |
| // ===================================================== | |
| // A. 活动总量/剂量特征 | |
| derived.Total_MET_minutes_week = parseFloat(totalMETMinutesWeek.toFixed(2)); | |
| derived.Total_Vigorous_Minutes_week = parseFloat(totalVigorousMinutesWeek.toFixed(2)); | |
| derived.Total_Moderate_Minutes_week = parseFloat(totalModerateMinutesWeek.toFixed(2)); | |
| // B. 活动模式/行为特征 | |
| derived.Avg_Vigorous_Duration_Per_Bout = parseFloat(avgVigorousDurationPerBout.toFixed(2)); | |
| derived.Avg_Moderate_Duration_Per_Bout = parseFloat(avgModerateDurationPerBout.toFixed(2)); | |
| derived.Activity_Diversity_Index = activityDiversityIndex; | |
| // C. 活动比例/构成特征 | |
| derived.Vigorous_MET_Ratio = parseFloat(vigorousMETRatio.toFixed(3)); | |
| derived.Activity_Sedentary_Ratio = parseFloat(activitySedentaryRatio.toFixed(3)); | |
| // D. 指南达标特征 | |
| derived.Total_Moderate_Equivalent_Minutes = parseFloat(totalModerateEquivalentMinutes.toFixed(2)); | |
| derived.Guideline_Adherence_Binary = guidelineAdherenceBinary; | |
| derived.Activity_Level_Categorical = activityLevelCategorical; | |
| console.log('NHANES PAQ原始数据:', { | |
| PAQ610: data.PAQ610, PAD615: data.PAD615, | |
| PAQ625: data.PAQ625, PAD630: data.PAD630, | |
| PAQ640: data.PAQ640, PAD645: data.PAD645, | |
| PAQ655: data.PAQ655, PAD660: data.PAD660, | |
| PAQ670: data.PAQ670, PAD675: data.PAD675, | |
| PAD680: data.PAD680 | |
| }); | |
| console.log('计算的中间值:', {vigorousWorkMETs, moderateWorkMETs, transportationMETs, vigorousRecMETs, moderateRecMETs}); | |
| console.log('计算的时间总量:', {totalVigorousMinutesWeek, totalModerateMinutesWeek, totalActiveMinutesWeek}); | |
| console.log('最终衍生特征:', derived); | |
| return derived; | |
| } | |
| /** | |
| * 执行完整评估 | |
| */ | |
| async function performFullAssessment(userData) { | |
| console.log('开始完整评估...'); | |
| const response = await fetch('/api/full_assessment', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(userData) | |
| }); | |
| if (!response.ok) { | |
| let errorMessage = `HTTP ${response.status}: `; | |
| try { | |
| const errorData = await response.json(); | |
| console.error('服务器错误详情:', errorData); | |
| if (response.status === 422 && errorData.detail) { | |
| // 处理数据验证错误 | |
| if (Array.isArray(errorData.detail)) { | |
| const validationErrors = errorData.detail.map(err => | |
| `字段"${err.loc.join('.')}"错误: ${err.msg}` | |
| ).join('; '); | |
| errorMessage += validationErrors; | |
| } else { | |
| errorMessage += JSON.stringify(errorData.detail); | |
| } | |
| } else { | |
| errorMessage += errorData.detail || JSON.stringify(errorData); | |
| } | |
| } catch (parseError) { | |
| errorMessage += response.statusText || '服务器错误'; | |
| } | |
| throw new Error(errorMessage); | |
| } | |
| const result = await response.json(); | |
| console.log('评估结果:', result); | |
| return result; | |
| } | |
| /** | |
| * 显示加载状态 | |
| */ | |
| function showLoading() { | |
| document.getElementById('loadingSection').style.display = 'block'; | |
| document.getElementById('resultsSection').style.display = 'none'; | |
| // 滚动到加载区域 | |
| document.getElementById('loadingSection').scrollIntoView({ | |
| behavior: 'smooth' | |
| }); | |
| } | |
| /** | |
| * 隐藏加载状态 | |
| */ | |
| function hideLoading() { | |
| document.getElementById('loadingSection').style.display = 'none'; | |
| } | |
| /** | |
| * 显示评估结果 | |
| */ | |
| function displayResults(assessment) { | |
| hideLoading(); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const resultsContent = document.getElementById('resultsContent'); | |
| // 构建结果HTML | |
| const html = buildResultsHTML(assessment); | |
| resultsContent.innerHTML = html; | |
| // 显示结果区域 | |
| resultsSection.style.display = 'block'; | |
| resultsSection.classList.add('fade-in-up'); | |
| // 滚动到结果区域 | |
| resultsSection.scrollIntoView({ | |
| behavior: 'smooth' | |
| }); | |
| console.log('结果显示完成'); | |
| } | |
| /** | |
| * 构建结果HTML | |
| */ | |
| function buildResultsHTML(assessment) { | |
| const screening = assessment.screening; | |
| const advisory = assessment.advisory; | |
| const explanation = assessment.risk_explanation; | |
| let html = ` | |
| <!-- 风险评估结果 --> | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <h4 class="text-primary mb-3"> | |
| <i class="fas fa-chart-pie me-2"></i> | |
| <span class="lang-zh">风险评估结果</span> | |
| <span class="lang-en" style="display: none;">Risk Assessment Results</span> | |
| </h4> | |
| </div> | |
| </div> | |
| <div class="row mb-4"> | |
| <!-- 综合风险 --> | |
| <div class="col-md-12 mb-3"> | |
| <div class="risk-level risk-${screening.overall_risk}"> | |
| <i class="fas fa-heartbeat fa-2x mb-2"></i> | |
| <div> | |
| <span class="lang-zh">综合风险评估: ${getRiskLevelText(screening.overall_risk)}</span> | |
| <span class="lang-en" style="display: none;">Overall Risk Assessment: ${getRiskLevelText(screening.overall_risk, 'en')}</span> | |
| </div> | |
| <small> | |
| <span class="lang-zh">系统置信度: ${(screening.confidence * 100).toFixed(1)}%</span> | |
| <span class="lang-en" style="display: none;">System Confidence: ${(screening.confidence * 100).toFixed(1)}%</span> | |
| </small> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 详细模型结果 --> | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <h5 class="text-secondary mb-3"> | |
| <i class="fas fa-chart-bar me-2"></i> | |
| <span class="lang-zh">详细评估结果</span> | |
| <span class="lang-en" style="display: none;">Detailed Assessment Results</span> | |
| </h5> | |
| </div> | |
| </div> | |
| <!-- 筛查模型结果 --> | |
| <div class="row mb-3"> | |
| <div class="col-12"> | |
| <h6 class="text-info"> | |
| <i class="fas fa-search me-1"></i> | |
| <span class="lang-zh">筛查阶段 (高召回率 - 不漏诊)</span> | |
| <span class="lang-en" style="display: none;">Screening Stage (High Recall - No Missed Diagnosis)</span> | |
| </h6> | |
| </div> | |
| </div> | |
| <div class="row mb-4"> | |
| <!-- SarcoI筛查 --> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card border-info bg-light"> | |
| <div class="card-body text-center"> | |
| <div class="risk-level risk-${screening.sarcoI_risk} mb-2"> | |
| <i class="fas fa-user-injured fa-lg mb-1"></i> | |
| <div> | |
| <strong> | |
| <span class="lang-zh">SarcoI 筛查</span> | |
| <span class="lang-en" style="display: none;">SarcoI Screening</span> | |
| </strong> | |
| </div> | |
| <div class="small"> | |
| <span class="lang-zh">RandomForest 模型</span> | |
| <span class="lang-en" style="display: none;">RandomForest Model</span> | |
| </div> | |
| </div> | |
| <div class="text-muted"> | |
| <div> | |
| <span class="lang-zh">风险等级: <strong>${getRiskLevelText(screening.sarcoI_risk)}</strong></span> | |
| <span class="lang-en" style="display: none;">Risk Level: <strong>${getRiskLevelText(screening.sarcoI_risk, 'en')}</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">肌少症特征相似度: <strong>${(screening.sarcoI_probability * 100).toFixed(1)}%</strong> (筛查阈值: 50%)</span> | |
| <span class="lang-en" style="display: none;">Sarcopenia Feature Similarity: <strong>${(screening.sarcoI_probability * 100).toFixed(1)}%</strong> (Screening Threshold: 50%)</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 small text-info"> | |
| <div>Recall: <strong>91.14%</strong></div> | |
| <div>Precision: <strong>43.05%</strong></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SarcoII筛查 --> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card border-info bg-light"> | |
| <div class="card-body text-center"> | |
| <div class="risk-level risk-${screening.sarcoII_risk} mb-2"> | |
| <i class="fas fa-wheelchair fa-lg mb-1"></i> | |
| <div> | |
| <strong> | |
| <span class="lang-zh">SarcoII 筛查</span> | |
| <span class="lang-en" style="display: none;">SarcoII Screening</span> | |
| </strong> | |
| </div> | |
| <div class="small"> | |
| <span class="lang-zh">CatBoost 模型</span> | |
| <span class="lang-en" style="display: none;">CatBoost Model</span> | |
| </div> | |
| </div> | |
| <div class="text-muted"> | |
| <div> | |
| <span class="lang-zh">风险等级: <strong>${getRiskLevelText(screening.sarcoII_risk)}</strong></span> | |
| <span class="lang-en" style="display: none;">Risk Level: <strong>${getRiskLevelText(screening.sarcoII_risk, 'en')}</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">肌少症特征相似度: <strong>${(screening.sarcoII_probability * 100).toFixed(1)}%</strong> (筛查阈值: 50%)</span> | |
| <span class="lang-en" style="display: none;">Sarcopenia Feature Similarity: <strong>${(screening.sarcoII_probability * 100).toFixed(1)}%</strong> (Screening Threshold: 50%)</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 small text-info"> | |
| <div>Precision: <strong>25.48%</strong></div> | |
| <div>Recall: <strong>89.83%</strong></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 建议模型结果 --> | |
| <!-- 建议模型部分 - 始终显示 --> | |
| <div class="row mb-3"> | |
| <div class="col-12"> | |
| <h6 class="text-success"> | |
| <i class="fas fa-lightbulb me-1"></i> | |
| <span class="lang-zh">建议阶段 (高精确率 - 减少误诊)</span> | |
| <span class="lang-en" style="display: none;">Advisory Stage (High Precision - Reduce Misdiagnosis)</span> | |
| </h6> | |
| </div> | |
| </div> | |
| <div class="row mb-4"> | |
| <!-- SarcoI建议 --> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card border-success bg-light"> | |
| <div class="card-body text-center"> | |
| <div class="risk-level risk-${screening.sarcoI_advisory_risk} mb-2"> | |
| <i class="fas fa-user-injured fa-lg mb-1"></i> | |
| <div> | |
| <strong> | |
| <span class="lang-zh">SarcoI 建议</span> | |
| <span class="lang-en" style="display: none;">SarcoI Advisory</span> | |
| </strong> | |
| </div> | |
| <div class="small"> | |
| <span class="lang-zh">CatBoost 模型</span> | |
| <span class="lang-en" style="display: none;">CatBoost Model</span> | |
| </div> | |
| </div> | |
| <div class="text-muted"> | |
| <div> | |
| <span class="lang-zh">风险等级: <strong>${screening.sarcoI_advisory_risk ? getRiskLevelText(screening.sarcoI_advisory_risk) : '未评估'}</strong></span> | |
| <span class="lang-en" style="display: none;">Risk Level: <strong>${screening.sarcoI_advisory_risk ? getRiskLevelText(screening.sarcoI_advisory_risk, 'en') : 'Not Assessed'}</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">肌少症特征相似度: <strong>${screening.sarcoI_advisory_probability ? (screening.sarcoI_advisory_probability * 100).toFixed(1) + '%' : 'N/A'}</strong> (建议阈值: 36%)</span> | |
| <span class="lang-en" style="display: none;">Sarcopenia Feature Similarity: <strong>${screening.sarcoI_advisory_probability ? (screening.sarcoI_advisory_probability * 100).toFixed(1) + '%' : 'N/A'}</strong> (Advisory Threshold: 36%)</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 small text-success"> | |
| <div> | |
| <span class="lang-zh">Precision: <strong>高精确率</strong></span> | |
| <span class="lang-en" style="display: none;">Precision: <strong>High Precision</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">Recall: <strong>DiCE优化</strong></span> | |
| <span class="lang-en" style="display: none;">Recall: <strong>DiCE Optimized</strong></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SarcoII建议 --> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card border-success bg-light"> | |
| <div class="card-body text-center"> | |
| <div class="risk-level risk-${screening.sarcoII_advisory_risk} mb-2"> | |
| <i class="fas fa-wheelchair fa-lg mb-1"></i> | |
| <div> | |
| <strong> | |
| <span class="lang-zh">SarcoII 建议</span> | |
| <span class="lang-en" style="display: none;">SarcoII Advisory</span> | |
| </strong> | |
| </div> | |
| <div class="small"> | |
| <span class="lang-zh">RandomForest 模型</span> | |
| <span class="lang-en" style="display: none;">RandomForest Model</span> | |
| </div> | |
| </div> | |
| <div class="text-muted"> | |
| <div> | |
| <span class="lang-zh">风险等级: <strong>${screening.sarcoII_advisory_risk ? getRiskLevelText(screening.sarcoII_advisory_risk) : '未评估'}</strong></span> | |
| <span class="lang-en" style="display: none;">Risk Level: <strong>${screening.sarcoII_advisory_risk ? getRiskLevelText(screening.sarcoII_advisory_risk, 'en') : 'Not Assessed'}</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">肌少症特征相似度: <strong>${screening.sarcoII_advisory_probability ? (screening.sarcoII_advisory_probability * 100).toFixed(1) + '%' : 'N/A'}</strong> (建议阈值: 52%)</span> | |
| <span class="lang-en" style="display: none;">Sarcopenia Feature Similarity: <strong>${screening.sarcoII_advisory_probability ? (screening.sarcoII_advisory_probability * 100).toFixed(1) + '%' : 'N/A'}</strong> (Advisory Threshold: 52%)</span> | |
| </div> | |
| </div> | |
| <div class="mt-2 small text-success"> | |
| <div> | |
| <span class="lang-zh">Precision: <strong>高精确率</strong></span> | |
| <span class="lang-en" style="display: none;">Precision: <strong>High Precision</strong></span> | |
| </div> | |
| <div> | |
| <span class="lang-zh">Recall: <strong>DiCE优化</strong></span> | |
| <span class="lang-en" style="display: none;">Recall: <strong>DiCE Optimized</strong></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 风险解释 --> | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <div class="alert alert-info"> | |
| <h5 class="alert-heading"> | |
| <i class="fas fa-info-circle me-2"></i>${explanation.title} | |
| </h5> | |
| <p class="mb-2">${explanation.description}</p> | |
| <hr> | |
| <p class="mb-0"> | |
| <strong> | |
| <span class="lang-zh">建议:</span> | |
| <span class="lang-en" style="display: none;">Recommendation:</span> | |
| </strong> ${explanation.recommendation} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // 如果有个性化建议,显示建议内容 | |
| if (advisory && assessment.needs_advisory) { | |
| html += buildAdvisoryHTML(advisory); | |
| } | |
| // 添加处理时间信息 | |
| html += ` | |
| <div class="row mt-4"> | |
| <div class="col-12"> | |
| <small class="text-muted"> | |
| <i class="fas fa-clock me-1"></i> | |
| <span class="lang-zh">评估耗时: ${assessment.total_processing_time.toFixed(2)} 秒</span> | |
| <span class="lang-en" style="display: none;">Processing Time: ${assessment.total_processing_time.toFixed(2)} seconds</span> | |
| </small> | |
| </div> | |
| </div> | |
| `; | |
| return html; | |
| } | |
| /** | |
| * 构建建议HTML | |
| */ | |
| function buildAdvisoryHTML(advisory) { | |
| let html = ` | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <h4 class="text-success mb-3"> | |
| <i class="fas fa-lightbulb me-2"></i> | |
| <span class="lang-zh">个性化建议</span> | |
| <span class="lang-en" style="display: none;">Personalized Recommendations</span> | |
| </h4> | |
| </div> | |
| </div> | |
| `; | |
| // 优先级行动 | |
| if (advisory.priority_actions && advisory.priority_actions.length > 0) { | |
| html += ` | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <div class="alert alert-warning"> | |
| <h6 class="alert-heading"> | |
| <i class="fas fa-exclamation-triangle me-2"></i> | |
| <span class="lang-zh">优先建议</span> | |
| <span class="lang-en" style="display: none;">Priority Recommendations</span> | |
| </h6> | |
| <ul class="mb-0"> | |
| ${advisory.priority_actions.map(action => `<li>${action}</li>`).join('')} | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // SarcoI建议 | |
| if (advisory.sarcoI_recommendations && advisory.sarcoI_recommendations.length > 0) { | |
| html += ` | |
| <div class="row mb-3"> | |
| <div class="col-12"> | |
| <h5 class="text-secondary"> | |
| <i class="fas fa-user-injured me-2"></i> | |
| <span class="lang-zh">SarcoI相关建议</span> | |
| <span class="lang-en" style="display: none;">SarcoI Related Recommendations</span> | |
| </h5> | |
| </div> | |
| </div> | |
| <div class="row mb-4"> | |
| ${advisory.sarcoI_recommendations.map(rec => buildRecommendationCard(rec)).join('')} | |
| </div> | |
| `; | |
| } | |
| // SarcoII建议 | |
| if (advisory.sarcoII_recommendations && advisory.sarcoII_recommendations.length > 0) { | |
| html += ` | |
| <div class="row mb-3"> | |
| <div class="col-12"> | |
| <h5 class="text-secondary"> | |
| <i class="fas fa-wheelchair me-2"></i> | |
| <span class="lang-zh">SarcoII相关建议</span> | |
| <span class="lang-en" style="display: none;">SarcoII Related Recommendations</span> | |
| </h5> | |
| </div> | |
| </div> | |
| <div class="row mb-4"> | |
| ${advisory.sarcoII_recommendations.map(rec => buildRecommendationCard(rec)).join('')} | |
| </div> | |
| `; | |
| } | |
| // 目标指标 | |
| if (advisory.target_metrics && Object.keys(advisory.target_metrics).length > 0) { | |
| html += ` | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <h5 class="text-secondary mb-3"> | |
| <i class="fas fa-target me-2"></i> | |
| <span class="lang-zh">目标指标</span> | |
| <span class="lang-en" style="display: none;">Target Metrics</span> | |
| </h5> | |
| <div class="row"> | |
| ${Object.entries(advisory.target_metrics).map(([key, value]) => ` | |
| <div class="col-md-6 mb-2"> | |
| <div class="d-flex justify-content-between"> | |
| <span class="fw-bold">${key}:</span> | |
| <span class="text-primary">${value}</span> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // 降级提示 | |
| if (advisory.fallback_used) { | |
| html += ` | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <div class="alert alert-info"> | |
| <i class="fas fa-info-circle me-2"></i> | |
| <span class="lang-zh">部分建议基于规则生成,建议咨询专业医生获取更详细的个性化指导。</span> | |
| <span class="lang-en" style="display: none;">Some recommendations are rule-based. Please consult professional doctors for more detailed personalized guidance.</span> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| return html; | |
| } | |
| /** | |
| * 构建单个建议卡片 | |
| */ | |
| function buildRecommendationCard(recommendation) { | |
| // 获取优先级文本 | |
| const getPriorityText = (priority, lang) => { | |
| const priorityTexts = { | |
| 'zh': { | |
| 'High': '高', | |
| 'Medium': '中', | |
| 'Low': '低' | |
| }, | |
| 'en': { | |
| 'High': 'High', | |
| 'Medium': 'Medium', | |
| 'Low': 'Low' | |
| } | |
| }; | |
| return priorityTexts[lang]?.[priority] || priority; | |
| }; | |
| return ` | |
| <div class="col-md-6 mb-3"> | |
| <div class="recommendation-item"> | |
| <div class="recommendation-title"> | |
| <i class="fas fa-arrow-right me-2 text-primary"></i> | |
| ${recommendation.title} | |
| </div> | |
| <div class="recommendation-description"> | |
| ${recommendation.description} | |
| </div> | |
| ${recommendation.target_change ? ` | |
| <div class="text-muted small mb-2"> | |
| <i class="fas fa-target me-1"></i> | |
| ${recommendation.target_change} | |
| </div> | |
| ` : ''} | |
| <div class="recommendation-meta"> | |
| <span class="priority-badge priority-${recommendation.priority.toLowerCase()}"> | |
| <span class="lang-zh">${getPriorityText(recommendation.priority, 'zh')}优先级</span> | |
| <span class="lang-en" style="display: none;">${getPriorityText(recommendation.priority, 'en')} Priority</span> | |
| </span> | |
| ${recommendation.expected_impact ? ` | |
| <small class="text-success"> | |
| <i class="fas fa-chart-line me-1"></i> | |
| ${recommendation.expected_impact} | |
| </small> | |
| ` : ''} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| /** | |
| * 获取风险等级文本 | |
| */ | |
| function getRiskLevelText(level, language = null) { | |
| const lang = language || currentLanguage; | |
| const levels = { | |
| 'zh': { | |
| 'low': '低风险', | |
| 'medium': '中等风险', | |
| 'high': '高风险' | |
| }, | |
| 'en': { | |
| 'low': 'Low Risk', | |
| 'medium': 'Medium Risk', | |
| 'high': 'High Risk' | |
| } | |
| }; | |
| return levels[lang]?.[level] || level; | |
| } | |
| /** | |
| * 显示警告信息 | |
| */ | |
| function showAlert(message, type = 'info') { | |
| const alertHTML = ` | |
| <div class="alert alert-${type} alert-dismissible fade show" role="alert"> | |
| <i class="fas fa-${getAlertIcon(type)} me-2"></i> | |
| ${message} | |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | |
| </div> | |
| `; | |
| // 在表单上方插入警告 | |
| const form = document.getElementById('assessmentForm'); | |
| const alertContainer = document.createElement('div'); | |
| alertContainer.innerHTML = alertHTML; | |
| form.parentNode.insertBefore(alertContainer, form); | |
| // 5秒后自动关闭 | |
| setTimeout(() => { | |
| const alert = alertContainer.querySelector('.alert'); | |
| if (alert) { | |
| alert.remove(); | |
| } | |
| }, 5000); | |
| } | |
| /** | |
| * 获取警告图标 | |
| */ | |
| function getAlertIcon(type) { | |
| const icons = { | |
| 'info': 'info-circle', | |
| 'warning': 'exclamation-triangle', | |
| 'danger': 'exclamation-circle', | |
| 'success': 'check-circle' | |
| }; | |
| return icons[type] || 'info-circle'; | |
| } | |
| /** | |
| * 添加输入验证 | |
| */ | |
| function addInputValidation() { | |
| // 身高合理性检查 | |
| const heightInput = document.getElementById('height'); | |
| if (heightInput) { | |
| heightInput.addEventListener('input', function() { | |
| const height = parseFloat(this.value); | |
| if (height && (height < 120 || height > 220)) { | |
| const message = currentLanguage === 'zh' ? '身高数值请确认是否正确' : 'Please confirm if height value is correct'; | |
| showInputWarning(this, message); | |
| } else { | |
| hideInputWarning(this); | |
| } | |
| }); | |
| } | |
| // 体重合理性检查 | |
| const weightInput = document.getElementById('weight'); | |
| if (weightInput) { | |
| weightInput.addEventListener('input', function() { | |
| const weight = parseFloat(this.value); | |
| if (weight && (weight < 35 || weight > 150)) { | |
| const message = currentLanguage === 'zh' ? '体重数值请确认是否正确' : 'Please confirm if weight value is correct'; | |
| showInputWarning(this, message); | |
| } else { | |
| hideInputWarning(this); | |
| } | |
| }); | |
| } | |
| // 腰围合理性检查 | |
| const waistInput = document.getElementById('waist'); | |
| if (waistInput) { | |
| waistInput.addEventListener('input', function() { | |
| const waist = parseFloat(this.value); | |
| if (waist && (waist < 60 || waist > 150)) { | |
| const message = currentLanguage === 'zh' ? '腰围数值请确认是否正确' : 'Please confirm if waist circumference is correct'; | |
| showInputWarning(this, message); | |
| } else { | |
| hideInputWarning(this); | |
| } | |
| }); | |
| } | |
| // 年龄合理性检查 | |
| const ageInput = document.getElementById('age_years'); | |
| if (ageInput) { | |
| ageInput.addEventListener('input', function() { | |
| const age = parseFloat(this.value); | |
| if (age && age > 80) { | |
| const message = currentLanguage === 'zh' ? '高龄用户建议咨询专业医生' : 'Elderly users are advised to consult professional doctors'; | |
| showInputWarning(this, message); | |
| } else { | |
| hideInputWarning(this); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * 显示输入警告 | |
| */ | |
| function showInputWarning(input, message) { | |
| let warning = input.parentNode.querySelector('.input-warning'); | |
| if (!warning) { | |
| warning = document.createElement('div'); | |
| warning.className = 'input-warning text-warning small mt-1'; | |
| input.parentNode.appendChild(warning); | |
| } | |
| warning.innerHTML = `<i class="fas fa-exclamation-triangle me-1"></i>${message}`; | |
| } | |
| /** | |
| * 隐藏输入警告 | |
| */ | |
| function hideInputWarning(input) { | |
| const warning = input.parentNode.querySelector('.input-warning'); | |
| if (warning) { | |
| warning.remove(); | |
| } | |
| } | |
| /** | |
| * 导出评估结果 (未来功能) | |
| */ | |
| function exportResults() { | |
| if (!currentAssessment) { | |
| const noResultMsg = currentLanguage === 'zh' ? | |
| '没有可导出的评估结果' : | |
| 'No assessment results to export'; | |
| showAlert(noResultMsg, 'warning'); | |
| return; | |
| } | |
| // TODO: 实现结果导出功能 | |
| console.log('导出评估结果:', currentAssessment); | |
| const devMsg = currentLanguage === 'zh' ? | |
| '导出功能正在开发中' : | |
| 'Export function is under development'; | |
| showAlert(devMsg, 'info'); | |
| } | |
| /** | |
| * 初始化语言设置 | |
| */ | |
| function initializeLanguage() { | |
| // 从localStorage读取语言设置 | |
| const savedLang = localStorage.getItem('sarco-language'); | |
| if (savedLang) { | |
| currentLanguage = savedLang; | |
| if (currentLanguage === 'en') { | |
| switchToEnglish(); | |
| } | |
| } | |
| } | |
| /** | |
| * 切换语言 | |
| */ | |
| function toggleLanguage() { | |
| if (currentLanguage === 'zh') { | |
| switchToEnglish(); | |
| } else { | |
| switchToChinese(); | |
| } | |
| } | |
| /** | |
| * 切换到英文 | |
| */ | |
| function switchToEnglish() { | |
| currentLanguage = 'en'; | |
| document.querySelectorAll('.lang-zh').forEach(el => el.style.display = 'none'); | |
| document.querySelectorAll('.lang-en').forEach(el => el.style.display = 'inline'); | |
| document.getElementById('langToggleText').textContent = '中文'; | |
| document.documentElement.lang = 'en'; | |
| document.title = 'SarcoAdvisor-BSU - Sarcopenia Risk Assessment System'; | |
| // 更新select选项 | |
| updateSelectOptions('en'); | |
| // 如果有已显示的结果,重新渲染 | |
| if (currentAssessment) { | |
| displayResults(currentAssessment); | |
| } | |
| // 保存设置 | |
| localStorage.setItem('sarco-language', 'en'); | |
| } | |
| /** | |
| * 切换到中文 | |
| */ | |
| function switchToChinese() { | |
| currentLanguage = 'zh'; | |
| document.querySelectorAll('.lang-en').forEach(el => el.style.display = 'none'); | |
| document.querySelectorAll('.lang-zh').forEach(el => el.style.display = 'inline'); | |
| document.getElementById('langToggleText').textContent = 'English'; | |
| document.documentElement.lang = 'zh-CN'; | |
| document.title = 'SarcoAdvisor-BSU - 肌少症风险评估系统'; | |
| // 更新select选项 | |
| updateSelectOptions('zh'); | |
| // 如果有已显示的结果,重新渲染 | |
| if (currentAssessment) { | |
| displayResults(currentAssessment); | |
| } | |
| // 保存设置 | |
| localStorage.setItem('sarco-language', 'zh'); | |
| } | |
| /** | |
| * 更新select选项的显示 | |
| */ | |
| function updateSelectOptions(lang) { | |
| const select = document.getElementById('race_ethnicity'); | |
| const options = select.querySelectorAll('option'); | |
| options.forEach(option => { | |
| if (option.value === '') { | |
| option.textContent = lang === 'zh' ? '请选择' : 'Please select'; | |
| } else if (option.value === '0') { | |
| option.textContent = lang === 'zh' ? '美洲原住民' : 'Native American'; | |
| } else if (option.value === '1') { | |
| option.textContent = lang === 'zh' ? '亚洲人' : 'Asian'; | |
| } else if (option.value === '2') { | |
| option.textContent = lang === 'zh' ? '非洲裔美国人' : 'African American'; | |
| } else if (option.value === '3') { | |
| option.textContent = lang === 'zh' ? '西班牙裔' : 'Hispanic'; | |
| } else if (option.value === '4') { | |
| option.textContent = lang === 'zh' ? '白人' : 'White'; | |
| } | |
| }); | |
| } | |
| /** | |
| * 初始化自动计算功能 | |
| */ | |
| function initializeAutoCalculation() { | |
| // 获取输入元素 | |
| const heightInput = document.getElementById('height'); | |
| const weightInput = document.getElementById('weight'); | |
| const waistInput = document.getElementById('waist'); | |
| const bmiInput = document.getElementById('body_mass_index'); | |
| const wwiInput = document.getElementById('WWI'); | |
| // 添加事件监听器 | |
| if (heightInput && weightInput && waistInput) { | |
| heightInput.addEventListener('input', calculateBMIAndWWI); | |
| weightInput.addEventListener('input', calculateBMIAndWWI); | |
| waistInput.addEventListener('input', calculateBMIAndWWI); | |
| console.log('自动计算功能已初始化'); | |
| } else { | |
| console.warn('未找到身体测量输入字段'); | |
| } | |
| } | |
| /** | |
| * 计算BMI和WWI | |
| */ | |
| function calculateBMIAndWWI() { | |
| const height = parseFloat(document.getElementById('height').value); | |
| const weight = parseFloat(document.getElementById('weight').value); | |
| const waist = parseFloat(document.getElementById('waist').value); | |
| const bmiInput = document.getElementById('body_mass_index'); | |
| const wwiInput = document.getElementById('WWI'); | |
| // 计算BMI | |
| if (height && weight && height > 0) { | |
| const heightInMeters = height / 100; // 转换为米 | |
| const bmi = weight / (heightInMeters * heightInMeters); | |
| bmiInput.value = bmi.toFixed(1); | |
| // 添加BMI颜色指示 | |
| updateBMIStatus(bmi); | |
| } else { | |
| bmiInput.value = ''; | |
| bmiInput.className = 'form-control bg-light'; | |
| } | |
| // 计算WWI | |
| if (waist && weight && weight > 0) { | |
| const wwi = waist / Math.sqrt(weight); | |
| wwiInput.value = wwi.toFixed(2); | |
| // 添加WWI颜色指示 | |
| updateWWIStatus(wwi); | |
| } else { | |
| wwiInput.value = ''; | |
| wwiInput.className = 'form-control bg-light'; | |
| } | |
| // 触发表单验证更新 | |
| updateSubmitButton(); | |
| } | |
| /** | |
| * 更新BMI状态指示 | |
| */ | |
| function updateBMIStatus(bmi) { | |
| const bmiInput = document.getElementById('body_mass_index'); | |
| // 移除旧的状态类 | |
| bmiInput.className = 'form-control bg-light'; | |
| if (bmi < 18.5) { | |
| bmiInput.classList.add('border-info'); // 偏瘦 | |
| } else if (bmi >= 18.5 && bmi < 24) { | |
| bmiInput.classList.add('border-success'); // 正常 | |
| } else if (bmi >= 24 && bmi < 28) { | |
| bmiInput.classList.add('border-warning'); // 超重 | |
| } else if (bmi >= 28) { | |
| bmiInput.classList.add('border-danger'); // 肥胖 | |
| } | |
| } | |
| /** | |
| * 更新WWI状态指示 | |
| */ | |
| function updateWWIStatus(wwi) { | |
| const wwiInput = document.getElementById('WWI'); | |
| // 移除旧的状态类 | |
| wwiInput.className = 'form-control bg-light'; | |
| // WWI正常范围大约在9-12之间 | |
| if (wwi < 9) { | |
| wwiInput.classList.add('border-info'); // 较低 | |
| } else if (wwi >= 9 && wwi <= 12) { | |
| wwiInput.classList.add('border-success'); // 正常 | |
| } else if (wwi > 12 && wwi <= 14) { | |
| wwiInput.classList.add('border-warning'); // 较高 | |
| } else if (wwi > 14) { | |
| wwiInput.classList.add('border-danger'); // 很高 | |
| } | |
| } | |
| /** | |
| * 初始化PAQ问卷条件显示逻辑 | |
| */ | |
| function initializePAQLogic() { | |
| // 工作高强度活动条件显示 | |
| const paq605 = document.getElementById('PAQ605'); | |
| const vigorousWorkDetails = document.getElementById('vigorous_work_details'); | |
| if (paq605 && vigorousWorkDetails) { | |
| paq605.addEventListener('change', function() { | |
| if (this.value === '1') { | |
| vigorousWorkDetails.style.display = 'block'; | |
| document.getElementById('PAQ610').required = true; | |
| document.getElementById('PAD615').required = true; | |
| } else { | |
| vigorousWorkDetails.style.display = 'none'; | |
| document.getElementById('PAQ610').required = false; | |
| document.getElementById('PAD615').required = false; | |
| document.getElementById('PAQ610').value = ''; | |
| document.getElementById('PAD615').value = ''; | |
| } | |
| }); | |
| } | |
| // 工作中等强度活动条件显示 | |
| const paq620 = document.getElementById('PAQ620'); | |
| const moderateWorkDetails = document.getElementById('moderate_work_details'); | |
| if (paq620 && moderateWorkDetails) { | |
| paq620.addEventListener('change', function() { | |
| if (this.value === '1') { | |
| moderateWorkDetails.style.display = 'block'; | |
| document.getElementById('PAQ625').required = true; | |
| document.getElementById('PAD630').required = true; | |
| } else { | |
| moderateWorkDetails.style.display = 'none'; | |
| document.getElementById('PAQ625').required = false; | |
| document.getElementById('PAD630').required = false; | |
| document.getElementById('PAQ625').value = ''; | |
| document.getElementById('PAD630').value = ''; | |
| } | |
| }); | |
| } | |
| // 交通活动条件显示 | |
| const paq635 = document.getElementById('PAQ635'); | |
| const transportDetails = document.getElementById('transport_details'); | |
| if (paq635 && transportDetails) { | |
| paq635.addEventListener('change', function() { | |
| if (this.value === '1') { | |
| transportDetails.style.display = 'block'; | |
| document.getElementById('PAQ640').required = true; | |
| document.getElementById('PAD645').required = true; | |
| } else { | |
| transportDetails.style.display = 'none'; | |
| document.getElementById('PAQ640').required = false; | |
| document.getElementById('PAD645').required = false; | |
| document.getElementById('PAQ640').value = ''; | |
| document.getElementById('PAD645').value = ''; | |
| } | |
| }); | |
| } | |
| // 休闲高强度活动条件显示 | |
| const paq650 = document.getElementById('PAQ650'); | |
| const vigorousRecDetails = document.getElementById('vigorous_rec_details'); | |
| if (paq650 && vigorousRecDetails) { | |
| paq650.addEventListener('change', function() { | |
| if (this.value === '1') { | |
| vigorousRecDetails.style.display = 'block'; | |
| document.getElementById('PAQ655').required = true; | |
| document.getElementById('PAD660').required = true; | |
| } else { | |
| vigorousRecDetails.style.display = 'none'; | |
| document.getElementById('PAQ655').required = false; | |
| document.getElementById('PAD660').required = false; | |
| document.getElementById('PAQ655').value = ''; | |
| document.getElementById('PAD660').value = ''; | |
| } | |
| }); | |
| } | |
| // 休闲中等强度活动条件显示 | |
| const paq665 = document.getElementById('PAQ665'); | |
| const moderateRecDetails = document.getElementById('moderate_rec_details'); | |
| if (paq665 && moderateRecDetails) { | |
| paq665.addEventListener('change', function() { | |
| if (this.value === '1') { | |
| moderateRecDetails.style.display = 'block'; | |
| document.getElementById('PAQ670').required = true; | |
| document.getElementById('PAD675').required = true; | |
| } else { | |
| moderateRecDetails.style.display = 'none'; | |
| document.getElementById('PAQ670').required = false; | |
| document.getElementById('PAD675').required = false; | |
| document.getElementById('PAQ670').value = ''; | |
| document.getElementById('PAD675').value = ''; | |
| } | |
| }); | |
| } | |
| console.log('PAQ问卷条件显示逻辑已初始化'); | |
| } | |
| // 导出到全局作用域 | |
| window.SarcoAdvisor = { | |
| exportResults, | |
| showAlert, | |
| getCurrentAssessment: () => currentAssessment, | |
| toggleLanguage | |
| }; |