import { THEME_CSS, escapeHtml, escapeAttr, containsCI, highlightAll, tabNavHTML, aiStatusBadge } from "../render/layout"; import { BUILD_ID } from "../build"; import { getProject, listIssues, listLabelsForIssues, type IssueWithMeta, type LabelRow } from "../store"; import { relTime } from "../render/components"; export const LIST_PAGE_SIZE = 50; function labelChip(label: LabelRow, owner: string, repo: string, state: string): string { const href = `/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?state=${state}&label=${encodeURIComponent(label.name)}`; return `${escapeHtml(label.name)}`; } function issueRow( it: IssueWithMeta, owner: string, repo: string, q: string, state: string, labels: LabelRow[], ): string { const href = `/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${it.number}`; const title = q ? highlightAll(it.title, q) : escapeHtml(it.title); const chips = labels.length ? `${labels.map((l) => labelChip(l, owner, repo, state)).join("")}` : ""; const aiBadge = aiStatusBadge(it.ai_status); const modelChip = it.model ? ` · ${escapeHtml(it.model)}` : ""; // Synced issues carry their upstream (GitHub) author; local ones the poster. const authorChip = it.author ? ` · 👤 ${escapeHtml(it.author)}` : ""; return `
${title}${chips}
#${it.number}${authorChip} · 💬 ${it.comment_count} · ${relTime(it.updated_at)}${aiBadge ? ` · ${aiBadge}` : ""}${modelChip}
`; } export async function buildIssueList( owner: string, repo: string, state: "open" | "closed" | "all", writesEnabled: boolean, q: string, label: string = "", viewer?: { login: string; is_admin: number } | null, ): Promise { const project = await getProject(owner, repo); if (!project) { return notFoundProject(owner, repo); } const issues = await listIssues(project.id, { state, q, label, limit: LIST_PAGE_SIZE }); const labelMap = await listLabelsForIssues(issues.map((it) => it.id)); const matchesFirst = q ? [...issues].sort((a, b) => Number(containsCI(b.title, q)) - Number(containsCI(a.title, q))) : issues; const openActive = state === "open" ? " active" : ""; const closedActive = state === "closed" ? " active" : ""; const allActive = state === "all" ? " active" : ""; const qParam = q ? `&q=${encodeURIComponent(q)}` : ""; const labelParam = label ? `&label=${encodeURIComponent(label)}` : ""; const labelFilterHint = label ? `
标签: ${escapeHtml(label)}
` : ""; const newBtn = writesEnabled ? `+ 新建` : ""; const rows = matchesFirst.length ? matchesFirst.map((it) => issueRow(it, owner, repo, q, state, labelMap.get(it.id) ?? [])).join("") : `
暂无工单
`; const searchVal = escapeAttr(q); return ` ${escapeHtml(owner)}/${escapeHtml(repo)} · Issues
🏠 / ${escapeHtml(owner)}/${escapeHtml(repo)}
${tabNavHTML("issues", viewer ? { login: viewer.login, is_admin: viewer.is_admin } : undefined)}
Open Closed All ${newBtn}
${labelFilterHint}
${rows}
`; } function notFoundProject(owner: string, repo: string): string { return `项目不存在

项目 ${escapeHtml(owner)}/${escapeHtml(repo)} 不存在

← 返回项目列表

`; }