This commit is contained in:
DelLevin-Home
2026-06-17 21:05:01 +08:00
parent 835e22b6dc
commit d94a9bad1b
13 changed files with 464 additions and 38 deletions

View File

@@ -280,6 +280,56 @@
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
border-radius: 8px; pointer-events: none;
}
#segmentOverlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
border-radius: 8px; pointer-events: none; z-index: 1;
}
.seg-gray {
position: absolute; top: 0; height: 100%;
background: rgba(0,0,0,0.18); pointer-events: none;
}
#playCursor {
position: absolute; top: 0; height: 100%; width: 10px;
transform: translateX(-5px); z-index: 5;
pointer-events: auto; cursor: col-resize;
display: none;
}
#playCursor::after {
content: ''; position: absolute; left: 50%; top: 0; bottom: 0;
width: 2px; transform: translateX(-50%);
background: #e53935; box-shadow: 0 0 4px rgba(229,57,53,0.4);
pointer-events: none;
}
#cursorHandle {
position: absolute; left: 50%; top: -6px; transform: translateX(-50%);
width: 12px; height: 12px; border-radius: 50%;
background: #e53935; border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
pointer-events: none;
}
.player-controls {
display: flex; align-items: center; gap: 10px;
padding: 6px 12px; margin-top: 4px;
background: #fff; border: 1px solid #e0e0e0; border-radius: 8px;
font-size: 12px; color: #555;
}
.player-controls button {
width: 30px; height: 30px; border: none; border-radius: 50%;
background: #0078d4; color: #fff; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 12px; transition: background 0.15s;
}
.player-controls button:hover { background: #005fa3; }
.player-controls .p-time { font-variant-numeric: tabular-nums; min-width: 36px; text-align: center; }
.player-controls input[type="range"] {
flex: 1; height: 4px; -webkit-appearance: none; appearance: none;
background: #e0e0e0; border-radius: 2px; outline: none; cursor: pointer;
}
.player-controls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: #0078d4; cursor: pointer;
}
.cut-marker {
position: absolute; top: 0; width: 24px; height: 100%;
transform: translateX(-50%); z-index: 2; cursor: grab;
@@ -431,6 +481,12 @@
</div>
<div id="timelineArea" style="display:none;">
<div class="timeline-wrap" id="timelineBar"></div>
<div class="player-controls" id="playerControls" style="display:none;">
<button onclick="playerToggle()" title="播放/暂停"><i id="playerIcon" class="fas fa-play"></i></button>
<span class="p-time" id="playerTime">0:00</span>
<input type="range" id="playerSeekBar" min="0" max="1000" value="0" step="1">
<span class="p-time" id="playerTotal">0:00</span>
</div>
<div id="redoWrap" style="display:none;text-align:center;margin-top:8px;">
<button class="action-btn slice" style="width:auto;padding:8px 20px;font-size:13px;" onclick="redoManualSlice()">
<i class="fas fa-redo"></i> 切割点已更改,点击重新切割
@@ -466,6 +522,14 @@
let manualSegChanged = false; // 用户是否手动调整过勾选
let manualSegInit = false; // 新建预览时重置勾选
// 播放器状态
var playerAudio = null;
var playerPlaying = false;
var _playerRAF = null;
var _playerStartSec = 0;
var _playerEndSec = 0;
var _playerMode = 'full'; // 'full' 全曲 | 'seg' 片段
function showMessage(text, type, duration) {
type = type || 'info'; duration = duration || 3000;
var t = document.createElement('div');
@@ -648,14 +712,6 @@
document.getElementById('sliceList').innerHTML = html;
}
function playPreviewSeg(index) {
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
var r = window._previewRanges[index];
var audio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
audio.play();
currentAudio = audio;
}
function toggleSegment(idx) {
var pos = selectedSegments.indexOf(idx);
@@ -696,6 +752,7 @@
if (el) el.textContent = '已选 ' + selectedSegments.length + '/' + total;
var btnSlice = document.getElementById('btnSlice');
if (btnSlice) btnSlice.disabled = selectedSegments.length === 0;
_updateSegmentOverlay();
}
async function doSlice() {
@@ -790,13 +847,8 @@
document.getElementById('sliceList').innerHTML = html;
}
var currentAudio = null;
function playSlice(index) {
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
audio.play();
currentAudio = audio;
}
function downloadSlice(index) {
var a = document.createElement('a');
@@ -902,8 +954,6 @@
// 新预览时重置勾选
if (manualSegInit) {
manualSegInit = false;
manualSegSelected = [];
manualSegChanged = false;
}
var html = '';
@@ -918,8 +968,8 @@
}
var boundaries = [0].concat(cutPoints).concat([audioDuration]);
// 确保勾选数组长度匹配
if (manualSegSelected.length !== boundaries.length - 1) {
// 仅初始化时全选;用户已手动调整勾选则保留
if (manualSegSelected.length !== boundaries.length - 1 && !manualSegChanged) {
manualSegSelected = [];
for (var j = 0; j < boundaries.length - 1; j++) manualSegSelected.push(j);
}
@@ -965,13 +1015,6 @@
markCutPointsChanged();
}
var previewAudio = null;
function playPreview(start, end) {
if (!taskId) return;
if (previewAudio) { previewAudio.pause(); previewAudio = null; }
previewAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
previewAudio.play();
}
function toggleManualSeg(idx) {
var pos = manualSegSelected.indexOf(idx);
@@ -988,6 +1031,7 @@
if (row) row.className = 'seg-row' + (pos < 0 ? ' selected' : ' dimmed');
updateManualSegCount();
updateManualBtn();
_updateSegmentOverlay();
}
function selectManualSegments() {
@@ -995,6 +1039,7 @@
for (var i = 0; i < cutPoints.length + 1; i++) manualSegSelected.push(i);
manualSegChanged = true;
renderCutList();
_updateSegmentOverlay();
}
@@ -1030,7 +1075,9 @@
bar.className = readOnly ? 'readonly' : '';
var html = '<canvas id="waveformCanvas"></canvas>';
var html = '<canvas id="waveformCanvas"></canvas>'
+ '<div id="segmentOverlay"></div>'
+ '<div id="playCursor"><div id="cursorHandle"></div></div>';
// 时间刻度(在波形条上方)
var step = Math.max(1, Math.ceil(audioDuration / 10));
for (var t = 0; t <= audioDuration; t += step) {
@@ -1050,20 +1097,62 @@
// 绘制波形
if (waveformPeaks) drawWaveform();
_updateSegmentOverlay();
if (readOnly) return; // 只读模式不绑定交互事件
// 播放控制条
var pc = document.getElementById('playerControls');
if (pc) { pc.style.display = audioDuration > 0 ? '' : 'none'; }
var pt = document.getElementById('playerTotal');
if (pt) pt.textContent = formatTime(audioDuration);
_showCursorAtCurrent();
// 时间轴空白区域点击 → 添加切割点
// 绑定事件(只绑一次)
if (!bar._bound) {
bar._bound = true;
// 时间轴点击:只读模式 → seek手动模式 → 添加切割点
bar.addEventListener('click', function(e) {
if (bar._suppressClick) { bar._suppressClick = false; return; }
if (bar.classList.contains('readonly')) return;
if (e.target.closest('.cut-marker')) return;
if (e.target.closest('.cut-marker') || e.target.closest('#playCursor')) return;
var rect = bar.getBoundingClientRect();
var sec = (e.clientX - rect.left) / rect.width * audioDuration;
if (bar.classList.contains('readonly')) {
playerSeekTo(sec);
return;
}
// 手动模式:不允许在灰色片段内添加切割点
var ranges = _getSegmentRanges();
var sel = _getSelectedSet();
if (ranges.length && sel.length) {
for (var i = 0; i < ranges.length; i++) {
if (sec >= ranges[i][0] && sec <= ranges[i][1]) {
if (sel.indexOf(i) < 0) return;
break;
}
}
}
if (sec > 0.5 && sec < audioDuration - 0.5) addCutPoint(sec);
});
// 播放游标拖拽
_bindCursorDrag(bar);
// 进度条拖拽 seek
var seekBar = document.getElementById('playerSeekBar');
if (seekBar) {
seekBar.addEventListener('input', function() {
var sec = (parseFloat(this.value) / 1000) * audioDuration;
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (cursor) { cursor.style.display = 'block'; cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%'; }
if (timeEl) timeEl.textContent = formatTime(sec);
if (playerAudio) {
var rel = sec - _playerStartSec;
var dur = _playerEndSec - _playerStartSec;
if (rel >= 0 && rel <= dur) playerAudio.currentTime = rel;
}
});
}
}
// 为每个标记绑定拖动
@@ -1099,6 +1188,245 @@
}
}
// ── 片段灰色覆盖(取消选中的片段变灰) ──
function _getSegmentRanges() {
// 手动模式:从 cutPoints 计算
if (cutMode === 'manual') {
if (cutPoints.length > 0) {
var b = [0].concat(cutPoints.slice().sort(function(a,b){return a-b;})).concat([audioDuration]);
var ranges = [];
for (var i = 0; i < b.length - 1; i++) ranges.push([b[i], b[i+1]]);
return ranges;
}
return [];
}
// 自动模式:用预览区间
if (window._previewRanges && window._previewRanges.length) {
return window._previewRanges;
}
return [];
}
function _getSelectedSet() {
if (manualSegSelected && manualSegSelected.length > 0 && cutPoints.length > 0) {
return manualSegSelected;
}
return selectedSegments;
}
function _updateSegmentOverlay() {
var overlay = document.getElementById('segmentOverlay');
if (!overlay || audioDuration <= 0) return;
var ranges = _getSegmentRanges();
if (!ranges.length) { overlay.innerHTML = ''; return; }
var sel = _getSelectedSet();
var html = '';
for (var i = 0; i < ranges.length; i++) {
if (sel.indexOf(i) < 0) {
var left = (ranges[i][0] / audioDuration * 100).toFixed(2);
var width = ((ranges[i][1] - ranges[i][0]) / audioDuration * 100).toFixed(2);
html += '<div class="seg-gray" style="left:' + left + '%;width:' + width + '%;"></div>';
}
}
overlay.innerHTML = html;
}
// ── 播放器系统 ──
function _showCursorAtCurrent() {
var cursor = document.getElementById('playCursor');
if (!cursor) return;
if (playerAudio) {
var sec = _playerStartSec + playerAudio.currentTime;
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
}
}
function _playerTick() {
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (!playerAudio || !cursor) return;
var sec = _playerStartSec + playerAudio.currentTime;
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
if (timeEl) timeEl.textContent = formatTime(sec);
var seekBar = document.getElementById('playerSeekBar');
if (seekBar && audioDuration > 0) seekBar.value = (sec / audioDuration * 1000).toFixed(0);
_playerRAF = requestAnimationFrame(_playerTick);
}
function playerStartTick() {
playerStopTick();
var cursor = document.getElementById('playCursor');
if (cursor) cursor.style.display = 'block';
_playerRAF = requestAnimationFrame(_playerTick);
}
function playerStopTick() {
if (_playerRAF) { cancelAnimationFrame(_playerRAF); _playerRAF = null; }
}
function playerSeekTo(sec) {
sec = Math.max(0, Math.min(audioDuration, sec));
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (cursor) {
cursor.style.display = 'block';
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
}
if (timeEl) timeEl.textContent = formatTime(sec);
if (playerAudio && !playerAudio.paused) {
var rel = sec - _playerStartSec;
if (rel >= 0 && rel <= _playerEndSec - _playerStartSec) {
playerAudio.currentTime = rel;
}
}
}
function playerToggle() {
if (playerPlaying) {
playerPause();
} else {
playerPlay();
}
}
function playerPlay() {
if (!taskId || audioDuration <= 0) return;
if (playerAudio && !playerAudio.paused) return;
// 恢复暂停的音频
if (playerAudio && playerAudio.paused && playerAudio.currentTime > 0 && playerAudio.duration > 0) {
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
return;
}
// 新建全曲播放
_playerStopAll();
_playerMode = 'full';
_playerStartSec = 0;
_playerEndSec = audioDuration;
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=0&end=' + audioDuration);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
function playerPause() {
if (playerAudio) playerAudio.pause();
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
}
function _playerStopAll() {
playerStopTick();
if (playerAudio) {
playerAudio.pause();
playerAudio.src = '';
playerAudio = null;
}
playerPlaying = false;
_updatePlayerIcon();
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
}
function _updatePlayerIcon() {
var icon = document.getElementById('playerIcon');
if (icon) icon.className = playerPlaying ? 'fas fa-pause' : 'fas fa-play';
}
// 播放片段(自动模式列表按钮)
function playPreviewSeg(index) {
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
_playerStopAll();
var r = window._previewRanges[index];
_playerMode = 'seg';
_playerStartSec = r[0];
_playerEndSec = r[1];
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
// 播放片段(手动模式列表按钮)
function playPreview(start, end) {
if (!taskId) return;
_playerStopAll();
_playerMode = 'seg';
_playerStartSec = start;
_playerEndSec = end;
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
// 播放已切片结果
function playSlice(index) {
_playerStopAll();
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
audio.play();
currentAudio = audio;
}
// 游标拖拽
function _bindCursorDrag(bar) {
var cursor = document.getElementById('playCursor');
if (!cursor) return;
var dragging = false;
cursor.addEventListener('pointerdown', function(e) {
e.preventDefault();
e.stopPropagation();
dragging = true;
cursor.setPointerCapture(e.pointerId);
});
cursor.addEventListener('pointermove', function(e) {
if (!dragging) return;
var rect = bar.getBoundingClientRect();
var sec = (e.clientX - rect.left) / rect.width * audioDuration;
sec = Math.max(0, Math.min(audioDuration, sec));
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
var timeEl = document.getElementById('playerTime');
if (timeEl) timeEl.textContent = formatTime(sec);
// 同步 audio currentTime
if (playerAudio) {
var rel = sec - _playerStartSec;
var dur = _playerEndSec - _playerStartSec;
if (rel >= 0 && rel <= dur) {
playerAudio.currentTime = rel;
}
}
});
cursor.addEventListener('pointerup', function(e) {
if (!dragging) return;
dragging = false;
cursor.releasePointerCapture(e.pointerId);
bar._suppressClick = true;
});
}
async function loadWaveform() {
if (!taskId) return;
try {