import {api, el, select, input, loading, note, errorBox, link, scriptUrl} from './ui.js'; const roles = {entry: '执行入口', support: '配套文件', self_test: '套件内部验证', variant: '相关实现 / 场景'}; export function relatedFiles(ctx, script) { const content = el('div', {class: 'related-files'}); const panel = el('details', {class: 'panel disclosure'}, el('summary', {}, `相关文件 · ${script.file_count || 1} 个`), content); let loaded = false; let busy = false; async function load() { if (busy || loaded || !panel.open) return; busy = true; content.replaceChildren(loading('正在读取项目文件…')); try { const {items} = await api(`${scriptUrl(script.id)}/files`, {}, ctx.signal); if (!ctx.alive) return; const search = input({type: 'search', placeholder: '按文件名或路径查找', 'aria-label': '搜索相关文件'}); const choice = select('查看相关文件', []); const info = el('p', {class: 'source-info mono'}); const feedback = el('div'); const download = link('下载此文件', '', 'button small'); const code = el('pre', {class: 'code', tabindex: '0', 'aria-label': '相关文件源代码,只读'}); content.replaceChildren(note('这里展示测试入口、配套模块和相关实现。查看文件不会改变本次测试的执行入口。'), search, choice, info, download, feedback, code); let generation = 0; async function show() { const ticket = ++generation; const file = items.find(item => item.id === choice.value); download.hidden = true; feedback.replaceChildren(); info.textContent = file ? `${roles[file.role] || '配套文件'} · ${file.source_path}` : ''; code.textContent = file ? '正在读取…' : '没有匹配的文件。'; if (!file) return; try { const data = await api(`${scriptUrl(file.id)}/code`, {}, ctx.signal); if (!ctx.alive || ticket !== generation) return; code.textContent = data.code || '(空文件)'; download.href = `${scriptUrl(file.id)}/download`; download.hidden = Boolean(data.redacted); if (data.redacted) feedback.append(note('疑似凭据已隐藏,该文件不可下载。')); } catch (error) { if (ctx.alive && ticket === generation) { code.textContent = ''; feedback.replaceChildren(errorBox(error, show)); } } } function filter() { const term = search.value.toLowerCase(); const selected = choice.value; const matching = items.filter(file => (file.name + file.source_path).toLowerCase().includes(term)); choice.replaceChildren(...matching.map(file => el('option', {value: file.id}, `${roles[file.role]} · ${file.relative_path}`))); if (matching.some(file => file.id === selected)) choice.value = selected; show(); } choice.addEventListener('change', show); search.addEventListener('input', filter); loaded = true; filter(); } catch (error) { if (ctx.alive) content.replaceChildren(errorBox(error, load)); } finally { busy = false; } } panel.addEventListener('toggle', load); return panel; }