/**
* 智能垃圾分类系统 - 高级交互 v3
* Minimalist premium design with haptic feedback + object identification
*/
(function () {
'use strict';
// ============================================
// 工具函数
// ============================================
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
// 触觉反馈
function haptic(pattern = 10) {
if (navigator.vibrate) {
navigator.vibrate(pattern);
}
}
// 涟漪效果
function createRipple(e, element) {
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left - size / 2;
const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
const container = $('#rippleContainer') || document.body;
container.appendChild(ripple);
ripple.addEventListener('animationend', () => ripple.remove());
}
// 为所有带 data-ripple 的元素添加点击效果
function initRipples() {
$$('[data-ripple]').forEach(el => {
el.addEventListener('click', function (e) {
haptic();
createRipple(e, this);
});
el.addEventListener('touchstart', function (e) {
haptic(8);
}, { passive: true });
});
}
// 鼠标跟踪光晕
function initGlowTracking() {
const placeholder = $('#uploadPlaceholder');
if (!placeholder) return;
placeholder.addEventListener('mousemove', (e) => {
const rect = placeholder.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
placeholder.style.setProperty('--mx', x + '%');
placeholder.style.setProperty('--my', y + '%');
});
}
// 物品识别 emoji 图标
function getObjectEmoji(name) {
if (!name) return '📦';
const emojiMap = [
['水瓶', '🦴'], ['玻璃杯', '🥛'], ['咖啡杯', '☕'], ['茶杯', '🛖'],
['易拉罐', '🥛'], ['保温杯', '🧢'], ['啤酒瓶', '🍺'],
['塑料杯', '🥤'], ['纸杯', '🥤'],
['盘子', '🍽️'], ['碗', '🥣'], ['筷子', '🥢'], ['叉子', '🍴'],
['勺子', '🥄'], ['刀', '🔪'],
['外卖盒', '📦'], ['食物容器', '🍱'],
['苹果', '🍎'], ['香蕉', '🍌'], ['橙子', '🍊'],
['面包', '🍞'], ['蛋糕', '🎂'], ['饼干', '🍪'],
['鸡蛋', '🥚'], ['米饭', '🍚'], ['面条', '🍜'],
['三明治', '🥪'], ['披萨', '🍕'],
['巧克力', '🍫'], ['水果', '🍉'], ['蔬菜', '🥬'],
['西瓜', '🍉'], ['葡萄', '🍇'], ['草莓', '🍓'],
['薯片', '🥨'], ['泡面', '🍜'],
['手机', '📱'], ['笔记本', '💻'], ['平板', '📱'],
['耳机', '🎧'], ['充电线', '🔌'], ['遥控器', '📟'],
['键盘', '⌨️'], ['鼠标', '🖱️'], ['相机', '📷'],
['充电宝', '🔋'],
['T恤', '👕'], ['裤子', '👖'], ['鞋子', '👟'],
['帽子', '🧢'], ['外套', '🧥'], ['袜子', '🧦'],
['背包', '🎒'], ['手提包', '👜'], ['钱包', '👛'],
['手套', '🧤'], ['围巾', '🧣'], ['墨镜', '🕶️'],
['书本', '📚'], ['笔记本', '📓'], ['笔', '✏️'],
['铅笔', '✏️'], ['剪刀', '✂️'],
['报纸', '📰'], ['杂志', '📰'], ['纸张', '📄'],
['纸箱', '📦'], ['信封', '✉️'],
['钥匙', '🔑'], ['雨伞', '☂️'], ['眼镜', '👓'],
['牙刷', '🦷'], ['毛巾', '🦴'], ['肥皂', '🧼'],
['洗发水', '🦴'], ['纸巾盒', '📦'],
['垃圾袋', '🗑️'], ['塑料袋', '🛍️'],
['枕头', '🛏️'], ['毯子', '🛌'], ['靠垫', '🛋️'],
['玻璃罐', '🥙'], ['金属罐', '🥛'], ['牛奶盒', '🥛'],
['塑料瓶', '🦴'],
['锤子', '🔨'], ['螺丝刀', '🪛'], ['扳手', '🔧'],
['钳子', '🔧'], ['手电筒', '🔦'],
['足球', '⚽'], ['篮球', '🏀'], ['网球', '🎾'],
['棒球', '⚾'], ['飞盘', '🥏'],
['口罩', '😷'], ['烟蒂', '🚬'],
['电池', '🔋'], ['灯泡', '💡'], ['药片', '💊'],
['尿不湿', '👶'], ['湿巾', '🧻'],
['吸管', '🥤'], ['牙签', '🦷'], ['棉签', '🦷'],
['创可贴', '🪹'],
['花', '🌸'], ['盆栽', '🪴'],
['玩具', '🧸'], ['毛绒', '🧸'],
['口红', '💄'], ['化妆刷', '🖌️'], ['指甲油', '💅'],
['梳子', '🪮'], ['剃须刀', '🪒'],
['海绵', '🧽'], ['抹布', '🧹'], ['扫把', '🧹'],
['洗衣液', '🦴'],
['泡面桶', '🍜'], ['一次性筷子', '🥢'],
['纸碗', '🥣'], ['泡沫饭盒', '📦'],
];
for (const [key, emoji] of emojiMap) {
if (name.includes(key)) return emoji;
}
return '📦';
}
// ============================================
// DOM 元素
// ============================================
const uploadArea = $('#uploadArea');
const uploadPlaceholder = $('#uploadPlaceholder');
const uploadPreview = $('#uploadPreview');
const previewImage = $('#previewImage');
const fileInput = $('#fileInput');
const fileInputCamera = $('#fileInputCamera');
const btnCamera = $('#btnCamera');
const btnGallery = $('#btnGallery');
const btnUpload = $('#btnUpload');
const btnRemove = $('#btnRemove');
const btnAgain = $('#btnAgain');
const btnErrorRetry = $('#btnErrorRetry');
const uploadSection = $('#uploadSection');
const loadingSection = $('#loadingSection');
const resultSection = $('#resultSection');
const errorSection = $('#errorSection');
const resultCard = $('#resultCard');
const resultAccent = $('#resultAccent');
const badgeIcon = $('#badgeIcon');
const badgeText = $('#badgeText');
const binLabel = $('#binLabel');
const binDesc = $('#binDesc');
const examplesText = $('#examplesText');
const tipText = $('#tipText');
const binVisual = $('#binVisual');
const detectedItem = $('#detectedItem');
const detectedConfidence = $('#detectedConfidence');
const otherPredictions = $('#otherPredictions');
const predictionList = $('#predictionList');
const uploadActions = $('#uploadActions');
// DNA 知识卡片 DOM
const dnaSection = $('#dnaSection');
const dnaCard = $('#dnaCard');
const dnaMaterial = $('#dnaMaterial');
const dnaDecomposition = $('#dnaDecomposition');
const dnaDecompositionDesc = $('#dnaDecompositionDesc');
const dnaRecycledInto = $('#dnaRecycledInto');
const dnaProcess = $('#dnaProcess');
const dnaCo2Row = $('#dnaCo2Row');
const dnaCo2Text = $('#dnaCo2Text');
const dnaTips = $('#dnaTips');
const dnaFacts = $('#dnaFacts');
// 物品识别 DOM
const identifySection = $('#identifySection');
const objectName = $('#objectName');
const objectConfidence = $('#objectConfidence');
const objectIcon = $('#objectIcon');
const errorTitle = $('#errorTitle');
const errorDetail = $('#errorDetail');
// ============================================
// 状态
// ============================================
let selectedFile = null;
let isProcessing = false;
let currentMode = 'single'; // 'single' | 'multi'
let multiObjects = []; // 多物体结果缓存
let selectedMultiIndex = 0;
// ============================================
// 映射
// ============================================
const BIN_COLORS = {
'可回收垃圾': { class: 'bg-blue', color: '#5b9cf5', label: '蓝色垃圾桶' },
'有害垃圾': { class: 'bg-red', color: '#f56b6b', label: '红色垃圾桶' },
'厨余垃圾': { class: 'bg-green', color: '#5bd68a', label: '绿色垃圾桶' },
'其他垃圾': { class: 'bg-gray', color: '#8a8a9a', label: '灰色/黑色垃圾桶' },
};
const BIN_ICONS = {
'可回收垃圾': '♻️',
'有害垃圾': '☣️',
'厨余垃圾': '🍚',
'其他垃圾': '🗑️',
};
// ============================================
// 页面切换
// ============================================
function showSection(section) {
[uploadSection, loadingSection, resultSection, errorSection].forEach(s => {
s.style.display = 'none';
});
section.style.display = 'flex';
section.style.animation = 'none';
void section.offsetWidth;
section.style.animation = '';
}
// ============================================
// 文件选择
// ============================================
function handleFileSelect(file) {
if (!file) return;
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/bmp', 'image/gif'];
if (!validTypes.includes(file.type) && !file.type.startsWith('image/')) {
showError('不支持的文件格式', '请选择 JPG、PNG 或 WebP 格式的图片');
haptic(20);
return;
}
if (file.size > 10 * 1024 * 1024) {
showError('图片太大', `文件 ${(file.size / 1024 / 1024).toFixed(1)}MB,请选择小于 10MB 的图片`);
haptic(20);
return;
}
selectedFile = file;
btnUpload.disabled = false;
haptic(12);
const reader = new FileReader();
reader.onload = function (e) {
previewImage.src = e.target.result;
uploadPlaceholder.style.display = 'none';
uploadPreview.style.display = 'block';
if (uploadActions) uploadActions.style.display = 'none';
};
reader.readAsDataURL(file);
}
uploadPlaceholder.addEventListener('click', () => {
haptic(6);
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
handleFileSelect(e.target.files[0]);
}
});
if (btnCamera) {
btnCamera.addEventListener('click', () => {
haptic(10);
fileInputCamera.click();
});
}
if (btnGallery) {
btnGallery.addEventListener('click', () => {
haptic(10);
fileInput.click();
});
}
if (fileInputCamera) {
fileInputCamera.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
handleFileSelect(e.target.files[0]);
}
});
}
btnRemove.addEventListener('click', (e) => {
e.stopPropagation();
haptic(6);
resetUpload();
});
// ============================================
// 模式切换 (单物体 / 多物体)
// ============================================
const modeSingle = $('#modeSingle');
const modeMulti = $('#modeMulti');
const multiResultSection = $('#multiResultSection');
const multiResultImage = $('#multiResultImage');
const multiBboxCanvas = $('#multiBboxCanvas');
function switchMode(mode) {
currentMode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.toggle('mode-btn--active', btn.dataset.mode === mode);
});
haptic(6);
}
if (modeSingle) {
modeSingle.addEventListener('click', () => switchMode('single'));
}
if (modeMulti) {
modeMulti.addEventListener('click', () => switchMode('multi'));
}
function resetUpload() {
selectedFile = null;
btnUpload.disabled = true;
fileInput.value = '';
fileInputCamera.value = '';
previewImage.src = '';
uploadPreview.style.display = 'none';
uploadPlaceholder.style.display = 'flex';
if (uploadActions) uploadActions.style.display = 'grid';
}
// ============================================
// 拖放
// ============================================
let dragCounter = 0;
uploadArea.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter++;
uploadPlaceholder.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter--;
if (dragCounter === 0) {
uploadPlaceholder.classList.remove('dragover');
}
});
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter = 0;
uploadPlaceholder.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files && files[0]) {
handleFileSelect(files[0]);
}
});
// ============================================
// 分类请求
// ============================================
async function classifyImage() {
if (!selectedFile || isProcessing) return;
isProcessing = true;
btnUpload.disabled = true;
haptic(15);
showSection(loadingSection);
try {
const formData = new FormData();
formData.append('file', selectedFile);
// 根据模式选择端点
const endpoint = currentMode === 'multi' ? '/api/classify-multi' : '/api/classify';
const response = await fetch(endpoint, {
method: 'POST',
body: formData,
});
if (!response.ok) {
let detail = '服务器处理出错';
try {
const errData = await response.json();
detail = errData.detail || detail;
} catch (e) { }
throw new Error(detail);
}
const result = await response.json();
await new Promise(r => setTimeout(r, 600));
if (currentMode === 'multi' && result.multi) {
showMultiResult(result);
} else {
showResult(result);
}
} catch (err) {
console.error('分类出错:', err);
await new Promise(r => setTimeout(r, 400));
showError('识别失败', err.message || '网络错误,请检查连接后重试');
} finally {
isProcessing = false;
btnUpload.disabled = !selectedFile;
}
}
btnUpload.addEventListener('click', classifyImage);
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && selectedFile && !isProcessing) {
const visible = uploadSection.style.display !== 'none';
if (visible) classifyImage();
}
});
// ============================================
// 展示结果
// ============================================
function showResult(result) {
haptic(20);
if (!result.success) {
showError('识别失败', '模型未能识别该图片中的垃圾类型,请换一张照片试试');
return;
}
const category = result.category || '其他垃圾';
const binColor = BIN_COLORS[category] || BIN_COLORS['其他垃圾'];
// 卡片样式
resultCard.className = 'result-card ' + binColor.class;
// 徽章
badgeIcon.textContent = BIN_ICONS[category] || '🗑️';
badgeText.textContent = category;
binLabel.textContent = binColor.label;
// 信息
const binInfo = result.category_info || {};
binDesc.textContent = binInfo.description || '';
examplesText.textContent = binInfo.examples || '';
tipText.textContent = binInfo.tip || '';
// 物品识别
if (result.object && result.object.name) {
identifySection.style.display = 'flex';
// 优先使用自然描述(含OCR文字),其次用物品名
const displayText = result.object.description || result.object.name_zh || result.object.name;
objectName.textContent = displayText;
const objConfPct = (result.object.confidence * 100).toFixed(1);
objectConfidence.textContent = objConfPct + '%';
objectConfidence.style.color = result.object.confidence > 0.5
? 'var(--color-green)'
: result.object.confidence > 0.3
? '#f0a030'
: 'var(--color-red)';
objectIcon.textContent = getObjectEmoji(result.object.name_zh || '');
} else {
identifySection.style.display = 'none';
}
// 详情
detectedItem.textContent = result.item_zh || result.item || '-';
const confPct = (result.confidence * 100).toFixed(1);
detectedConfidence.textContent = confPct + '%';
const confNum = result.confidence;
detectedConfidence.style.color = confNum > 0.7
? 'var(--color-green)'
: confNum > 0.4
? '#f0a030'
: 'var(--color-red)';
// 可能分类
if (result.predictions && result.predictions.length > 1) {
otherPredictions.style.display = 'block';
predictionList.innerHTML = '';
result.predictions.forEach((p, index) => {
const score = (p.score * 100).toFixed(1);
const item = document.createElement('div');
item.className = 'prediction-item';
item.style.animationDelay = (index * 0.1) + 's';
const barWidth = Math.max(p.score * 100, 3);
item.innerHTML = `
${p.label_zh || p.label}
${score}%
`;
predictionList.appendChild(item);
setTimeout(() => {
const fill = item.querySelector('.prediction-bar-fill');
if (fill) fill.style.width = barWidth + '%';
}, 200 + index * 150);
});
} else {
otherPredictions.style.display = 'none';
}
// 渲染垃圾 DNA 知识卡片
renderDnaCard(result.dna);
showSection(resultSection);
// 每日挑战追踪
ChallengeTracker.track();
// 碳足迹追踪
CarbonTracker.track(result);
// 保存到图鉴
Atlas.addEntry(previewImage, result);
setTimeout(() => {
resultSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
haptic([10, 50, 10]);
}
// ============================================
// 垃圾 DNA 知识卡片渲染
// ============================================
function renderDnaCard(dna) {
if (!dna || !dna.material) {
dnaSection.style.display = 'none';
return;
}
dnaSection.style.display = 'block';
// 材质
dnaMaterial.textContent = dna.material;
// 降解时间
const years = dna.decomposition_years;
if (years !== null && years !== undefined) {
let timeStr;
if (years >= 1000000) {
timeStr = '约 ' + (years / 10000).toFixed(0) + ' 万年';
} else if (years >= 10000) {
timeStr = '约 ' + (years / 10000).toFixed(1) + ' 万年';
} else if (years >= 100) {
timeStr = '约 ' + years.toFixed(0) + ' 年';
} else if (years >= 1) {
timeStr = '约 ' + years.toFixed(0) + ' 年';
} else {
timeStr = '约 ' + (years * 12).toFixed(0) + ' 个月';
}
dnaDecomposition.textContent = timeStr;
} else {
dnaDecomposition.textContent = '极难自然降解';
}
dnaDecompositionDesc.textContent = dna.decomposition_desc || '';
// 回收后可变成
dnaRecycledInto.innerHTML = '';
if (dna.recycled_into && dna.recycled_into.length > 0) {
dna.recycled_into.forEach((item, index) => {
const tag = document.createElement('span');
tag.className = 'dna-tag';
tag.textContent = item;
tag.style.animationDelay = (index * 0.08) + 's';
dnaRecycledInto.appendChild(tag);
});
}
// 回收流程
dnaProcess.textContent = dna.recycling_process || '';
// 环境效益(碳减排)
if (dna.co2_saving_kg !== null && dna.co2_saving_kg !== undefined) {
dnaCo2Row.style.display = 'flex';
let co2Text = '每件回收可减少 ' + dna.co2_saving_kg.toFixed(1) + ' kg CO₂ 排放';
if (dna.tree_saving) {
co2Text += ',相当于种了 ' + dna.tree_saving.toFixed(2) + ' 棵树';
}
dnaCo2Text.textContent = co2Text;
} else {
dnaCo2Row.style.display = 'none';
}
// 投放建议
dnaTips.innerHTML = '';
if (dna.disposal_tips && dna.disposal_tips.length > 0) {
dna.disposal_tips.forEach((tip, index) => {
const li = document.createElement('li');
li.textContent = tip;
li.style.animationDelay = (index * 0.06) + 's';
dnaTips.appendChild(li);
});
}
// 趣味知识
dnaFacts.innerHTML = '';
if (dna.fun_facts && dna.fun_facts.length > 0) {
dna.fun_facts.forEach((fact, index) => {
const li = document.createElement('li');
li.textContent = fact;
li.style.animationDelay = (index * 0.08) + 's';
dnaFacts.appendChild(li);
});
}
// 入场动画
dnaCard.style.animation = 'none';
void dnaCard.offsetWidth;
dnaCard.style.animation = 'dnaFadeIn 0.6s ease-out forwards';
}
// ============================================
// 错误处理
// ============================================
// ============================================
// 多物体识别 — 结果展示
// ============================================
function showMultiResult(result) {
haptic(20);
if (!result.success || !result.objects || result.objects.length === 0) {
showError('识别失败', '多物体识别未检测到有效物品');
return;
}
multiObjects = result.objects;
selectedMultiIndex = 0;
// 显示图片 + 绘制 Bounding Box
const imageUrl = previewImage.src;
multiResultImage.src = imageUrl;
multiResultImage.onload = function() {
drawMultiBboxes(multiResultImage, multiBboxCanvas, multiObjects, selectedMultiIndex);
};
// 如果图片已经加载完成
if (multiResultImage.complete && multiResultImage.naturalWidth > 0) {
drawMultiBboxes(multiResultImage, multiBboxCanvas, multiObjects, selectedMultiIndex);
}
// 渲染物体标签
renderMultiTabs(multiObjects);
// 选中第一个物体显示详情
selectMultiObject(0);
// 隐藏其他 section,显示多物体结果
[uploadSection, loadingSection, resultSection, errorSection].forEach(s => {
s.style.display = 'none';
});
multiResultSection.style.display = 'flex';
multiResultSection.style.animation = 'none';
void multiResultSection.offsetWidth;
multiResultSection.style.animation = '';
// 碳足迹追踪
CarbonTracker.trackMulti(result.objects);
// 图鉴
Atlas.addEntry(previewImage, {
item_zh: result.objects.map(o => o.label_zh).join(' + '),
category: result.overall || '多物体',
confidence: result.objects[0]?.confidence || 0,
});
setTimeout(() => {
multiResultSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
haptic([10, 50, 10]);
}
function drawMultiBboxes(imgEl, canvasEl, objects, highlightIndex) {
if (!canvasEl || !imgEl) return;
const rect = imgEl.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
// canvas 尺寸匹配显示尺寸
canvasEl.width = w;
canvasEl.height = h;
const ctx = canvasEl.getContext('2d');
ctx.clearRect(0, 0, w, h);
// 图片实际渲染尺寸(保持比例)
const iw = imgEl.naturalWidth || 1;
const ih = imgEl.naturalHeight || 1;
const scale = Math.min(w / iw, h / ih);
const ox = (w - iw * scale) / 2;
const oy = (h - ih * scale) / 2;
const COLORS = ['#5bd68a', '#5b9cf5', '#f5a623', '#f56b6b', '#9b59b6', '#1abc9c'];
objects.forEach((obj, index) => {
if (!obj.bbox) return;
const color = COLORS[index % COLORS.length];
const isHighlight = index === highlightIndex;
const box = obj.bbox;
const x = ox + box.xmin * scale;
const y = oy + box.ymin * scale;
const bw = (box.xmax - box.xmin) * scale;
const bh = (box.ymax - box.ymin) * scale;
ctx.lineWidth = isHighlight ? 3 : 1.5;
ctx.strokeStyle = color;
ctx.globalAlpha = isHighlight ? 1 : 0.6;
ctx.strokeRect(x, y, bw, bh);
// 标签背景
const label = obj.label_zh || obj.label || `物体${index + 1}`;
const conf = obj.confidence ? (obj.confidence * 100).toFixed(0) + '%' : '';
const text = `#${index + 1} ${label} ${conf}`;
ctx.font = 'bold 12px "Noto Sans SC", sans-serif';
const metrics = ctx.measureText(text);
const tw = metrics.width + 10;
const th = 22;
// 标签画在方框上方
const lx = x;
const ly = y - th > 0 ? y - th : y + bh;
ctx.fillStyle = color;
ctx.globalAlpha = isHighlight ? 0.95 : 0.75;
ctx.beginPath();
ctx.roundRect(lx, ly, Math.min(tw, w - lx - 4), th, 4);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.globalAlpha = 1;
ctx.fillText(text, lx + 5, ly + 15);
});
}
// 渲染物体切换标签
function renderMultiTabs(objects) {
const container = $('#multiTabs');
if (!container) return;
const COLORS = ['#5bd68a', '#5b9cf5', '#f5a623', '#f56b6b', '#9b59b6', '#1abc9c'];
container.innerHTML = objects.map((obj, index) => {
const color = COLORS[index % COLORS.length];
const catIcon = BIN_ICONS[obj.category] || '🗑️';
return `
`;
}).join('');
// 绑定事件
container.querySelectorAll('.multi-tab').forEach(tab => {
tab.addEventListener('click', function() {
const idx = parseInt(this.dataset.index);
selectMultiObject(idx);
haptic(6);
});
});
}
function selectMultiObject(index) {
if (!multiObjects[index]) return;
selectedMultiIndex = index;
const obj = multiObjects[index];
// 更新标签高亮
document.querySelectorAll('.multi-tab').forEach(tab => {
tab.classList.toggle('multi-tab--active', parseInt(tab.dataset.index) === index);
});
// 更新 bounding box 高亮
drawMultiBboxes(multiResultImage, multiBboxCanvas, multiObjects, index);
// 更新详情卡片
const binName = (BIN_COLORS[obj.category] || BIN_COLORS['其他垃圾']).class;
const binColor = (BIN_COLORS[obj.category] || BIN_COLORS['其他垃圾']).color;
$('#multiDetailCard').className = 'multi-detail-card ' + binName;
$('#multiDetailAccent').style.background = binColor;
$('#multiBadgeIcon').textContent = BIN_ICONS[obj.category] || '🗑️';
$('#multiBadgeText').textContent = obj.category;
// 物品识别
if (obj.object && obj.object.name) {
const displayText = obj.object.description || obj.object.name_zh || obj.object.name;
$('#multiObjectName').textContent = displayText;
const objConf = (obj.object.confidence * 100).toFixed(1);
$('#multiObjectConfidence').textContent = objConf + '%';
$('#multiObjectConfidence').style.color = obj.object.confidence > 0.5
? 'var(--color-green)' : obj.object.confidence > 0.3 ? '#f0a030' : 'var(--color-red)';
$('#multiObjectIcon').textContent = getObjectEmoji(obj.object.name_zh || '');
$('#multiIdentifySection').style.display = 'flex';
} else {
$('#multiIdentifySection').style.display = 'none';
}
// 垃圾桶
const info = obj.category_info || {};
$('#multiBinLabel').textContent = (BIN_COLORS[obj.category] || BIN_COLORS['其他垃圾']).label;
$('#multiBinBody').style.background = binColor;
$('#multiBinDesc').textContent = info.description || '';
$('#multiExamplesText').textContent = info.examples || '';
$('#multiTipText').textContent = info.tip || '';
// DNA
renderMultiDna(obj.dna);
// 详情
$('#multiDetectedItem').textContent = obj.label_zh || obj.label || '-';
const confPct = (obj.confidence * 100).toFixed(1);
$('#multiDetectedConfidence').textContent = confPct + '%';
$('#multiDetectedConfidence').style.color = obj.confidence > 0.7
? 'var(--color-green)' : obj.confidence > 0.4 ? '#f0a030' : 'var(--color-red)';
}
function renderMultiDna(dna) {
const section = $('#multiDnaSection');
const body = $('#multiDnaBody');
if (!dna || !dna.material) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
const years = dna.decomposition_years;
let timeStr = '极难自然降解';
if (years !== null && years !== undefined) {
if (years >= 1000000) timeStr = '约 ' + (years / 10000).toFixed(0) + ' 万年';
else if (years >= 10000) timeStr = '约 ' + (years / 10000).toFixed(1) + ' 万年';
else if (years >= 1) timeStr = '约 ' + years.toFixed(0) + ' 年';
else timeStr = '约 ' + (years * 12).toFixed(0) + ' 个月';
}
let tagsHtml = '';
if (dna.recycled_into && dna.recycled_into.length > 0) {
tagsHtml = dna.recycled_into.map(t =>
`${t}`
).join('');
}
let tipsHtml = '';
if (dna.disposal_tips && dna.disposal_tips.length > 0) {
tipsHtml = dna.disposal_tips.map(t => `${t}`).join('');
}
let factsHtml = '';
if (dna.fun_facts && dna.fun_facts.length > 0) {
factsHtml = dna.fun_facts.map(f => `${f}`).join('');
}
let co2Html = '';
if (dna.co2_saving_kg !== null && dna.co2_saving_kg !== undefined) {
let co2Text = `每件回收可减少 ${dna.co2_saving_kg.toFixed(1)} kg CO₂ 排放`;
if (dna.tree_saving) co2Text += `,相当于种了 ${dna.tree_saving.toFixed(2)} 棵树`;
co2Html = `
`;
}
body.innerHTML = `
⏳
自然降解时间
${timeStr}
${dna.decomposition_desc || ''}
${tagsHtml ? `
` : ''}
${co2Html}
${tipsHtml ? `
` : ''}
${factsHtml ? `
` : ''}
`;
}
function showError(title, detail) {
haptic([20, 30, 20]);
errorTitle.textContent = title || '识别失败';
errorDetail.textContent = detail || '请重试或换一张照片';
showSection(errorSection);
}
// ============================================
// 重置
// ============================================
function resetAll() {
haptic(10);
resetUpload();
showSection(uploadSection);
multiResultSection.style.display = 'none';
}
// 支持多物体的 "再识别一张"
btnAgain.addEventListener('click', resetAll);
btnErrorRetry.addEventListener('click', resetAll);
const btnMultiAgain = $('#btnMultiAgain');
if (btnMultiAgain) btnMultiAgain.addEventListener('click', resetAll);
// ============================================
// 初始化
// ============================================
function init() {
showSection(uploadSection);
initRipples();
initGlowTracking();
// 初始化每日挑战
ChallengeTracker.init();
// 初始化碳足迹
CarbonTracker.init();
// 初始化游戏中心
GameCenter.init();
TimedQuiz.init();
MemoryMatch.init();
// 图鉴清空按钮
const clearBtn = $('#atlasClear');
if (clearBtn) clearBtn.addEventListener('click', () => Atlas.clear());
// 碳足迹折叠按钮
const carbonToggleBtn = $('#carbonToggle');
if (carbonToggleBtn) {
carbonToggleBtn.addEventListener('click', () => {
const body = $('#carbonBody');
if (body) {
body.classList.toggle('carbon-body--collapsed');
carbonToggleBtn.classList.toggle('carbon-toggle--open');
}
});
}
fetch('/api/health')
.then(r => r.json())
.then(data => console.log('Backend connected:', data))
.catch(() => {});
// ESC 退出所有游戏界面
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideAllGameScreens();
}
});
}
// 退出所有游戏界面,返回主页面
function hideAllGameScreens() {
// 清除所有游戏计时器
if (TimedQuiz && TimedQuiz.timer) { clearInterval(TimedQuiz.timer); TimedQuiz.timer = null; }
if (MemoryMatch && MemoryMatch.timer) { clearInterval(MemoryMatch.timer); MemoryMatch.timer = null; }
$('#gameCenterOverlay').style.display = 'none';
$('#timedQuizScreen').style.display = 'none';
$('#memoryScreen').style.display = 'none';
// 恢复到主页面
showSection(uploadSection);
haptic(6);
}
// ============================================
// 每日挑战 — 融入式追踪
// ============================================
const CHALLENGE_KEY = 'gc_challenge_data';
const CHALLENGE_GOAL = 10; // 每日目标分类数
const MEDAL_DEFS = [
{ id: 'first_try', label: '首战告捷', desc: '首次完成每日挑战', icon: '🎖️', check: s => s.firstTry },
{ id: 'streak_3', label: '三日连续', desc: '连续打卡 3 天', icon: '🔥', check: s => s.streak >= 3 },
{ id: 'streak_7', label: '七日连续', desc: '连续打卡 7 天', icon: '💎', check: s => s.streak >= 7 },
{ id: 'streak_30', label: '月之星', desc: '连续打卡 30 天', icon: '🌟', check: s => s.streak >= 30 },
{ id: 'classify_50', label: '小试牛刀', desc: '累计分类 50 件', icon: '📦', check: s => s.total >= 50 },
{ id: 'classify_100', label: '分类达人', desc: '累计分类 100 件', icon: '🎯', check: s => s.total >= 100 },
{ id: 'classify_500', label: '环保先锋', desc: '累计分类 500 件', icon: '♻️', check: s => s.total >= 500 },
];
const ChallengeTracker = {
// 读取存档
load() {
try {
const raw = localStorage.getItem(CHALLENGE_KEY);
if (raw) return JSON.parse(raw);
} catch (e) {}
return { count: 0, date: null, streak: 0, total: 0, medals: [], completedDates: [], firstTry: false };
},
save(data) {
try { localStorage.setItem(CHALLENGE_KEY, JSON.stringify(data)); } catch (e) {}
},
// 每次分类成功后调用
track() {
const data = this.load();
const today = new Date().toISOString().split('T')[0];
// 如果是新的一天,重置计数
if (data.date !== today) {
// 检查上一日是否有打卡 → 更新连续天数
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
if (data.date === yesterday) {
data.streak++;
} else if (data.date !== today) {
// 断了或第一次
if (data.date !== null) data.streak = 0;
}
data.count = 0;
data.date = today;
}
// 递增
data.count++;
data.total++;
// 检查是否达成今日目标
const justCompleted = data.count === CHALLENGE_GOAL;
if (justCompleted) {
data.firstTry = true;
if (!data.completedDates.includes(today)) {
data.completedDates.push(today);
}
// 打卡天数 = streak + 今天
// 但如果 steam 已含今天就不重复加
}
// 检查勋章
const newMedals = MEDAL_DEFS.filter(m => m.check(data) && !data.medals.includes(m.id));
newMedals.forEach(m => {
if (!data.medals.includes(m.id)) data.medals.push(m.id);
});
this.save(data);
// 更新入口卡片
this.updateEntry(data);
// 如果今日目标达成,弹庆祝
if (justCompleted) {
setTimeout(() => this.showCelebration(data), 300);
}
return { data, justCompleted, newMedals };
},
// 更新入口卡片的进度
updateEntry(data) {
const today = new Date().toISOString().split('T')[0];
// 如果日期变了但还没分类,显示 0
const count = (data.date === today) ? data.count : 0;
const pct = Math.min(100, (count / CHALLENGE_GOAL) * 100);
$('#challengeEntryDesc').textContent = `今天已分类 ${count} 件物品`;
$('#challengeEntryPct').textContent = Math.round(pct) + '%';
// 环形进度
const circumference = 2 * Math.PI * 18; // r=18
const offset = circumference * (1 - pct / 100);
const ring = $('#challengeEntryRing');
if (ring) ring.setAttribute('stroke-dashoffset', offset);
// 等级
const level = data.total >= 500 ? 5 : data.total >= 100 ? 4 : data.total >= 50 ? 3 : data.total >= 10 ? 2 : 1;
$('#challengeEntryBadge').textContent = 'Lv.' + level;
},
// 显示庆祝
showCelebration(data) {
const section = $('#challengeResultSection');
section.style.display = 'block';
section.style.animation = 'none';
void section.offsetWidth;
section.style.animation = 'resultIn 0.6s ease-out';
$('#challengeResultIcon').textContent = '🎉';
$('#challengeResultTitle').textContent = '今日挑战完成!';
$('#challengeResultSub').textContent = `已成功分类 ${CHALLENGE_GOAL} 件垃圾`;
$('#resultCount').textContent = data.count;
// 如果 streak 是 0 但今天完成了,至少算 1 天
const streak = Math.max(1, data.streak);
$('#resultStreak').textContent = streak;
$('#resultTotal').textContent = data.total;
$('#resultMedals').textContent = data.medals.length;
// 新勋章
const newMedals = MEDAL_DEFS.filter(m => m.check(data) && !data._shownMedals);
if (newMedals.length > 0) {
const medalsDiv = $('#challengeMedals');
medalsDiv.style.display = 'block';
const list = $('#medalsList');
list.innerHTML = '';
newMedals.forEach((m, i) => {
data._shownMedals = data._shownMedals || [];
data._shownMedals.push(m.id);
const div = document.createElement('div');
div.className = 'medal-item medal-new';
div.style.animationDelay = (i * 0.1) + 's';
div.innerHTML = `
${m.icon}
${m.label}
${m.desc}
NEW
`;
list.appendChild(div);
});
} else {
$('#challengeMedals').style.display = 'none';
}
section.scrollIntoView({ behavior: 'smooth', block: 'center' });
haptic([15, 30, 15]);
},
// 初始化入口卡片
init() {
const data = this.load();
this.updateEntry(data);
// 点击卡片打开图鉴
$('#btnChallengeDetails').addEventListener('click', () => {
Atlas.toggle();
});
// 分享按钮
$('#btnChallengeShare').addEventListener('click', () => {
const d = this.load();
const text =
`🎉 我在垃圾分类系统中完成了今日挑战!\n\n` +
`连续打卡: ${Math.max(1, d.streak)} 天\n` +
`累计分类: ${d.total} 件\n` +
`🏅 勋章: ${d.medals.length} 枚\n\n` +
`一起保护地球吧!🌱`;
if (navigator.share) {
navigator.share({ title: '垃圾分类每日挑战', text }).catch(() => {});
} else {
navigator.clipboard.writeText(text).then(() => haptic(10)).catch(() => {});
}
});
},
};
// ============================================
// 图鉴系统 (Collection Atlas)
// ============================================
const COLLECTION_KEY = 'gc_atlas';
const Atlas = {
load() {
try {
const raw = localStorage.getItem(COLLECTION_KEY);
return raw ? JSON.parse(raw) : [];
} catch(e) { return []; }
},
save(items) {
try { localStorage.setItem(COLLECTION_KEY, JSON.stringify(items)); } catch(e) {}
},
// 每次分类后保存
addEntry(imgEl, result) {
let items = this.load();
if (items.length >= 20) items = items.slice(-19);
// 提取图片 src,一些国产浏览器对 img 元素有兼容问题
const src = (imgEl && imgEl.src) || '';
if (!src) return;
items.push({
id: Date.now(),
image: src,
item: result.item_zh || result.item || '未知',
category: result.category || '其他垃圾',
confidence: result.confidence ? (result.confidence * 100).toFixed(1) + '%' : '',
});
this.save(items);
},
// 切换图鉴显示
toggle() {
const section = $('#atlasSection');
if (!section) return;
const isOpen = section.style.display !== 'none';
if (isOpen) {
section.style.display = 'none';
} else {
this.render();
section.style.display = 'block';
section.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
render() {
const items = this.load();
const grid = $('#atlasGrid');
const empty = $('#atlasEmpty');
const count = $('#atlasCount');
count.textContent = items.length + ' 件';
if (!items.length) {
grid.innerHTML = '';
empty.style.display = 'flex';
return;
}
empty.style.display = 'none';
// 倒序显示(最新的在前)
grid.innerHTML = [...items].reverse().map(item => {
const catClass = item.category === '可回收垃圾' ? 'blue'
: item.category === '有害垃圾' ? 'red'
: item.category === '厨余垃圾' ? 'green'
: 'gray';
return `
${item.category}
${item.item}
`;
}).join('');
},
clear() {
if (confirm('确定清空所有图鉴记录?')) {
this.save([]);
this.render();
haptic(10);
}
},
};
// ============================================
// 碳足迹追踪 (Carbon Footprint Tracker)
// ============================================
const CARBON_KEY = 'gc_carbon_data';
const CARBON_ACHIEVEMENTS = [
{ id: 'first_step', name: '第一步', icon: '🌱', check: d => d.totalCo2 >= 0.1 },
{ id: 'kg_co2', name: '减碳 1kg', icon: '🍃', check: d => d.totalCo2 >= 1 },
{ id: 'tree_planter', name: '种树者', icon: '🌳', check: d => d.trees >= 1 },
{ id: 'co2_10', name: '减碳 10kg', icon: '🌿', check: d => d.totalCo2 >= 10 },
{ id: 'co2_50', name: '环保先锋', icon: '♻️', check: d => d.totalCo2 >= 50 },
{ id: 'co2_100', name: '碳中和小能手', icon: '🏆', check: d => d.totalCo2 >= 100 },
{ id: 'items_100', name: '分类 100件', icon: '📦', check: d => d.totalItems >= 100 },
{ id: 'items_500', name: '分类 500件', icon: '💎', check: d => d.totalItems >= 500 },
{ id: 'week_streak', name: '连续7天', icon: '🔥', check: d => d.weekStreak >= 7 },
];
const CarbonTracker = {
// 默认数据
_defaults() {
return {
totalCo2: 0,
totalTrees: 0,
totalItems: 0,
history: [], // { date, co2, trees, items }
weekly: {}, // '2026-W25': { co2, items }
achievements: [],
lastActiveDate: null,
weekStreak: 0,
};
},
load() {
try {
const raw = localStorage.getItem(CARBON_KEY);
if (raw) {
const d = JSON.parse(raw);
// 确保所有字段存在
const def = this._defaults();
for (const k of Object.keys(def)) {
if (d[k] === undefined) d[k] = def[k];
}
return d;
}
} catch(e) {}
return this._defaults();
},
save(data) {
try {
localStorage.setItem(CARBON_KEY, JSON.stringify(data));
} catch(e) {}
},
// 单物体分类后追踪
track(result) {
const data = this.load();
const today = new Date().toISOString().split('T')[0];
const co2 = result.dna?.co2_saving_kg || 0;
const trees = result.dna?.tree_saving || 0;
if (co2 > 0 || trees > 0) {
data.totalCo2 += co2;
data.totalTrees += trees;
data.totalItems += 1;
data.history.push({
date: today,
co2: co2,
trees: trees,
item: result.item_zh || result.item || '未知',
});
// 按天聚合
const todayEntry = data.history.filter(h => h.date === today);
if (todayEntry.length > 0) {
// 已有今日记录
}
// 周聚合
const weekKey = this._getWeekKey(today);
if (!data.weekly[weekKey]) data.weekly[weekKey] = { co2: 0, items: 0 };
data.weekly[weekKey].co2 += co2;
data.weekly[weekKey].items += 1;
// 连续活跃检查
if (data.lastActiveDate !== today) {
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
if (data.lastActiveDate === yesterday) {
data.weekStreak = (data.weekStreak || 0) + 1;
} else if (data.lastActiveDate !== today) {
data.weekStreak = 1;
}
data.lastActiveDate = today;
}
this._checkAchievements(data);
this.save(data);
this.render(data);
}
},
// 多物体分类后追踪
trackMulti(objects) {
if (!objects || objects.length === 0) return;
const data = this.load();
const today = new Date().toISOString().split('T')[0];
const weekKey = this._getWeekKey(today);
let totalCo2 = 0;
let totalTrees = 0;
objects.forEach(obj => {
const co2 = obj.dna?.co2_saving_kg || 0;
const trees = obj.dna?.tree_saving || 0;
totalCo2 += co2;
totalTrees += trees;
});
if (totalCo2 > 0 || totalTrees > 0) {
data.totalCo2 += totalCo2;
data.totalTrees += totalTrees;
data.totalItems += objects.length;
data.history.push({
date: today,
co2: totalCo2,
trees: totalTrees,
item: objects.map(o => o.label_zh).join(' + '),
});
if (!data.weekly[weekKey]) data.weekly[weekKey] = { co2: 0, items: 0 };
data.weekly[weekKey].co2 += totalCo2;
data.weekly[weekKey].items += objects.length;
if (data.lastActiveDate !== today) {
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
if (data.lastActiveDate === yesterday) {
data.weekStreak = (data.weekStreak || 0) + 1;
} else if (data.lastActiveDate !== today) {
data.weekStreak = 1;
}
data.lastActiveDate = today;
}
this._checkAchievements(data);
this.save(data);
this.render(data);
}
},
_getWeekKey(dateStr) {
const d = new Date(dateStr + 'T00:00:00');
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return d.getUTCFullYear() + '-W' + String(weekNo).padStart(2, '0');
},
_checkAchievements(data) {
CARBON_ACHIEVEMENTS.forEach(a => {
if (!data.achievements.includes(a.id) && a.check(data)) {
data.achievements.push(a.id);
}
});
},
render(data) {
const section = $('#carbonSection');
if (!section) return;
if (!data || data.totalCo2 <= 0 && data.totalItems <= 0) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
// 主要指标
$('#carbonCo2Saved').textContent = data.totalCo2.toFixed(1);
$('#carbonTreesPlanted').textContent = data.totalTrees.toFixed(2);
$('#carbonItemsClassified').textContent = data.totalItems || 0;
// 等效对比(基于环保部推荐换算系数)
const carKm = data.totalCo2 / 0.17; // 0.17 kg CO₂/km(普通家用车)
const bulbHours = data.totalCo2 / 0.04; // 0.04 kg CO₂/h(60W节能灯)
const phoneCharges = data.totalCo2 / 0.005; // 0.005 kg CO₂/次(手机充电)
const treeDays = data.totalCo2 / 0.02; // 0.02 kg CO₂/天(一棵树日吸收量)
$('#equivCar').textContent = `相当于开车减少 ${carKm >= 1 ? carKm.toFixed(1) : '<1'} km`;
$('#equivBulb').textContent = `相当于灯泡点亮 ${bulbHours >= 1 ? Math.round(bulbHours) : '<1'} 小时`;
$('#equivCharge').textContent = `相当于手机充电 ${phoneCharges >= 1 ? Math.round(phoneCharges) : '<1'} 次`;
// 月份趋势
this._renderWeeklyBars(data);
// 成就
this._renderAchievements(data);
},
_renderWeeklyBars(data) {
const container = $('#carbonBars');
const monthlySection = $('#carbonMonthly');
if (!container || !monthlySection) return;
const weeks = Object.keys(data.weekly).sort().slice(-8);
if (weeks.length < 2) {
monthlySection.style.display = 'none';
return;
}
monthlySection.style.display = 'block';
const maxCo2 = Math.max(...weeks.map(w => data.weekly[w].co2), 0.01);
container.innerHTML = weeks.map(w => {
const weekData = data.weekly[w];
const pct = (weekData.co2 / maxCo2) * 100;
const shortLabel = w.split('-W')[1] || w.slice(-2);
return `
周${shortLabel}: ${weekData.co2.toFixed(2)}kg
`;
}).join('');
},
_renderAchievements(data) {
const list = $('#carbonAchievementList');
if (!list) return;
list.innerHTML = CARBON_ACHIEVEMENTS.map(a => {
const unlocked = data.achievements.includes(a.id);
return `
${unlocked ? a.icon : '🔒'}
${a.name}
`;
}).join('');
},
init() {
const data = this.load();
if (data && data.totalCo2 > 0) {
this.render(data);
}
// 修改 showResult 中调用 track 的行为:添加碳足迹追踪
// 通过 monkey-patch ChallengeTracker.track 的方式已足够
// CarbonTracker 在 track() 和 trackMulti() 中自动保存
},
};
// ============================================
// 游戏数据常量
// ============================================
const CATEGORIES = ['可回收垃圾', '有害垃圾', '厨余垃圾', '其他垃圾'];
const GAME_ITEMS = [
// 可回收垃圾
{ name_en: 'plastic bottle for drinking', name_zh: '塑料瓶', category: '可回收垃圾', emoji: '🧴' },
{ name_en: 'glass bottle for drinking', name_zh: '玻璃瓶', category: '可回收垃圾', emoji: '🥃' },
{ name_en: 'aluminum can for soda or beer', name_zh: '易拉罐', category: '可回收垃圾', emoji: '🥤' },
{ name_en: 'paper and cardboard for recycling', name_zh: '纸张', category: '可回收垃圾', emoji: '📄' },
{ name_en: 'cardboard box for packaging', name_zh: '纸箱', category: '可回收垃圾', emoji: '📦' },
{ name_en: 'newspaper and magazines', name_zh: '报纸杂志', category: '可回收垃圾', emoji: '📰' },
{ name_en: 'clothing and textiles', name_zh: '衣物', category: '可回收垃圾', emoji: '👕' },
{ name_en: 'plastic bag for shopping', name_zh: '塑料袋', category: '可回收垃圾', emoji: '🛍️' },
{ name_en: 'metal tools and hardware', name_zh: '金属制品', category: '可回收垃圾', emoji: '🔧' },
{ name_en: 'electronics and wires with circuit', name_zh: '电子产品', category: '可回收垃圾', emoji: '💻' },
{ name_en: 'wooden furniture or wood pieces', name_zh: '木材家具', category: '可回收垃圾', emoji: '🪵' },
{ name_en: 'books and notebooks for school', name_zh: '书本', category: '可回收垃圾', emoji: '📚' },
{ name_en: 'glass jar with lid', name_zh: '玻璃罐', category: '可回收垃圾', emoji: '🫙' },
{ name_en: 'iron and steel food cans', name_zh: '铁罐', category: '可回收垃圾', emoji: '🥫' },
{ name_en: 'shoes and bags made of fabric', name_zh: '鞋包', category: '可回收垃圾', emoji: '👟' },
{ name_en: 'plastic container for food storage', name_zh: '塑料容器', category: '可回收垃圾', emoji: '🥡' },
{ name_en: 'milk carton or tetra pak drink box', name_zh: '牛奶盒', category: '可回收垃圾', emoji: '🥛' },
{ name_en: 'plastic bottle cap', name_zh: '瓶盖', category: '可回收垃圾', emoji: '🔘' },
{ name_en: 'disposable wooden chopsticks pair', name_zh: '一次性筷子', category: '可回收垃圾', emoji: '🥢' },
// 有害垃圾
{ name_en: 'battery cell for electronics', name_zh: '电池', category: '有害垃圾', emoji: '🔋' },
{ name_en: 'light bulb glass transparent', name_zh: '灯泡', category: '有害垃圾', emoji: '💡' },
{ name_en: 'medicine pills and tablets', name_zh: '药品', category: '有害垃圾', emoji: '💊' },
{ name_en: 'paint can with metal handle', name_zh: '油漆桶', category: '有害垃圾', emoji: '🎨' },
{ name_en: 'mercury thermometer glass', name_zh: '温度计', category: '有害垃圾', emoji: '🌡️' },
{ name_en: 'pesticide spray bottle', name_zh: '杀虫剂', category: '有害垃圾', emoji: '🧪' },
{ name_en: 'fluorescent tube light', name_zh: '荧光灯管', category: '有害垃圾', emoji: '💡' },
{ name_en: 'nail polish bottle small', name_zh: '化妆品', category: '有害垃圾', emoji: '💄' },
// 厨余垃圾
{ name_en: 'food waste and leftovers on plate', name_zh: '食物残渣', category: '厨余垃圾', emoji: '🍽️' },
{ name_en: 'fruit and fruit peels organic waste', name_zh: '水果果皮', category: '厨余垃圾', emoji: '🍌' },
{ name_en: 'banana peel yellow', name_zh: '香蕉皮', category: '厨余垃圾', emoji: '🍌' },
{ name_en: 'apple core with seeds', name_zh: '苹果核', category: '厨余垃圾', emoji: '🍎' },
{ name_en: 'tea leaves and coffee grounds wet', name_zh: '茶叶咖啡渣', category: '厨余垃圾', emoji: '🫖' },
{ name_en: 'egg shell broken pieces', name_zh: '蛋壳', category: '厨余垃圾', emoji: '🥚' },
{ name_en: 'fish bones and chicken bones', name_zh: '骨头', category: '厨余垃圾', emoji: '🍗' },
{ name_en: 'leftover rice and noodles in bowl', name_zh: '剩饭剩面', category: '厨余垃圾', emoji: '🍚' },
{ name_en: 'cooked meat and bones leftovers', name_zh: '肉渣骨头', category: '厨余垃圾', emoji: '🥩' },
// 其他垃圾
{ name_en: 'styrofoam and foam packaging white', name_zh: '泡沫塑料', category: '其他垃圾', emoji: '🧊' },
{ name_en: 'disposable face mask blue', name_zh: '口罩', category: '其他垃圾', emoji: '😷' },
{ name_en: 'tissue paper and napkins used', name_zh: '纸巾', category: '其他垃圾', emoji: '🧻' },
{ name_en: 'cigarette butt with filter', name_zh: '烟蒂', category: '其他垃圾', emoji: '🚬' },
{ name_en: 'diaper and sanitary pads', name_zh: '尿不湿', category: '其他垃圾', emoji: '👶' },
{ name_en: 'broken glass not recyclable shards', name_zh: '碎玻璃', category: '其他垃圾', emoji: '🪟' },
{ name_en: 'instant noodle cup styrofoam cup with lid', name_zh: '泡面桶', category: '其他垃圾', emoji: '🍜' },
{ name_en: 'takeout food container plastic box', name_zh: '外卖盒', category: '其他垃圾', emoji: '🥡' },
{ name_en: 'bubble tea cup with plastic lid and straw', name_zh: '奶茶杯', category: '其他垃圾', emoji: '🧋' },
{ name_en: 'plastic drinking straw', name_zh: '吸管', category: '其他垃圾', emoji: '🥤' },
{ name_en: 'wet wipes in package', name_zh: '湿巾', category: '其他垃圾', emoji: '🧻' },
{ name_en: 'cotton swab for ears', name_zh: '棉签', category: '其他垃圾', emoji: '🪥' },
{ name_en: 'bandage and adhesive tape medical', name_zh: '创可贴', category: '其他垃圾', emoji: '🩹' },
{ name_en: 'broken porcelain ceramic shards', name_zh: '碎瓷器', category: '其他垃圾', emoji: '🏺' },
{ name_en: 'disposable foam lunch box', name_zh: '泡沫饭盒', category: '其他垃圾', emoji: '📦' },
];
const ITEM_FUN_FACTS = {
"plastic bottle for drinking": "每回收 1 吨 PET 塑料瓶,可节省约 6 吨石油资源",
"glass bottle for drinking": "玻璃可以无限次 100% 回收,质量不会下降",
"aluminum can for soda or beer": "回收一个易拉罐节省的电量可让电视运行 3 小时",
"paper and cardboard for recycling": "回收 1 吨废纸可造 800 公斤新纸,少砍 17 棵树",
"cardboard box for packaging": "纸箱回收后可以做成再生纸板箱或鸡蛋托",
"newspaper and magazines": "旧报纸回收后可以制成铅笔、鸡蛋托等新产品",
"clothing and textiles": "旧衣物可以回收制成工业擦布、填充材料或再生纤维",
"plastic bag for shopping": "塑料袋自然降解需要 10-1000 年",
"metal tools and hardware": "金属可以反复回收利用,性能不会下降",
"electronics and wires with circuit": "1 吨废旧手机含金量是 1 吨金矿的 100 倍",
"books and notebooks for school": "旧书回收后可制成再生纸浆,用于生产包装纸",
"glass jar with lid": "玻璃回收可减少 20% 的空气污染和 50% 的水污染",
"milk carton or tetra pak drink box": "利乐包装可回收制成板材或再生纸",
"plastic bottle cap": "瓶盖通常使用 HDPE 塑料,回收后可制成塑料管材",
"battery cell for electronics": "一粒纽扣电池可污染 60 万升水",
"light bulb glass transparent": "节能灯含汞,破碎后汞会挥发到空气中",
"medicine pills and tablets": "过期药品属于有害垃圾,应投入红色垃圾桶",
"paint can with metal handle": "油漆桶含有害化学物质,需专业处理",
"nail polish bottle small": "化妆品和指甲油含有害化学物质",
"food waste and leftovers on plate": "厨余垃圾可以堆肥转化为有机肥料",
"fruit and fruit peels organic waste": "果皮果核是制作环保酵素的好材料",
"banana peel yellow": "香蕉皮富含钾,可堆肥制成天然肥料",
"tea leaves and coffee grounds wet": "茶渣咖啡渣是优质的堆肥原料,还可除臭",
"egg shell broken pieces": "蛋壳富含钙质,磨碎后可作天然肥料",
"fish bones and chicken bones": "骨头经粉碎后可加工成动物饲料或肥料",
"styrofoam and foam packaging white": "泡沫塑料自然降解需要数百年",
"disposable face mask blue": "一次性口罩的主要原料是聚丙烯,很难自然降解",
"tissue paper and napkins used": "用过的纸巾水溶性太差,不适合回收造纸",
"cigarette butt with filter": "烟蒂中的醋酸纤维在自然环境中需 10 年才能降解",
"diaper and sanitary pads": "尿不湿含有高分子吸水树脂,属于其他垃圾",
"instant noodle cup styrofoam cup with lid": "泡面桶内壁有塑料淋膜,回收难度大",
"plastic drinking straw": "一次性吸管虽小,但回收价值低",
"wet wipes in package": "湿巾含有化纤成分,不可冲入马桶",
"cotton swab for ears": "棉签杆多为塑料材质,小件易被遗漏",
"bandage and adhesive tape medical": "创可贴使用后可能沾有体液,不宜回收",
};
// ============================================
// 游戏中心 (GameCenter)
// ============================================
const GC_KEY = 'gc_game_data';
const GameCenter = {
_defaults() {
return {
coins: 0,
timedquiz_highest: 0,
timedquiz_last: null,
memory_besttime: null,
memory_bestattempts: null,
memory_last: null,
};
},
load() {
try {
const raw = localStorage.getItem(GC_KEY);
if (raw) {
const d = JSON.parse(raw);
const def = this._defaults();
for (const k of Object.keys(def)) { if (d[k] === undefined) d[k] = def[k]; }
return d;
}
} catch(e) {}
return this._defaults();
},
save(data) {
try { localStorage.setItem(GC_KEY, JSON.stringify(data)); } catch(e) {}
},
getCoins() { return this.load().coins; },
addCoins(amount) {
const data = this.load();
data.coins += amount;
this.save(data);
this.updateCoinsDisplay();
},
updateCoinsDisplay() {
const el = $('#gcCoinsDisplay');
if (el) el.textContent = '🪙 ' + this.getCoins();
},
showGameCenter() {
const data = this.load();
// 更新高分数值
const tqHigh = $('#gcTqHighScore');
if (tqHigh) tqHigh.textContent = data.timedquiz_highest || '0';
const memBest = $('#gcMemBestTime');
if (memBest) {
memBest.textContent = data.memory_besttime ? data.memory_besttime + 's' : '--';
}
this.updateCoinsDisplay();
$('#gameCenterOverlay').style.display = 'flex';
haptic(8);
},
hideGameCenter() {
$('#gameCenterOverlay').style.display = 'none';
},
init() {
// 入口卡片
const entry = $('#gameCenterEntry');
if (entry) {
entry.addEventListener('click', () => this.showGameCenter());
}
// 返回按钮
const backBtn = $('#gcBack');
if (backBtn) backBtn.addEventListener('click', () => this.hideGameCenter());
// 限时游戏入口
const tqCard = $('#gcTimedQuiz');
if (tqCard) tqCard.addEventListener('click', () => {
this.hideGameCenter();
TimedQuiz.showStart();
});
// 连连看入口
const memCard = $('#gcMemoryMatch');
if (memCard) memCard.addEventListener('click', () => {
this.hideGameCenter();
MemoryMatch.showStart();
});
},
};
// ============================================
// 限时分类挑战 (TimedQuiz)
// ============================================
const TimedQuiz = {
TOTAL_TIME: 60,
ITEMS_PER_GAME: 20,
SPEED_BONUS_TIME: 3,
SPEED_BONUS_POINTS: 2,
state: null,
timer: null,
showStart() {
// 隐藏当前使用 section
[uploadSection, loadingSection, resultSection, errorSection, multiResultSection].forEach(s => {
s.style.display = 'none';
});
const data = GameCenter.load();
$('#tqStartHighScore').textContent = data.timedquiz_highest || '0';
$('#timedQuizScreen').style.display = 'flex';
$('#tqStart').style.display = 'flex';
$('#tqGame').style.display = 'none';
$('#tqResult').style.display = 'none';
},
startGame() {
// 准备题目: 打乱后选 ITEMS_PER_GAME 个
const shuffled = [...GAME_ITEMS].sort(() => Math.random() - 0.5);
const items = shuffled.slice(0, this.ITEMS_PER_GAME);
this.state = {
items: items,
currentIndex: 0,
score: 0,
timeLeft: this.TOTAL_TIME,
streak: 0,
maxStreak: 0,
answers: [],
startTime: Date.now(),
answered: false,
};
$('#tqStart').style.display = 'none';
$('#tqGame').style.display = 'flex';
$('#tqResult').style.display = 'none';
this.renderHUD();
this.showItem();
this.resetFeedback();
// 启动计时器
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => this.tick(), 1000);
},
tick() {
if (!this.state) return;
this.state.timeLeft--;
this.renderHUD();
const timerEl = $('#tqTimer');
if (timerEl) {
timerEl.classList.toggle('tq-timer--warning', this.state.timeLeft <= 5);
}
if (this.state.timeLeft <= 0) {
this.endGame();
}
},
currentItem() {
return this.state.items[this.state.currentIndex];
},
showItem() {
const item = this.currentItem();
if (!item) {
this.endGame();
return;
}
this.state.answered = false;
$('#tqItemEmoji').textContent = item.emoji || '📦';
$('#tqItemName').textContent = item.name_zh;
// 重置按钮状态
$$('.tq-btn').forEach(btn => {
btn.classList.remove('tq-btn--correct', 'tq-btn--wrong', 'tq-btn--show-correct');
btn.disabled = false;
btn.style.pointerEvents = 'auto';
});
// 重置进度
const pct = ((this.state.currentIndex) / this.state.items.length) * 100;
const fill = $('#tqProgressFill');
if (fill) fill.style.width = Math.min(pct, 100) + '%';
// 物品入场动画
const card = $('#tqItemCard');
if (card) {
card.style.animation = 'none';
void card.offsetWidth;
card.style.animation = 'tqItemIn 0.35s var(--ease-out)';
}
this.resetFeedback();
},
submitAnswer(category) {
if (this.state.answered) return;
this.state.answered = true;
const item = this.currentItem();
if (!item) return;
const isCorrect = category === item.category;
const timeElapsed = this.TOTAL_TIME - this.state.timeLeft;
const isSpeedBonus = isCorrect && timeElapsed <= this.SPEED_BONUS_TIME;
// 禁用所有按钮
$$('.tq-btn').forEach(btn => {
btn.disabled = true;
btn.style.pointerEvents = 'none';
});
// 高亮正确/错误
$$('.tq-btn').forEach(btn => {
const btnCat = btn.dataset.category;
if (btnCat === item.category) {
btn.classList.add('tq-btn--correct');
if (!isCorrect) btn.classList.add('tq-btn--show-correct');
}
if (btnCat === category && !isCorrect) {
btn.classList.add('tq-btn--wrong');
}
});
if (isCorrect) {
this.state.score++;
this.state.streak++;
if (this.state.streak > this.state.maxStreak) this.state.maxStreak = this.state.streak;
if (isSpeedBonus) this.state.score += this.SPEED_BONUS_POINTS;
this.showFeedback(true, item, isSpeedBonus);
// === 仅计入 ChallengeTracker(答题不产生实际减排) ===
ChallengeTracker.track();
// 不向 CarbonTracker 写入虚假数据
} else {
this.state.streak = 0;
this.showFeedback(false, item, false);
}
this.state.answers.push({ item, guess: category, correct: isCorrect, speedBonus: isSpeedBonus });
this.renderHUD();
// 下一题
this.state.currentIndex++;
if (this.state.currentIndex >= this.state.items.length) {
setTimeout(() => this.endGame(), 1200);
} else {
setTimeout(() => this.showItem(), 1200);
}
},
showFeedback(isCorrect, item, speedBonus) {
const fb = $('#tqFeedback');
if (!fb) return;
fb.style.display = 'flex';
if (isCorrect) {
$('#tqFeedbackIcon').textContent = speedBonus ? '⚡' : '✅';
$('#tqFeedbackText').textContent = speedBonus ? `正确! +${1 + this.SPEED_BONUS_POINTS} 分 (速度奖励!)` : '正确! +1 分';
const fact = ITEM_FUN_FACTS[item.name_en];
$('#tqFeedbackSub').textContent = fact || `${item.name_zh} → ${item.category}`;
} else {
$('#tqFeedbackIcon').textContent = '❌';
$('#tqFeedbackText').textContent = `错了! 正确答案是: ${item.category}`;
$('#tqFeedbackSub').textContent = ITEM_FUN_FACTS[item.name_en] || '';
}
},
resetFeedback() {
const fb = $('#tqFeedback');
if (fb) fb.style.display = 'none';
},
renderHUD() {
const timerEl = $('#tqTimer');
if (timerEl) timerEl.textContent = Math.max(0, this.state.timeLeft);
const scoreEl = $('#tqScore');
if (scoreEl) scoreEl.textContent = this.state.score;
const streakEl = $('#tqStreak');
if (streakEl) streakEl.textContent = '🔥 ' + this.state.streak;
},
endGame() {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
if (!this.state || !this.state.answers) return;
const total = this.state.answers.length;
const correctCount = this.state.answers.filter(a => a.correct).length;
const accuracy = total > 0 ? Math.round((correctCount / total) * 100) : 0;
const score = this.state.score;
const maxStreak = this.state.maxStreak;
// 检查最高分
const data = GameCenter.load();
const isNewRecord = score > (data.timedquiz_highest || 0);
if (isNewRecord) {
data.timedquiz_highest = score;
}
// 游戏币: 答对×2 + 最高连击≥5 +10 + 新纪录+15
let coins = correctCount * 2;
if (maxStreak >= 5) coins += 10;
if (isNewRecord) coins += 15;
data.timedquiz_last = { score, accuracy, maxStreak, correctCount, total, date: new Date().toISOString() };
GameCenter.addCoins(coins);
// 显示结果
$('#tqGame').style.display = 'none';
$('#tqResult').style.display = 'flex';
if (score >= 30) $('#tqResultIcon').textContent = '🏆';
else if (score >= 20) $('#tqResultIcon').textContent = '🎉';
else if (score >= 10) $('#tqResultIcon').textContent = '👍';
else $('#tqResultIcon').textContent = '💪';
$('#tqResultTitle').textContent = isNewRecord ? '新纪录! 太棒了!' : '挑战完成!';
$('#tqResultScore').textContent = score + ' 分';
$('#tqResultNewRecord').style.display = isNewRecord ? 'block' : 'none';
$('#tqStatCorrect').textContent = correctCount;
$('#tqStatTotal').textContent = total;
$('#tqStatAccuracy').textContent = accuracy + '%';
$('#tqStatStreak').textContent = maxStreak;
$('#tqCoinsEarned').textContent = coins;
haptic([10, 30, 10]);
},
init() {
// 开始按钮
const startBtn = $('#tqStartBtn');
if (startBtn) startBtn.addEventListener('click', () => this.startGame());
// 分类按钮
$$('.tq-btn').forEach(btn => {
btn.addEventListener('click', () => {
const cat = btn.dataset.category;
if (cat) this.submitAnswer(cat);
});
});
// 返回按钮
const backBtns = ['tqBack', 'tqResultBack'];
backBtns.forEach(id => {
const el = $(`#${id}`);
if (el) el.addEventListener('click', () => {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
$('#timedQuizScreen').style.display = 'none';
GameCenter.showGameCenter();
});
});
// 再来一次 / 返回
const playAgain = $('#tqPlayAgain');
if (playAgain) playAgain.addEventListener('click', () => this.showStart());
const toGC = $('#tqToGameCenter');
if (toGC) toGC.addEventListener('click', () => {
$('#timedQuizScreen').style.display = 'none';
GameCenter.showGameCenter();
});
// 游戏内退出按钮
const tqGameClose = $('#tqGameClose');
if (tqGameClose) tqGameClose.addEventListener('click', () => {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
$('#timedQuizScreen').style.display = 'none';
GameCenter.showGameCenter();
});
},
};
// ============================================
// 垃圾分类连连看 (MemoryMatch)
// ============================================
const MemoryMatch = {
PAIRS_COUNT: 8,
COLS: 4,
state: null,
timer: null,
showStart() {
[uploadSection, loadingSection, resultSection, errorSection, multiResultSection].forEach(s => {
s.style.display = 'none';
});
const data = GameCenter.load();
const recordEl = $('#memStartRecord');
if (recordEl) {
if (data.memory_besttime) {
recordEl.innerHTML = `最佳纪录: ${data.memory_besttime}s (${data.memory_bestattempts || '?'} 次尝试)`;
} else {
recordEl.textContent = '来创造你的最佳纪录吧!';
}
}
$('#memoryScreen').style.display = 'flex';
$('#memStart').style.display = 'flex';
$('#memGame').style.display = 'none';
$('#memResult').style.display = 'none';
},
startGame() {
// 选取 8 对物品
const shuffled = [...GAME_ITEMS].sort(() => Math.random() - 0.5);
const selected = shuffled.slice(0, this.PAIRS_COUNT);
const emojis = ['♻️', '☣️', '🍚', '🗑️', '📦', '🔋', '🥤', '🧴', '💊', '👕', '📄', '🔧', '🥫', '🧻', '🥢', '💄'];
const catIcons = { '可回收垃圾': '♻️', '有害垃圾': '☣️', '厨余垃圾': '🍚', '其他垃圾': '🗑️' };
// 创建卡牌: 每对 = 1 张物品卡 + 1 张类别卡
const cards = [];
selected.forEach((item, idx) => {
// 物品卡
cards.push({
id: idx * 2,
pairId: idx,
type: 'item',
name_zh: item.name_zh,
emoji: item.emoji || '📦',
category: item.category,
});
// 类别卡
cards.push({
id: idx * 2 + 1,
pairId: idx,
type: 'category',
name_zh: item.category,
emoji: catIcons[item.category] || '🗑️',
category: item.category,
});
});
// 洗牌
cards.sort(() => Math.random() - 0.5);
this.state = {
cards: cards,
flipped: [],
matched: new Set(),
attempts: 0,
matchCount: 0,
startTime: Date.now(),
isLocked: false,
};
$('#memStart').style.display = 'none';
$('#memGame').style.display = 'flex';
$('#memResult').style.display = 'none';
this.renderGrid();
this.updateHUD();
// 计时器
if (this.timer) clearInterval(this.timer);
this.timer = setInterval(() => this.updateTimer(), 1000);
},
renderGrid() {
const grid = $('#memGrid');
if (!grid) return;
grid.innerHTML = this.state.cards.map((card, index) => {
return `
${card.type === 'item' ? `
${card.emoji}
${card.name_zh}
` : `
${card.emoji}
${card.name_zh}
`}
`;
}).join('');
// 绑定点击事件
grid.querySelectorAll('.memory-card').forEach(el => {
el.addEventListener('click', () => {
const index = parseInt(el.dataset.index);
this.flipCard(index);
});
});
},
flipCard(index) {
if (this.state.isLocked) return;
const card = this.state.cards[index];
if (!card) return;
if (this.state.flipped.includes(index)) return;
if (this.state.matched.has(card.pairId)) return;
this.state.flipped.push(index);
const cardEl = document.querySelector(`.memory-card[data-index="${index}"]`);
if (cardEl) cardEl.classList.add('memory-card--flipped');
haptic(4);
if (this.state.flipped.length === 2) {
this.state.attempts++;
this.updateHUD();
this.checkMatch();
}
},
checkMatch() {
this.state.isLocked = true;
const [i1, i2] = this.state.flipped;
const c1 = this.state.cards[i1];
const c2 = this.state.cards[i2];
if (c1.pairId === c2.pairId) {
// 匹配成功!
this.state.matched.add(c1.pairId);
this.state.matchCount++;
const el1 = document.querySelector(`.memory-card[data-index="${i1}"]`);
const el2 = document.querySelector(`.memory-card[data-index="${i2}"]`);
if (el1) el1.classList.add('memory-card--matched');
if (el2) el2.classList.add('memory-card--matched');
this.state.flipped = [];
this.state.isLocked = false;
// 集成
ChallengeTracker.track();
this.updateHUD();
haptic([8, 8, 8]);
if (this.state.matchCount >= this.PAIRS_COUNT) {
setTimeout(() => this.endGame(), 600);
}
} else {
// 不匹配
setTimeout(() => {
const el1 = document.querySelector(`.memory-card[data-index="${i1}"]`);
const el2 = document.querySelector(`.memory-card[data-index="${i2}"]`);
if (el1) el1.classList.remove('memory-card--flipped');
if (el2) el2.classList.remove('memory-card--flipped');
this.state.flipped = [];
this.state.isLocked = false;
haptic(6);
}, 900);
}
},
updateHUD() {
const attempts = $('#memAttempts');
if (attempts) attempts.textContent = this.state.attempts;
const matches = $('#memMatches');
if (matches) matches.textContent = this.state.matchCount + '/' + this.PAIRS_COUNT;
},
updateTimer() {
if (!this.state) return;
const elapsed = Math.floor((Date.now() - this.state.startTime) / 1000);
const timerEl = $('#memTimer');
if (timerEl) timerEl.textContent = elapsed + 's';
},
endGame() {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
const elapsed = Math.floor((Date.now() - this.state.startTime) / 1000);
const data = GameCenter.load();
const isNewTime = !data.memory_besttime || elapsed < data.memory_besttime;
const isNewAttempts = !data.memory_bestattempts || this.state.attempts < data.memory_bestattempts;
if (isNewTime) data.memory_besttime = elapsed;
if (isNewAttempts) data.memory_bestattempts = this.state.attempts;
data.memory_last = { elapsed, attempts: this.state.attempts, date: new Date().toISOString() };
GameCenter.save(data);
$('#memGame').style.display = 'none';
$('#memResult').style.display = 'flex';
$('#memResultNewRecord').style.display = (isNewTime || isNewAttempts) ? 'block' : 'none';
$('#memResultAttempts').textContent = this.state.attempts + ' 次';
$('#memResultTime').textContent = elapsed + 's';
GameCenter.addCoins(this.state.attempts <= 12 ? 15 : 8);
haptic([10, 30, 10]);
},
init() {
const startBtn = $('#memStartBtn');
if (startBtn) startBtn.addEventListener('click', () => this.startGame());
const backBtns = ['memBack', 'memResultBack'];
backBtns.forEach(id => {
const el = $(`#${id}`);
if (el) el.addEventListener('click', () => {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
$('#memoryScreen').style.display = 'none';
GameCenter.showGameCenter();
});
});
const playAgain = $('#memPlayAgain');
if (playAgain) playAgain.addEventListener('click', () => this.showStart());
const toGC = $('#memToGameCenter');
if (toGC) toGC.addEventListener('click', () => {
$('#memoryScreen').style.display = 'none';
GameCenter.showGameCenter();
});
// 游戏内退出按钮
const memGameClose = $('#memGameClose');
if (memGameClose) memGameClose.addEventListener('click', () => {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
$('#memoryScreen').style.display = 'none';
GameCenter.showGameCenter();
});
},
};
if (document.readyState === 'complete') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();