/**
* 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 = `
风险评估结果
Risk Assessment Results
综合风险评估: ${getRiskLevelText(screening.overall_risk)}
Overall Risk Assessment: ${getRiskLevelText(screening.overall_risk, 'en')}
系统置信度: ${(screening.confidence * 100).toFixed(1)}%
System Confidence: ${(screening.confidence * 100).toFixed(1)}%
详细评估结果
Detailed Assessment Results
筛查阶段 (高召回率 - 不漏诊)
Screening Stage (High Recall - No Missed Diagnosis)
SarcoI 筛查
SarcoI Screening
RandomForest 模型
RandomForest Model
风险等级: ${getRiskLevelText(screening.sarcoI_risk)}
Risk Level: ${getRiskLevelText(screening.sarcoI_risk, 'en')}
肌少症特征相似度: ${(screening.sarcoI_probability * 100).toFixed(1)}% (筛查阈值: 50%)
Sarcopenia Feature Similarity: ${(screening.sarcoI_probability * 100).toFixed(1)}% (Screening Threshold: 50%)
Recall: 91.14%
Precision: 43.05%
SarcoII 筛查
SarcoII Screening
CatBoost 模型
CatBoost Model
风险等级: ${getRiskLevelText(screening.sarcoII_risk)}
Risk Level: ${getRiskLevelText(screening.sarcoII_risk, 'en')}
肌少症特征相似度: ${(screening.sarcoII_probability * 100).toFixed(1)}% (筛查阈值: 50%)
Sarcopenia Feature Similarity: ${(screening.sarcoII_probability * 100).toFixed(1)}% (Screening Threshold: 50%)
Precision: 25.48%
Recall: 89.83%
建议阶段 (高精确率 - 减少误诊)
Advisory Stage (High Precision - Reduce Misdiagnosis)
SarcoI 建议
SarcoI Advisory
CatBoost 模型
CatBoost Model
风险等级: ${screening.sarcoI_advisory_risk ? getRiskLevelText(screening.sarcoI_advisory_risk) : '未评估'}
Risk Level: ${screening.sarcoI_advisory_risk ? getRiskLevelText(screening.sarcoI_advisory_risk, 'en') : 'Not Assessed'}
肌少症特征相似度: ${screening.sarcoI_advisory_probability ? (screening.sarcoI_advisory_probability * 100).toFixed(1) + '%' : 'N/A'} (建议阈值: 36%)
Sarcopenia Feature Similarity: ${screening.sarcoI_advisory_probability ? (screening.sarcoI_advisory_probability * 100).toFixed(1) + '%' : 'N/A'} (Advisory Threshold: 36%)
Precision: 高精确率
Precision: High Precision
Recall: DiCE优化
Recall: DiCE Optimized
SarcoII 建议
SarcoII Advisory
RandomForest 模型
RandomForest Model
风险等级: ${screening.sarcoII_advisory_risk ? getRiskLevelText(screening.sarcoII_advisory_risk) : '未评估'}
Risk Level: ${screening.sarcoII_advisory_risk ? getRiskLevelText(screening.sarcoII_advisory_risk, 'en') : 'Not Assessed'}
肌少症特征相似度: ${screening.sarcoII_advisory_probability ? (screening.sarcoII_advisory_probability * 100).toFixed(1) + '%' : 'N/A'} (建议阈值: 52%)
Sarcopenia Feature Similarity: ${screening.sarcoII_advisory_probability ? (screening.sarcoII_advisory_probability * 100).toFixed(1) + '%' : 'N/A'} (Advisory Threshold: 52%)
Precision: 高精确率
Precision: High Precision
Recall: DiCE优化
Recall: DiCE Optimized
${explanation.title}
${explanation.description}
建议:
Recommendation:
${explanation.recommendation}
`;
// 如果有个性化建议,显示建议内容
if (advisory && assessment.needs_advisory) {
html += buildAdvisoryHTML(advisory);
}
// 添加处理时间信息
html += `
评估耗时: ${assessment.total_processing_time.toFixed(2)} 秒
Processing Time: ${assessment.total_processing_time.toFixed(2)} seconds
`;
return html;
}
/**
* 构建建议HTML
*/
function buildAdvisoryHTML(advisory) {
let html = `
个性化建议
Personalized Recommendations
`;
// 优先级行动
if (advisory.priority_actions && advisory.priority_actions.length > 0) {
html += `
优先建议
Priority Recommendations
${advisory.priority_actions.map(action => `- ${action}
`).join('')}
`;
}
// SarcoI建议
if (advisory.sarcoI_recommendations && advisory.sarcoI_recommendations.length > 0) {
html += `
SarcoI相关建议
SarcoI Related Recommendations
${advisory.sarcoI_recommendations.map(rec => buildRecommendationCard(rec)).join('')}
`;
}
// SarcoII建议
if (advisory.sarcoII_recommendations && advisory.sarcoII_recommendations.length > 0) {
html += `
SarcoII相关建议
SarcoII Related Recommendations
${advisory.sarcoII_recommendations.map(rec => buildRecommendationCard(rec)).join('')}
`;
}
// 目标指标
if (advisory.target_metrics && Object.keys(advisory.target_metrics).length > 0) {
html += `
目标指标
Target Metrics
${Object.entries(advisory.target_metrics).map(([key, value]) => `
`).join('')}
`;
}
// 降级提示
if (advisory.fallback_used) {
html += `
部分建议基于规则生成,建议咨询专业医生获取更详细的个性化指导。
Some recommendations are rule-based. Please consult professional doctors for more detailed personalized guidance.
`;
}
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 `
${recommendation.title}
${recommendation.description}
${recommendation.target_change ? `
${recommendation.target_change}
` : ''}
${getPriorityText(recommendation.priority, 'zh')}优先级
${getPriorityText(recommendation.priority, 'en')} Priority
${recommendation.expected_impact ? `
${recommendation.expected_impact}
` : ''}
`;
}
/**
* 获取风险等级文本
*/
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 = `
${message}
`;
// 在表单上方插入警告
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 = `${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
};