Files
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

163 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { redirectToLogin, redirectToSetup } from './auth-client.js';
export function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export function formatBytes(bytes) {
if (bytes === null || bytes === undefined || Number.isNaN(Number(bytes))) {
return 'Unknown';
}
const numeric = Number(bytes);
if (numeric === 0) {
return '0 B';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = numeric;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex += 1;
}
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
export async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
if (response.status === 401 && window.location.pathname !== '/login.html') {
if (payload.error?.code === 'AUTH_SETUP_REQUIRED') {
redirectToSetup();
} else {
redirectToLogin();
}
}
throw new Error(`${payload.error?.code ?? 'REQUEST_FAILED'}: ${payload.error?.message ?? 'Request failed.'}`);
}
return payload;
}
export function requireQueryParam(name) {
const params = new URLSearchParams(window.location.search);
const value = params.get(name)?.trim();
if (!value) {
throw new Error(`MISSING_${name.toUpperCase()}: Query parameter "${name}" is required.`);
}
return value;
}
export function optionalQueryParam(name, fallback = '') {
const params = new URLSearchParams(window.location.search);
return params.get(name)?.trim() || fallback;
}
export function createFeedbackController(element) {
return {
clear() {
element.hidden = true;
element.className = 'feedback';
element.textContent = '';
},
set(kind, message) {
element.hidden = false;
element.className = `feedback feedback-${kind}`;
element.textContent = message;
}
};
}
export function inferEnhancementLabel(source) {
switch (source?.enhancement?.status) {
case 'ready':
return '已增强';
case 'running':
return '增强中';
case 'queued':
return '排队中';
case 'failed':
return '增强失败';
default:
return '未增强';
}
}
export function inferEnhancementClass(source) {
switch (source?.enhancement?.status) {
case 'ready':
return 'status-ready';
case 'running':
case 'queued':
return 'status-busy';
case 'failed':
return 'status-error';
default:
return 'status-idle';
}
}
export function renderSourceSummaryCard(source, extraActionsHtml = '') {
const title = escapeHtml(source.displayName || source.originalFilename);
const originalFilename = escapeHtml(source.originalFilename);
const matchedHint = source.matchedBackupName ? '任务名来自 Server DB' : '尚未匹配任务名';
const progress =
source.enhancement.totalVolumes > 0
? `${source.enhancement.processedVolumes}/${source.enhancement.totalVolumes}`
: '0/0';
return `
<article class="source-card">
<div class="source-main">
<div>
<p class="source-name">${title}</p>
<p class="source-meta">数据库文件:${originalFilename}</p>
<p class="source-meta">Source ID${escapeHtml(source.id)}</p>
</div>
<span class="status-pill ${inferEnhancementClass(source)}">${inferEnhancementLabel(source)}</span>
</div>
<div class="summary-chip">${escapeHtml(matchedHint)}</div>
<div class="card-grid">
<div class="stat">
<span>上传大小</span>
<strong>${formatBytes(source.fileSize)}</strong>
</div>
<div class="stat">
<span>最新快照</span>
<strong>${escapeHtml(source.latestSnapshot?.timestamp ?? '无')}</strong>
</div>
<div class="stat">
<span>目录浏览</span>
<strong>${source.canBrowse ? '可用' : '不可用'}</strong>
</div>
<div class="stat">
<span>增强进度</span>
<strong>${escapeHtml(progress)}</strong>
</div>
</div>
${extraActionsHtml}
</article>
`;
}
export function assertBrowserFeature(condition, message) {
if (!condition) {
throw new Error(message);
}
}