将原来的三页跳转结构(控制台/工作台/文件页)重构为单页资源管理器: 左侧数据源盘符树、主区列表/网格双视图、右侧内嵌播放器与详情面板, 管理功能收进侧栏对话框。媒体引擎(media-core + media-sw)原样复用, 仅把 file.js 的播放器逻辑抽成参数驱动组件。默认路由指向 explorer。 - 新增 explorer.html/js 与 modules/explorer/ 六个组件模块 - 删除旧的 index/source/file 页面及其控制器 - styles.css 新增 explorer 布局与三档响应式断点
98 lines
3.0 KiB
JavaScript
98 lines
3.0 KiB
JavaScript
import { escapeHtml, inferEnhancementClass, inferEnhancementLabel } from '../common.js';
|
|
|
|
// 侧栏数据源树。数据源当作"盘符",可展开显示一层根目录快捷入口。
|
|
// 事件通过回调向编排器上报:onOpenSource / onOpenPath / onContextMenu。
|
|
|
|
export function createSidebar(rootElement, { onOpenSource, onContextMenu }) {
|
|
let sources = [];
|
|
let activeSourceId = null;
|
|
|
|
function statusDotClass(source) {
|
|
// 复用增强状态的语义色:ready/busy/error/idle
|
|
switch (inferEnhancementClass(source)) {
|
|
case 'status-ready':
|
|
return 'ready';
|
|
case 'status-busy':
|
|
return 'busy';
|
|
case 'status-error':
|
|
return 'error';
|
|
default:
|
|
return 'idle';
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
if (sources.length === 0) {
|
|
rootElement.innerHTML = '<div class="explorer-tree-empty">暂无数据源,请从「上传任务库」添加。</div>';
|
|
return;
|
|
}
|
|
|
|
rootElement.innerHTML = sources
|
|
.map((source) => {
|
|
const active = source.id === activeSourceId;
|
|
const label = source.displayName || source.originalFilename || source.id;
|
|
const dot = statusDotClass(source);
|
|
const title = `${label} · ${inferEnhancementLabel(source)}`;
|
|
return `
|
|
<div class="explorer-tree-item${active ? ' active' : ''}"
|
|
data-source-id="${escapeHtml(source.id)}"
|
|
title="${escapeHtml(title)}"
|
|
role="button" tabindex="0">
|
|
<span class="explorer-drive-ico" aria-hidden="true">💽</span>
|
|
<span class="explorer-tree-name">${escapeHtml(label)}</span>
|
|
<span class="explorer-dot ${dot}" title="${escapeHtml(inferEnhancementLabel(source))}"></span>
|
|
</div>
|
|
`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
function findSource(sourceId) {
|
|
return sources.find((source) => source.id === sourceId) ?? null;
|
|
}
|
|
|
|
rootElement.addEventListener('click', (event) => {
|
|
const item = event.target.closest('.explorer-tree-item');
|
|
if (!(item instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
onOpenSource?.(item.dataset.sourceId);
|
|
});
|
|
|
|
rootElement.addEventListener('keydown', (event) => {
|
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
return;
|
|
}
|
|
const item = event.target.closest('.explorer-tree-item');
|
|
if (!(item instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
onOpenSource?.(item.dataset.sourceId);
|
|
});
|
|
|
|
rootElement.addEventListener('contextmenu', (event) => {
|
|
const item = event.target.closest('.explorer-tree-item');
|
|
if (!(item instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
onContextMenu?.(item.dataset.sourceId, { x: event.clientX, y: event.clientY });
|
|
});
|
|
|
|
return {
|
|
setSources(nextSources) {
|
|
sources = Array.isArray(nextSources) ? nextSources : [];
|
|
render();
|
|
},
|
|
setActive(sourceId) {
|
|
activeSourceId = sourceId;
|
|
render();
|
|
},
|
|
getSource: findSource,
|
|
getSources() {
|
|
return sources;
|
|
}
|
|
};
|
|
}
|