完成新页面开发
This commit is contained in:
@@ -56,11 +56,31 @@
|
||||
<input id="searchInput" type="search" placeholder="搜索会议、文件或用户" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<select id="typeFilter" class="recordings-select">
|
||||
<option value="all">全部格式</option>
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
<div class="recordings-format-filter" id="typeFilterControl">
|
||||
<select id="typeFilter" class="recordings-select-native" aria-hidden="true" tabindex="-1">
|
||||
<option value="all">全部格式</option>
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
<button id="typeFilterButton" class="recordings-filter-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||
<span id="typeFilterText">全部格式</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div id="typeFilterMenu" class="recordings-filter-menu hidden" role="listbox" aria-label="录制格式筛选">
|
||||
<button class="recordings-filter-option is-active" type="button" role="option" aria-selected="true" data-type-value="all">
|
||||
<span>全部格式</span>
|
||||
<small>所有录制</small>
|
||||
</button>
|
||||
<button class="recordings-filter-option" type="button" role="option" aria-selected="false" data-type-value="mp4">
|
||||
<span>MP4</span>
|
||||
<small>标准视频</small>
|
||||
</button>
|
||||
<button class="recordings-filter-option" type="button" role="option" aria-selected="false" data-type-value="webm">
|
||||
<span>WebM</span>
|
||||
<small>网页录制</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="recordings-content">
|
||||
@@ -127,6 +147,8 @@
|
||||
<tr>
|
||||
<th>文件</th>
|
||||
<th>会议</th>
|
||||
<th>房主</th>
|
||||
<th>参与者</th>
|
||||
<th>大小</th>
|
||||
<th>上传时间</th>
|
||||
<th>操作</th>
|
||||
@@ -179,7 +201,7 @@
|
||||
<input id="editOriginalFilename" class="recordings-input" type="text" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="recordings-label" for="editUserId">用户 ID</label>
|
||||
<label class="recordings-label" for="editUserId">房主用户 ID</label>
|
||||
<input id="editUserId" class="recordings-input" type="text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,10 @@ const elements = {
|
||||
refreshBtn: document.getElementById('refreshBtn'),
|
||||
searchInput: document.getElementById('searchInput'),
|
||||
typeFilter: document.getElementById('typeFilter'),
|
||||
typeFilterControl: document.getElementById('typeFilterControl'),
|
||||
typeFilterButton: document.getElementById('typeFilterButton'),
|
||||
typeFilterText: document.getElementById('typeFilterText'),
|
||||
typeFilterMenu: document.getElementById('typeFilterMenu'),
|
||||
clearSearchBtn: document.getElementById('clearSearchBtn'),
|
||||
uploadForm: document.getElementById('uploadForm'),
|
||||
uploadBtn: document.getElementById('uploadBtn'),
|
||||
@@ -41,6 +45,12 @@ const elements = {
|
||||
notificationText: document.getElementById('notificationText')
|
||||
};
|
||||
|
||||
const typeFilterLabels = {
|
||||
all: '全部格式',
|
||||
mp4: 'MP4',
|
||||
webm: 'WebM'
|
||||
};
|
||||
|
||||
function recordingKey(recording) {
|
||||
return `${recording.meetingId}/${recording.filename}`;
|
||||
}
|
||||
@@ -89,6 +99,87 @@ function formatDate(value) {
|
||||
});
|
||||
}
|
||||
|
||||
function getPersonId(person) {
|
||||
return person?.userId || person?.id || '';
|
||||
}
|
||||
|
||||
function getPersonName(person) {
|
||||
return person?.name || getPersonId(person) || '-';
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
function showNotification(message, isError = false) {
|
||||
elements.notificationText.textContent = message;
|
||||
elements.notification.classList.toggle('recordings-notification-error', isError);
|
||||
@@ -143,7 +234,8 @@ function applyFilters() {
|
||||
recording.meetingId,
|
||||
recording.filename,
|
||||
recording.originalFilename,
|
||||
recording.userId
|
||||
recording.userId,
|
||||
getPeopleSearchText(recording)
|
||||
].join(' ').toLowerCase();
|
||||
return (type === 'all' || extension === type) && (!query || haystack.includes(query));
|
||||
});
|
||||
@@ -169,6 +261,8 @@ function renderTable() {
|
||||
const key = recordingKey(recording);
|
||||
const active = key === state.selectedKey ? 'recordings-row-active' : '';
|
||||
const ext = escapeHtml((recording.filename || '').split('.').pop().toUpperCase());
|
||||
const host = getRecordingHost(recording);
|
||||
const participants = getRecordingParticipants(recording);
|
||||
|
||||
return `
|
||||
<tr class="${active}" data-key="${escapeHtml(key)}">
|
||||
@@ -182,6 +276,8 @@ function renderTable() {
|
||||
</button>
|
||||
</td>
|
||||
<td>${escapeHtml(recording.meetingId)}</td>
|
||||
<td>${escapeHtml(renderPersonSummary(host))}</td>
|
||||
<td>${participants.length}</td>
|
||||
<td>${formatBytes(recording.size)}</td>
|
||||
<td>${formatDate(recording.uploadedAt)}</td>
|
||||
<td>
|
||||
@@ -222,6 +318,8 @@ function selectRecording(recording) {
|
||||
}
|
||||
|
||||
state.selectedKey = recordingKey(recording);
|
||||
const host = getRecordingHost(recording);
|
||||
const participants = getRecordingParticipants(recording);
|
||||
elements.previewVideo.src = recording.streamUrl;
|
||||
elements.previewPlaceholder.classList.add('hidden');
|
||||
elements.previewTitle.textContent = recording.originalFilename || recording.filename;
|
||||
@@ -232,6 +330,14 @@ function selectRecording(recording) {
|
||||
<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>
|
||||
<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>
|
||||
<div class="recordings-preview-actions">
|
||||
<a class="recordings-primary-btn" href="${escapeHtml(recording.downloadUrl)}">
|
||||
<i class="fas fa-download"></i>
|
||||
@@ -349,9 +455,32 @@ function bindEvents() {
|
||||
elements.refreshBtn.addEventListener('click', loadRecordings);
|
||||
elements.searchInput.addEventListener('input', applyFilters);
|
||||
elements.typeFilter.addEventListener('change', applyFilters);
|
||||
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);
|
||||
}
|
||||
});
|
||||
elements.clearSearchBtn.addEventListener('click', () => {
|
||||
elements.searchInput.value = '';
|
||||
elements.typeFilter.value = 'all';
|
||||
setTypeFilter('all');
|
||||
applyFilters();
|
||||
});
|
||||
elements.recordingFile.addEventListener('change', () => {
|
||||
@@ -397,4 +526,5 @@ function bindEvents() {
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
setTypeFilter(elements.typeFilter.value);
|
||||
loadRecordings();
|
||||
|
||||
Reference in New Issue
Block a user