2026-05-25 17:39:57 +08:00
|
|
|
const state = {
|
|
|
|
|
recordings: [],
|
|
|
|
|
filtered: [],
|
|
|
|
|
selectedKey: '',
|
|
|
|
|
editing: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const elements = {
|
|
|
|
|
refreshBtn: document.getElementById('refreshBtn'),
|
|
|
|
|
searchInput: document.getElementById('searchInput'),
|
|
|
|
|
typeFilter: document.getElementById('typeFilter'),
|
2026-05-25 21:57:58 +08:00
|
|
|
typeFilterControl: document.getElementById('typeFilterControl'),
|
|
|
|
|
typeFilterButton: document.getElementById('typeFilterButton'),
|
|
|
|
|
typeFilterText: document.getElementById('typeFilterText'),
|
|
|
|
|
typeFilterMenu: document.getElementById('typeFilterMenu'),
|
2026-05-25 17:39:57 +08:00
|
|
|
clearSearchBtn: document.getElementById('clearSearchBtn'),
|
|
|
|
|
uploadForm: document.getElementById('uploadForm'),
|
|
|
|
|
uploadBtn: document.getElementById('uploadBtn'),
|
|
|
|
|
recordingFile: document.getElementById('recordingFile'),
|
|
|
|
|
fileNameText: document.getElementById('fileNameText'),
|
|
|
|
|
uploadMeetingId: document.getElementById('uploadMeetingId'),
|
|
|
|
|
uploadUserId: document.getElementById('uploadUserId'),
|
|
|
|
|
totalCount: document.getElementById('totalCount'),
|
|
|
|
|
meetingCount: document.getElementById('meetingCount'),
|
|
|
|
|
storageSize: document.getElementById('storageSize'),
|
|
|
|
|
recordingRootText: document.getElementById('recordingRootText'),
|
|
|
|
|
listSummary: document.getElementById('listSummary'),
|
|
|
|
|
loadingState: document.getElementById('loadingState'),
|
|
|
|
|
emptyState: document.getElementById('emptyState'),
|
|
|
|
|
recordingsTableWrap: document.getElementById('recordingsTableWrap'),
|
|
|
|
|
recordingsTableBody: document.getElementById('recordingsTableBody'),
|
|
|
|
|
previewVideo: document.getElementById('previewVideo'),
|
|
|
|
|
previewPlaceholder: document.getElementById('previewPlaceholder'),
|
|
|
|
|
previewTitle: document.getElementById('previewTitle'),
|
|
|
|
|
previewDetails: document.getElementById('previewDetails'),
|
|
|
|
|
editDialog: document.getElementById('editDialog'),
|
|
|
|
|
editForm: document.getElementById('editForm'),
|
|
|
|
|
editFilenameText: document.getElementById('editFilenameText'),
|
|
|
|
|
editMeetingId: document.getElementById('editMeetingId'),
|
|
|
|
|
editOriginalFilename: document.getElementById('editOriginalFilename'),
|
|
|
|
|
editUserId: document.getElementById('editUserId'),
|
|
|
|
|
closeEditBtn: document.getElementById('closeEditBtn'),
|
|
|
|
|
cancelEditBtn: document.getElementById('cancelEditBtn'),
|
|
|
|
|
notification: document.getElementById('notification'),
|
|
|
|
|
notificationText: document.getElementById('notificationText')
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 21:57:58 +08:00
|
|
|
const typeFilterLabels = {
|
|
|
|
|
all: '全部格式',
|
|
|
|
|
mp4: 'MP4',
|
|
|
|
|
webm: 'WebM'
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 17:39:57 +08:00
|
|
|
function recordingKey(recording) {
|
|
|
|
|
return `${recording.meetingId}/${recording.filename}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value) {
|
|
|
|
|
return String(value || '')
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatBytes(bytes) {
|
|
|
|
|
const value = Number(bytes) || 0;
|
|
|
|
|
if (value < 1024) {
|
|
|
|
|
return `${value} B`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
|
|
|
let size = value / 1024;
|
|
|
|
|
let index = 0;
|
|
|
|
|
while (size >= 1024 && index < units.length - 1) {
|
|
|
|
|
size /= 1024;
|
|
|
|
|
index += 1;
|
|
|
|
|
}
|
|
|
|
|
return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[index]}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDate(value) {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return date.toLocaleString('zh-CN', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 21:57:58 +08:00
|
|
|
function getPersonId(person) {
|
2026-06-02 02:49:47 +08:00
|
|
|
return person?.userId || person?.id || person?.participantId || '';
|
2026-05-25 21:57:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPersonName(person) {
|
2026-06-02 02:49:47 +08:00
|
|
|
return person?.name || person?.displayName || getPersonId(person) || '-';
|
2026-05-25 21:57:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRecordingHost(recording) {
|
|
|
|
|
return recording.host || (recording.userId ? {
|
|
|
|
|
userId: recording.userId,
|
|
|
|
|
id: recording.userId,
|
|
|
|
|
role: 'host'
|
|
|
|
|
} : null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRecordingParticipants(recording) {
|
|
|
|
|
return Array.isArray(recording.participants) ? recording.participants : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPeopleSearchText(recording) {
|
|
|
|
|
const host = getRecordingHost(recording);
|
|
|
|
|
const participants = getRecordingParticipants(recording);
|
|
|
|
|
return [
|
|
|
|
|
host?.participantId,
|
|
|
|
|
host?.userId,
|
|
|
|
|
host?.id,
|
|
|
|
|
host?.name,
|
|
|
|
|
...participants.flatMap(participant => [
|
|
|
|
|
participant.participantId,
|
|
|
|
|
participant.userId,
|
|
|
|
|
participant.id,
|
|
|
|
|
participant.name
|
|
|
|
|
])
|
|
|
|
|
].filter(Boolean).join(' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPersonSummary(person) {
|
|
|
|
|
if (!person) {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const name = getPersonName(person);
|
|
|
|
|
const id = getPersonId(person);
|
|
|
|
|
return id && id !== name ? `${name} (${id})` : name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPeopleList(people) {
|
|
|
|
|
if (!people.length) {
|
|
|
|
|
return '<div class="recordings-person-empty">暂无参与者</div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return people.map((person) => `
|
|
|
|
|
<div class="recordings-person">
|
|
|
|
|
<img src="${escapeHtml(person.avatar || '/images/p2.png')}" alt="">
|
|
|
|
|
<div>
|
|
|
|
|
<strong>${escapeHtml(getPersonName(person))}</strong>
|
|
|
|
|
<span>${escapeHtml(getPersonId(person) || person.participantId || '-')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setTypeFilter(value) {
|
|
|
|
|
const nextValue = typeFilterLabels[value] ? value : 'all';
|
|
|
|
|
elements.typeFilter.value = nextValue;
|
|
|
|
|
elements.typeFilterText.textContent = typeFilterLabels[nextValue];
|
|
|
|
|
|
|
|
|
|
elements.typeFilterMenu.querySelectorAll('[data-type-value]').forEach((option) => {
|
|
|
|
|
const isActive = option.dataset.typeValue === nextValue;
|
|
|
|
|
option.classList.toggle('is-active', isActive);
|
|
|
|
|
option.setAttribute('aria-selected', String(isActive));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setTypeFilterMenuOpen(isOpen) {
|
|
|
|
|
elements.typeFilterControl.classList.toggle('is-open', isOpen);
|
|
|
|
|
elements.typeFilterMenu.classList.toggle('hidden', !isOpen);
|
|
|
|
|
elements.typeFilterButton.setAttribute('aria-expanded', String(isOpen));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 17:39:57 +08:00
|
|
|
function showNotification(message, isError = false) {
|
|
|
|
|
elements.notificationText.textContent = message;
|
|
|
|
|
elements.notification.classList.toggle('recordings-notification-error', isError);
|
|
|
|
|
elements.notification.classList.add('recordings-notification-visible');
|
|
|
|
|
window.clearTimeout(showNotification.timer);
|
|
|
|
|
showNotification.timer = window.setTimeout(() => {
|
|
|
|
|
elements.notification.classList.remove('recordings-notification-visible');
|
|
|
|
|
}, 2600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestJson(url, options = {}) {
|
|
|
|
|
const response = await fetch(url, options);
|
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
|
|
|
if (!response.ok || payload.success === false) {
|
|
|
|
|
throw new Error(payload.message || `请求失败: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setLoading(isLoading) {
|
|
|
|
|
elements.loadingState.classList.toggle('hidden', !isLoading);
|
|
|
|
|
elements.recordingsTableWrap.classList.toggle('hidden', isLoading || state.filtered.length === 0);
|
|
|
|
|
elements.emptyState.classList.toggle('hidden', isLoading || state.filtered.length > 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadRecordings() {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const payload = await requestJson('/api/recordings');
|
|
|
|
|
state.recordings = payload.recordings || [];
|
|
|
|
|
elements.recordingRootText.textContent = payload.root || 'recordings';
|
|
|
|
|
applyFilters();
|
|
|
|
|
const selected = state.filtered.find(item => recordingKey(item) === state.selectedKey) || state.filtered[0];
|
|
|
|
|
selectRecording(selected || null);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
state.recordings = [];
|
|
|
|
|
applyFilters();
|
|
|
|
|
selectRecording(null);
|
|
|
|
|
showNotification(error.message, true);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyFilters() {
|
|
|
|
|
const query = elements.searchInput.value.trim().toLowerCase();
|
|
|
|
|
const type = elements.typeFilter.value;
|
|
|
|
|
|
|
|
|
|
state.filtered = state.recordings.filter((recording) => {
|
|
|
|
|
const extension = (recording.filename || '').split('.').pop().toLowerCase();
|
|
|
|
|
const haystack = [
|
|
|
|
|
recording.meetingId,
|
|
|
|
|
recording.filename,
|
|
|
|
|
recording.originalFilename,
|
2026-05-25 21:57:58 +08:00
|
|
|
recording.userId,
|
|
|
|
|
getPeopleSearchText(recording)
|
2026-05-25 17:39:57 +08:00
|
|
|
].join(' ').toLowerCase();
|
|
|
|
|
return (type === 'all' || extension === type) && (!query || haystack.includes(query));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
elements.clearSearchBtn.classList.toggle('hidden', !query && type === 'all');
|
|
|
|
|
renderSummary();
|
|
|
|
|
renderTable();
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSummary() {
|
|
|
|
|
const meetings = new Set(state.recordings.map(recording => recording.meetingId));
|
|
|
|
|
const totalSize = state.recordings.reduce((sum, recording) => sum + (Number(recording.size) || 0), 0);
|
|
|
|
|
|
|
|
|
|
elements.totalCount.textContent = state.recordings.length;
|
|
|
|
|
elements.meetingCount.textContent = meetings.size;
|
|
|
|
|
elements.storageSize.textContent = formatBytes(totalSize);
|
|
|
|
|
elements.listSummary.textContent = `当前显示 ${state.filtered.length} 条,共 ${state.recordings.length} 条`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTable() {
|
|
|
|
|
elements.recordingsTableBody.innerHTML = state.filtered.map((recording) => {
|
|
|
|
|
const key = recordingKey(recording);
|
|
|
|
|
const active = key === state.selectedKey ? 'recordings-row-active' : '';
|
|
|
|
|
const ext = escapeHtml((recording.filename || '').split('.').pop().toUpperCase());
|
2026-05-25 21:57:58 +08:00
|
|
|
const host = getRecordingHost(recording);
|
|
|
|
|
const participants = getRecordingParticipants(recording);
|
2026-05-25 17:39:57 +08:00
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<tr class="${active}" data-key="${escapeHtml(key)}">
|
|
|
|
|
<td>
|
|
|
|
|
<button class="recordings-file-cell" data-action="select" data-key="${escapeHtml(key)}">
|
|
|
|
|
<span class="recordings-file-icon">${ext}</span>
|
|
|
|
|
<span class="min-w-0">
|
|
|
|
|
<span class="recordings-file-name">${escapeHtml(recording.originalFilename || recording.filename)}</span>
|
|
|
|
|
<span class="recordings-file-sub">${escapeHtml(recording.filename)}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
|
<td>${escapeHtml(recording.meetingId)}</td>
|
2026-05-25 21:57:58 +08:00
|
|
|
<td>${escapeHtml(renderPersonSummary(host))}</td>
|
|
|
|
|
<td>${participants.length}</td>
|
2026-05-25 17:39:57 +08:00
|
|
|
<td>${formatBytes(recording.size)}</td>
|
|
|
|
|
<td>${formatDate(recording.uploadedAt)}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<div class="recordings-actions">
|
|
|
|
|
<button class="recordings-icon-btn" data-action="preview" data-key="${escapeHtml(key)}" title="预览">
|
|
|
|
|
<i class="fas fa-play"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<a class="recordings-icon-btn" href="${escapeHtml(recording.downloadUrl)}" title="下载">
|
|
|
|
|
<i class="fas fa-download"></i>
|
|
|
|
|
</a>
|
|
|
|
|
<button class="recordings-icon-btn" data-action="edit" data-key="${escapeHtml(key)}" title="编辑">
|
|
|
|
|
<i class="fas fa-pen"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="recordings-icon-btn recordings-danger" data-action="delete" data-key="${escapeHtml(key)}" title="删除">
|
|
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findRecording(key) {
|
|
|
|
|
return state.recordings.find(recording => recordingKey(recording) === key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectRecording(recording) {
|
|
|
|
|
if (!recording) {
|
|
|
|
|
state.selectedKey = '';
|
|
|
|
|
elements.previewVideo.removeAttribute('src');
|
|
|
|
|
elements.previewVideo.load();
|
|
|
|
|
elements.previewPlaceholder.classList.remove('hidden');
|
|
|
|
|
elements.previewTitle.textContent = '未选择录制';
|
|
|
|
|
elements.previewDetails.innerHTML = '<p>从左侧列表选择文件后,可播放、下载、编辑或删除。</p>';
|
|
|
|
|
renderTable();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.selectedKey = recordingKey(recording);
|
2026-05-25 21:57:58 +08:00
|
|
|
const host = getRecordingHost(recording);
|
|
|
|
|
const participants = getRecordingParticipants(recording);
|
2026-05-25 17:39:57 +08:00
|
|
|
elements.previewVideo.src = recording.streamUrl;
|
|
|
|
|
elements.previewPlaceholder.classList.add('hidden');
|
|
|
|
|
elements.previewTitle.textContent = recording.originalFilename || recording.filename;
|
|
|
|
|
elements.previewDetails.innerHTML = `
|
|
|
|
|
<div class="recordings-detail-row"><span>会议 ID</span><strong>${escapeHtml(recording.meetingId)}</strong></div>
|
|
|
|
|
<div class="recordings-detail-row"><span>文件名</span><strong>${escapeHtml(recording.filename)}</strong></div>
|
|
|
|
|
<div class="recordings-detail-row"><span>格式</span><strong>${escapeHtml(recording.mimetype)}</strong></div>
|
|
|
|
|
<div class="recordings-detail-row"><span>大小</span><strong>${formatBytes(recording.size)}</strong></div>
|
|
|
|
|
<div class="recordings-detail-row"><span>用户 ID</span><strong>${escapeHtml(recording.userId || '-')}</strong></div>
|
|
|
|
|
<div class="recordings-detail-row"><span>上传时间</span><strong>${formatDate(recording.uploadedAt)}</strong></div>
|
2026-05-25 21:57:58 +08:00
|
|
|
<div class="recordings-people-section">
|
|
|
|
|
<div class="recordings-people-title">房主</div>
|
|
|
|
|
${renderPeopleList(host ? [host] : [])}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="recordings-people-section">
|
|
|
|
|
<div class="recordings-people-title">参与者 (${participants.length})</div>
|
|
|
|
|
${renderPeopleList(participants)}
|
|
|
|
|
</div>
|
2026-05-25 17:39:57 +08:00
|
|
|
<div class="recordings-preview-actions">
|
|
|
|
|
<a class="recordings-primary-btn" href="${escapeHtml(recording.downloadUrl)}">
|
|
|
|
|
<i class="fas fa-download"></i>
|
|
|
|
|
<span>下载</span>
|
|
|
|
|
</a>
|
|
|
|
|
<button class="recordings-secondary-btn" type="button" data-preview-action="edit">编辑</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
renderTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEdit(recording) {
|
|
|
|
|
if (!recording) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.editing = recording;
|
|
|
|
|
elements.editFilenameText.textContent = recording.filename;
|
|
|
|
|
elements.editMeetingId.value = recording.meetingId || '';
|
|
|
|
|
elements.editOriginalFilename.value = recording.originalFilename || recording.filename || '';
|
|
|
|
|
elements.editUserId.value = recording.userId || '';
|
|
|
|
|
elements.editDialog.classList.remove('hidden');
|
|
|
|
|
elements.editDialog.classList.add('flex');
|
|
|
|
|
elements.editMeetingId.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeEdit() {
|
|
|
|
|
state.editing = null;
|
|
|
|
|
elements.editDialog.classList.add('hidden');
|
|
|
|
|
elements.editDialog.classList.remove('flex');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleUpload(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const file = elements.recordingFile.files[0];
|
|
|
|
|
if (!file) {
|
|
|
|
|
showNotification('请选择录制文件', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('recording', file, file.name);
|
|
|
|
|
formData.append('filename', file.name);
|
|
|
|
|
formData.append('meetingId', elements.uploadMeetingId.value.trim());
|
|
|
|
|
formData.append('userId', elements.uploadUserId.value.trim());
|
|
|
|
|
|
|
|
|
|
elements.uploadBtn.disabled = true;
|
|
|
|
|
try {
|
|
|
|
|
await requestJson('/api/recordings', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData
|
|
|
|
|
});
|
|
|
|
|
elements.uploadForm.reset();
|
|
|
|
|
elements.fileNameText.textContent = '选择 MP4 或 WebM 文件';
|
|
|
|
|
showNotification('录制已上传');
|
|
|
|
|
await loadRecordings();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showNotification(error.message, true);
|
|
|
|
|
} finally {
|
|
|
|
|
elements.uploadBtn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleEdit(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!state.editing) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recording = state.editing;
|
|
|
|
|
try {
|
|
|
|
|
const payload = await requestJson(`/api/recordings/${encodeURIComponent(recording.meetingId)}/${encodeURIComponent(recording.filename)}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
meetingId: elements.editMeetingId.value.trim(),
|
|
|
|
|
originalFilename: elements.editOriginalFilename.value.trim(),
|
|
|
|
|
userId: elements.editUserId.value.trim()
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
state.selectedKey = recordingKey(payload.recording);
|
|
|
|
|
closeEdit();
|
|
|
|
|
showNotification('录制信息已更新');
|
|
|
|
|
await loadRecordings();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showNotification(error.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteRecording(recording) {
|
|
|
|
|
if (!recording) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmed = window.confirm(`确定删除录制文件 "${recording.originalFilename || recording.filename}" 吗?`);
|
|
|
|
|
if (!confirmed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await requestJson(`/api/recordings/${encodeURIComponent(recording.meetingId)}/${encodeURIComponent(recording.filename)}`, {
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
});
|
|
|
|
|
if (state.selectedKey === recordingKey(recording)) {
|
|
|
|
|
state.selectedKey = '';
|
|
|
|
|
}
|
|
|
|
|
showNotification('录制已删除');
|
|
|
|
|
await loadRecordings();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showNotification(error.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindEvents() {
|
|
|
|
|
elements.refreshBtn.addEventListener('click', loadRecordings);
|
|
|
|
|
elements.searchInput.addEventListener('input', applyFilters);
|
|
|
|
|
elements.typeFilter.addEventListener('change', applyFilters);
|
2026-05-25 21:57:58 +08:00
|
|
|
elements.typeFilterButton.addEventListener('click', () => {
|
|
|
|
|
setTypeFilterMenuOpen(!elements.typeFilterControl.classList.contains('is-open'));
|
|
|
|
|
});
|
|
|
|
|
elements.typeFilterMenu.addEventListener('click', (event) => {
|
|
|
|
|
const option = event.target.closest('[data-type-value]');
|
|
|
|
|
if (!option) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTypeFilter(option.dataset.typeValue);
|
|
|
|
|
setTypeFilterMenuOpen(false);
|
|
|
|
|
applyFilters();
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener('click', (event) => {
|
|
|
|
|
if (!elements.typeFilterControl.contains(event.target)) {
|
|
|
|
|
setTypeFilterMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
setTypeFilterMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-25 17:39:57 +08:00
|
|
|
elements.clearSearchBtn.addEventListener('click', () => {
|
|
|
|
|
elements.searchInput.value = '';
|
2026-05-25 21:57:58 +08:00
|
|
|
setTypeFilter('all');
|
2026-05-25 17:39:57 +08:00
|
|
|
applyFilters();
|
|
|
|
|
});
|
|
|
|
|
elements.recordingFile.addEventListener('change', () => {
|
|
|
|
|
const file = elements.recordingFile.files[0];
|
|
|
|
|
elements.fileNameText.textContent = file ? file.name : '选择 MP4 或 WebM 文件';
|
|
|
|
|
});
|
|
|
|
|
elements.uploadForm.addEventListener('submit', handleUpload);
|
|
|
|
|
elements.editForm.addEventListener('submit', handleEdit);
|
|
|
|
|
elements.closeEditBtn.addEventListener('click', closeEdit);
|
|
|
|
|
elements.cancelEditBtn.addEventListener('click', closeEdit);
|
|
|
|
|
elements.editDialog.addEventListener('click', (event) => {
|
|
|
|
|
if (event.target === elements.editDialog) {
|
|
|
|
|
closeEdit();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
elements.recordingsTableBody.addEventListener('click', (event) => {
|
|
|
|
|
const control = event.target.closest('[data-action]');
|
|
|
|
|
if (!control) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recording = findRecording(control.dataset.key);
|
|
|
|
|
const action = control.dataset.action;
|
|
|
|
|
if (action === 'select' || action === 'preview') {
|
|
|
|
|
selectRecording(recording);
|
|
|
|
|
if (action === 'preview') {
|
|
|
|
|
elements.previewVideo.play().catch(() => undefined);
|
|
|
|
|
}
|
|
|
|
|
} else if (action === 'edit') {
|
|
|
|
|
openEdit(recording);
|
|
|
|
|
} else if (action === 'delete') {
|
|
|
|
|
deleteRecording(recording);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
elements.previewDetails.addEventListener('click', (event) => {
|
|
|
|
|
const control = event.target.closest('[data-preview-action]');
|
|
|
|
|
if (control && control.dataset.previewAction === 'edit') {
|
|
|
|
|
openEdit(findRecording(state.selectedKey));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bindEvents();
|
2026-05-25 21:57:58 +08:00
|
|
|
setTypeFilter(elements.typeFilter.value);
|
2026-05-25 17:39:57 +08:00
|
|
|
loadRecordings();
|