fix: 优化代码
This commit is contained in:
412
src/main.js
Normal file
412
src/main.js
Normal 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 }));
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user