fix: 优化代码

This commit is contained in:
Daniel
2026-03-08 00:19:29 +08:00
parent 321fad8a0d
commit 7d7f13d009
19 changed files with 1534 additions and 388 deletions

412
src/main.js Normal file
View File

@@ -0,0 +1,412 @@
/**
* 全景页业务逻辑,依赖全局 pannellum由 index.html 中 lib/pannellum.js 提供)
*/
(function () {
var API = '/api';
var container = document.getElementById('panorama');
var errorEl = document.getElementById('error');
var currentViewer = null;
var currentBlobUrls = [];
var viewerId = null;
var statsInterval = null;
var danmakuLastId = 0;
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c === 'y' ? 0x8 : 0x3;
return (v | r).toString(16);
});
}
function revokeBlobUrls() {
currentBlobUrls.forEach(function (u) {
try {
URL.revokeObjectURL(u);
} catch (e) {}
});
currentBlobUrls = [];
}
function showError(msg) {
errorEl.style.display = 'flex';
errorEl.innerHTML = '<p>' + (msg || '加载失败') + '</p>';
}
function hideError() {
errorEl.style.display = 'none';
}
function destroyViewer() {
if (currentViewer) {
try {
currentViewer.destroy();
} catch (e) {}
currentViewer = null;
}
revokeBlobUrls();
if (container) container.innerHTML = '';
}
function buildViewer(config) {
destroyViewer();
hideError();
if (container) container.style.display = 'block';
config.autoLoad = config.autoLoad !== false;
config.hfov = config.hfov || 100;
config.minHfov = config.minHfov != null ? config.minHfov : 50;
config.maxHfov = config.maxHfov != null ? config.maxHfov : 120;
config.showZoomCtrl = false;
config.compass = false;
config.showFullscreenCtrl = false;
try {
currentViewer = pannellum.viewer('panorama', config);
currentViewer.on('error', function (err) {
showError('全景图加载失败:' + (err && err.message ? err.message : '未知错误'));
});
} catch (e) {
showError('创建查看器失败:' + (e.message || e));
}
}
function updateStatsUI(stats) {
if (!stats) return;
var w = document.getElementById('watchingNow');
if (w) w.textContent = (stats.watchingNow || 0) + ' 人在看';
var v = document.getElementById('viewCount');
if (v) v.textContent = '共 ' + formatNum(stats.viewCount || 0) + ' 次播放';
var l = document.getElementById('likeCount');
if (l) l.textContent = formatNum(stats.likeCount || 0);
var s = document.getElementById('shareCount');
if (s) s.textContent = formatNum(stats.shareCount || 0);
var c = document.getElementById('commentCount');
if (c) c.textContent = formatNum(stats.commentCount || 0);
}
function formatNum(n) {
n = Number(n);
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
return String(n);
}
function showLikeFloat() {
var wrap = document.getElementById('likeFloatWrap');
if (!wrap) return;
var el = document.createElement('span');
el.className = 'like-float';
el.textContent = '+1';
wrap.appendChild(el);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 950);
}
function fetchStats(cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', API + '/stats', true);
xhr.onload = function () {
if (xhr.status === 200) {
try {
var s = JSON.parse(xhr.responseText);
updateStatsUI(s);
if (cb) cb(s);
} catch (e) {}
}
};
xhr.onerror = function () {
if (cb) cb(null);
};
xhr.send();
}
function sendView() {
var xhr = new XMLHttpRequest();
xhr.open('POST', API + '/view', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
try {
updateStatsUI(JSON.parse(xhr.responseText));
} catch (e) {}
}
};
xhr.send('{}');
}
function sendJoin() {
viewerId = sessionStorage.getItem('pano_viewer_id') || uuid();
sessionStorage.setItem('pano_viewer_id', viewerId);
var xhr = new XMLHttpRequest();
xhr.open('POST', API + '/join', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
try {
var r = JSON.parse(xhr.responseText);
updateStatsUI({ watchingNow: r.watchingNow });
} catch (e) {}
}
};
xhr.send(JSON.stringify({ viewerId: viewerId }));
}
function sendLeave() {
if (!viewerId) return;
navigator.sendBeacon &&
navigator.sendBeacon(API + '/leave', JSON.stringify({ viewerId: viewerId }));
viewerId = null;
}
function addDanmakuLine(text) {
var wrap = document.getElementById('danmaku-wrap');
if (!wrap || !wrap.classList.contains('danmaku-on')) return;
var line = document.createElement('div');
line.className = 'danmaku-line';
line.textContent = text || '';
line.style.top = (2 + Math.random() * 16) + '%';
line.style.animationDuration = 12 + Math.random() * 8 + 's';
wrap.appendChild(line);
setTimeout(function () {
if (line.parentNode) line.parentNode.removeChild(line);
}, 25000);
}
function loadDanmaku() {
var xhr = new XMLHttpRequest();
xhr.open('GET', API + '/comments?limit=50', true);
xhr.onload = function () {
if (xhr.status !== 200) return;
try {
var list = JSON.parse(xhr.responseText);
var wrap = document.getElementById('danmaku-wrap');
if (!wrap) return;
list.forEach(function (c, i) {
var line = document.createElement('div');
line.className = 'danmaku-line';
line.textContent = c.content || '';
line.style.top = (2 + (i % 6) * 3 + Math.random() * 2) + '%';
line.style.animationDuration = 14 + (i % 5) + 's';
line.style.animationDelay = i * 0.8 + 's';
wrap.appendChild(line);
});
danmakuLastId = list.length ? list[list.length - 1].id : 0;
} catch (e) {}
};
xhr.send();
}
function applyBackendConfig(cfg) {
var wrap = document.getElementById('danmaku-wrap');
if (!wrap) return;
if (cfg && cfg.danmakuEnabled === true) {
wrap.classList.add('danmaku-on');
loadDanmaku();
} else {
wrap.classList.remove('danmaku-on');
}
}
function updateUIFromConfig(config) {
var authorEl = document.getElementById('authorName');
if (authorEl && config.authorName) {
authorEl.textContent = config.authorName;
if (config.authorUrl) authorEl.href = config.authorUrl;
}
introText =
config.intro != null && config.intro !== '' ? String(config.intro) : '';
}
function loadFromConfig(cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'config.json', true);
xhr.onload = function () {
if (xhr.status !== 200) {
if (cb) cb(null);
return;
}
var config;
try {
config = JSON.parse(xhr.responseText);
} catch (e) {
if (cb) cb(null);
return;
}
config.autoLoad = config.autoLoad !== false;
var toCheck =
config.panorama ||
(config.cubeMap && config.cubeMap[0]) ||
'panorama/panorama.jpg';
var check = new XMLHttpRequest();
check.open('HEAD', toCheck, true);
check.onload = function () {
if (check.status === 200) {
buildViewer(config);
updateUIFromConfig(config);
if (cb) cb(config);
} else if (cb) cb(null);
};
check.onerror = function () {
if (cb) cb(null);
};
check.send();
};
xhr.onerror = function () {
if (cb) cb(null);
};
xhr.send();
}
function initApp() {
loadFromConfig(function (config) {
if (!config) {
showError('无法加载 config.json 或图片,请通过 http 访问。');
return;
}
fetchStats();
sendView();
sendJoin();
statsInterval = setInterval(fetchStats, 8000);
});
}
fetch('api.config.json')
.catch(function () {
return { json: function () { return Promise.resolve({ apiBase: '' }); } };
})
.then(function (r) {
return typeof r.json === 'function' ? r.json() : Promise.resolve({ apiBase: '' });
})
.then(function (apiConfig) {
API = (apiConfig.apiBase || '').replace(/\/$/, '') + '/api';
var xhr = new XMLHttpRequest();
xhr.open('GET', API + '/config', true);
xhr.onload = function () {
if (xhr.status === 200) {
try {
applyBackendConfig(JSON.parse(xhr.responseText));
} catch (e) {}
}
initApp();
};
xhr.onerror = function () {
initApp();
};
xhr.send();
});
window.addEventListener('beforeunload', sendLeave);
window.addEventListener('pagehide', sendLeave);
document.getElementById('btnFullscreen').addEventListener('click', function () {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(function () {});
} else {
document.exitFullscreen();
}
});
var introText = '';
document.getElementById('btnIntro').addEventListener('click', function () {
if (introText) {
document.getElementById('introContent').textContent = introText;
document.getElementById('introModal').classList.remove('hide');
} else {
alert('暂无简介。可在 config.json 的 intro 字段中填写。');
}
});
document.getElementById('introModalClose').addEventListener('click', function () {
document.getElementById('introModal').classList.add('hide');
});
document.getElementById('btnShare').addEventListener('click', function () {
var url = location.href;
if (navigator.share) {
navigator.share({ title: document.title, url: url }).catch(function () {
copyUrl(url);
});
} else {
copyUrl(url);
}
var xhr = new XMLHttpRequest();
xhr.open('POST', API + '/share', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
try {
updateStatsUI({ shareCount: JSON.parse(xhr.responseText).shareCount });
} catch (e) {}
}
};
xhr.send('{}');
});
function copyUrl(url) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(url)
.then(function () {
alert('链接已复制');
})
.catch(function () {
prompt('复制链接:', url);
});
} else {
prompt('复制链接:', url);
}
}
document.getElementById('btnLike').addEventListener('click', function () {
showLikeFloat();
var xhr = new XMLHttpRequest();
xhr.open('POST', API + '/like', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
try {
var r = JSON.parse(xhr.responseText);
updateStatsUI({ likeCount: r.likeCount });
} catch (e) {}
}
};
xhr.send('{}');
});
var modal = document.getElementById('commentModal');
var commentContent = document.getElementById('commentContent');
document.getElementById('btnComment').addEventListener('click', function () {
commentContent.value = '';
modal.classList.remove('hide');
commentContent.focus();
});
document.getElementById('commentCancel').addEventListener('click', function () {
modal.classList.add('hide');
});
document.getElementById('commentSubmit').addEventListener('click', function () {
var content = (commentContent.value || '').trim();
if (!content) {
alert('请输入留言内容');
return;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', API + '/comments', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
modal.classList.add('hide');
if (xhr.status === 200) {
try {
var c = JSON.parse(xhr.responseText);
addDanmakuLine(c.content);
fetchStats();
} catch (e) {}
} else {
alert('发送失败');
}
};
xhr.onerror = function () {
alert('发送失败');
};
xhr.send(JSON.stringify({ content: content }));
});
})();