Files
duplicati_preview/public/modules/explorer/sidebar.js
T
nanxun fcf56bcbd3 feat: rebuild frontend as file-manager style explorer SPA
将原来的三页跳转结构(控制台/工作台/文件页)重构为单页资源管理器:
左侧数据源盘符树、主区列表/网格双视图、右侧内嵌播放器与详情面板,
管理功能收进侧栏对话框。媒体引擎(media-core + media-sw)原样复用,
仅把 file.js 的播放器逻辑抽成参数驱动组件。默认路由指向 explorer。

- 新增 explorer.html/js 与 modules/explorer/ 六个组件模块
- 删除旧的 index/source/file 页面及其控制器
- styles.css 新增 explorer 布局与三档响应式断点
2026-07-04 14:53:07 +08:00

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;
}
};
}