feat: add public h5 dashboard and publish traceability

This commit is contained in:
Wyndham ARR
2026-08-03 13:04:41 +08:00
parent 7e7470821b
commit 2107f00e32
29 changed files with 1047 additions and 71 deletions

View File

@@ -154,7 +154,10 @@
}
if (!response.ok || !payload.ok) {
const error = payload?.error || {};
throw new Error(I18N?.errorMessage(error.code, error.message) || error.message || "请求未完成");
const requestError = new Error(I18N?.errorMessage(error.code, error.message) || error.message || "请求未完成");
requestError.code = error.code || "";
requestError.status = response.status;
throw requestError;
}
return returnEnvelope ? payload : payload.data;
}
@@ -516,7 +519,7 @@
const lines = [
`$ arr trace --job ${job.job_id || state.selectedJobId || "-"}`,
`trace_version=${traceConsoleValue(trace.trace_version)} status=${traceConsoleValue(job.status)} active=${Boolean(job.active)} current_stage=${traceConsoleValue(job.current_stage)}`,
`attempt_no=${traceConsoleValue(job.attempt_no)} remote_run_id=${traceConsoleValue(job.remote_run_id)} delivery_mode=${traceConsoleValue(job.delivery_mode)}`,
`attempt_no=${traceConsoleValue(job.attempt_no)} execution_scope=${traceConsoleValue(job.execution_scope || "unknown")} processor_mode=${traceConsoleValue(job.processor_mode || "unknown")} remote_dispatch=${traceConsoleValue(job.remote_dispatch || "unknown")} remote_run_id=${traceConsoleValue(job.remote_run_id)} delivery_mode=${traceConsoleValue(job.delivery_mode)}`,
`created_at=${traceConsoleValue(job.created_at)} updated_at=${traceConsoleValue(job.updated_at)} finished_at=${traceConsoleValue(job.finished_at)}`,
`evidence=${JSON.stringify(trace.evidence || {})}`,
"--------------------------------------------------------------------------------",
@@ -986,6 +989,52 @@
node.hidden = !message;
}
function setCompanySourceStatus(message = "") {
const node = $("#company-source-status");
node.textContent = I18N?.text(message) || message;
node.hidden = !message;
}
function companySourceFilename(source) {
const filename = String(source?.filename || "").trim();
return filename || (I18N?.t("company.source_file_missing") || "未记录文件名");
}
function companySourceMeta(source) {
const batchId = Number(source?.source_batch_id);
const rows = Number(source?.source_rows);
const rooms = Number(source?.room_quantity);
const batch = Number.isInteger(batchId) && batchId > 0 ? formatInteger(batchId) : "—";
const rowCount = Number.isInteger(rows) && rows >= 0 ? formatInteger(rows) : "—";
const roomCount = Number.isInteger(rooms) && rooms >= 0 ? formatInteger(rooms) : "—";
const activated = source?.activated_at ? formatDate(source.activated_at, true) : (I18N?.t("company.review_no_record") || "暂无");
return I18N?.t("company.source_meta", { batch, rows: rowCount, rooms: roomCount, activated })
|| `批次 #${batch} · ${rowCount} 条记录 · ${roomCount} 间 · 启用于 ${activated}`;
}
function companyJobSourceMeta(source) {
const batchId = Number(source?.source_batch_id);
const batch = Number.isInteger(batchId) && batchId > 0 ? formatInteger(batchId) : "—";
const activated = source?.activated_at ? formatDate(source.activated_at, true) : (I18N?.t("company.review_no_record") || "暂无");
return I18N?.t("company.job_source_meta", { batch, activated })
|| `批次 #${batch} · 启用于 ${activated}`;
}
function renderCompanySource(source) {
const panel = $("#company-source-current");
if (!panel) return;
if (!source) {
panel.hidden = true;
$("#company-source-current-filename").textContent = "";
$("#company-source-current-meta").textContent = "";
return;
}
panel.hidden = false;
$("#company-source-current-filename").textContent = companySourceFilename(source);
$("#company-source-current-filename").title = companySourceFilename(source);
$("#company-source-current-meta").textContent = companySourceMeta(source);
}
function updateCompanySourceUploadControls() {
const busy = state.companySourceUploading || state.companyReviewMutating || isCompanyReportActive();
const enabled = state.companySourceUploadReady && !busy;
@@ -1104,6 +1153,7 @@
function selectCompanySourceFile(file) {
setCompanySourceError();
setCompanySourceStatus();
state.companySourceFile = null;
if (file) {
if (!file.name.toLowerCase().endsWith(".xlsx")) {
@@ -1125,14 +1175,17 @@
async function loadCompanySource(showErrors = false) {
if (!state.companySourceUploadReady) {
state.companySource = null;
renderCompanySource(null);
updateCompanyReportControls();
return;
}
try {
state.companySource = await api("/api/company-reports/source");
renderCompanySource(state.companySource);
setCompanySourceError();
} catch (error) {
state.companySource = null;
renderCompanySource(null);
setCompanySourceError(I18N?.t("company.source_error") || "当前 Excel 数据源暂时无法读取");
if (showErrors) showToast(error.message, true);
}
@@ -1172,6 +1225,7 @@
if (!state.companySourceFile || !state.companySourceUploadReady || state.companySourceUploading) return;
state.companySourceUploading = true;
setCompanySourceError();
setCompanySourceStatus();
updateCompanySourceUploadControls();
updateCompanyReportControls();
try {
@@ -1195,6 +1249,17 @@
? (I18N?.t("company.extract_pending", { count: formatInteger(pending) }) || `提取完成,${formatInteger(pending)} 条需要人工确认`)
: (I18N?.t("company.extract_done") || "提取完成,可以确认并启用"));
} catch (error) {
if (error.code === "BOOKING_EXCEL_SOURCE_ALREADY_ACTIVATED") {
const message = I18N?.t("company.source_already_active") || "这份 Excel 已是当前启用来源;请在下方按月份查看或生成对应报表";
state.companySourceFile = null;
$("#company-excel-file").value = "";
$("#company-selected-file").textContent = I18N?.t("upload.file_not_selected") || "尚未选择文件";
setCompanySourceError();
setCompanySourceStatus(message);
await Promise.all([loadCompanySource(true), loadCompanyReportHistory(true)]);
showToast(message);
return;
}
setCompanySourceError(error.message || I18N?.t("company.file_extract_failed") || "Excel 文件未能完成提取");
showToast(error.message || I18N?.t("company.file_extract_failed") || "Excel 文件未能完成提取", true);
} finally {
@@ -1407,6 +1472,8 @@
state.companyReviewMutating = false;
clearCompanyReviewSelection();
renderCompanyDraft(null);
renderCompanySource(source);
setCompanySourceStatus();
updateCompanyReportControls();
showToast(I18N?.t("company.review_activated") || "人工核对完成Booking 数据源已启用");
} catch (error) {
@@ -1695,6 +1762,16 @@
$("#company-report-job-as-of").textContent = job.as_of_date ? formatDate(job.as_of_date) : (I18N?.t("company.review_no_record") || "暂无");
$("#company-report-job-duration").textContent = formatDurationSeconds(job.duration_seconds);
$("#company-report-warning-count").textContent = I18N?.t("company.report_warning_count", { count: formatInteger(companyWarningCount(job)) }) || `${companyWarningCount(job)}`;
const source = job?.source;
const sourcePanel = $("#company-report-job-source");
sourcePanel.classList.toggle("is-missing", !source);
$("#company-report-job-source-file").textContent = source
? companySourceFilename(source)
: (I18N?.t("company.job_source_missing") || "历史任务未记录来源");
$("#company-report-job-source-file").title = source ? companySourceFilename(source) : "";
$("#company-report-job-source-meta").textContent = source
? companyJobSourceMeta(source)
: (I18N?.t("company.review_no_record") || "暂无");
$("#company-result-month-end-header").textContent = companyPeriodRange(job.report_month, "21-month-end");
renderCompanyResultRows(job);
localStorage.setItem("arr:last-company-report-job", job.job_id);
@@ -1706,18 +1783,24 @@
state.companyReportJobs = jobs;
const body = $("#company-history-rows");
if (!jobs.length) {
body.innerHTML = `<tr><td class="empty-cell" colspan="7">${historyEmptyMarkup("company", "history.company_empty", "暂无公司渠道明细任务")}</td></tr>`;
body.innerHTML = `<tr><td class="empty-cell" colspan="8">${historyEmptyMarkup("company", "history.company_empty", "暂无公司渠道明细任务")}</td></tr>`;
return;
}
body.innerHTML = jobs.map((job) => `<tr>
body.innerHTML = jobs.map((job) => {
const source = job?.source;
const sourceFilename = source ? companySourceFilename(source) : (I18N?.t("company.job_source_missing") || "历史任务未记录来源");
const sourceMeta = source ? companyJobSourceMeta(source) : (I18N?.t("company.review_no_record") || "暂无");
return `<tr>
<td>${formatDate(job.created_at, true)}</td>
<td><strong>${escapeHtml(job.report_month ? monthLabel(job.report_month) : (I18N?.t("company.review_no_record") || "暂无"))}</strong></td>
<td>${escapeHtml(companyCutoffLabel(job))}</td>
<td><span class="status-chip ${companyStateStyle(job.state)}">${escapeHtml(companyStateLabel(job.state))}</span></td>
<td>${companySuccessCount(job)} / 5</td>
<td>${companyWarningCount(job)}</td>
<td><div class="company-history-source"><strong title="${escapeHtml(sourceFilename)}">${escapeHtml(sourceFilename)}</strong><small>${escapeHtml(sourceMeta)}</small></div></td>
<td><button class="company-detail-button" type="button" data-company-job-id="${escapeHtml(job.job_id)}">${escapeHtml(I18N?.t("common.view") || "查看")}</button></td>
</tr>`).join("");
</tr>`;
}).join("");
$$('[data-company-job-id]', body).forEach((button) => button.addEventListener("click", () => loadCompanyReportJob(button.dataset.companyJobId, true)));
}
@@ -1754,7 +1837,7 @@
if (target) await loadCompanyReportJob(target.job_id);
}
} catch (error) {
$("#company-history-rows").innerHTML = `<tr><td class="empty-cell" colspan="7">${escapeHtml(I18N?.t("company.draft_error") || "任务记录暂时无法读取")}</td></tr>`;
$("#company-history-rows").innerHTML = `<tr><td class="empty-cell" colspan="8">${escapeHtml(I18N?.t("company.draft_error") || "任务记录暂时无法读取")}</td></tr>`;
if (restore) showToast(error.message, true);
} finally {
state.companyHistoryLoading = false;
@@ -1888,6 +1971,7 @@
renderPagination("monthly", state.monthlyTotal, state.monthlyOffset, state.monthlyLoading);
if (state.analytics) renderAnalytics(state.analytics);
else resetAnalytics(I18N?.t("bi.empty") || "暂无渠道与房型数据");
renderCompanySource(state.companySource);
if (state.companySourceDraft?.summary) renderCompanyDraft(state.companySourceDraft);
else renderCompanyDraft(null);
renderPagination("company", state.companyReportsTotal, state.companyReportsOffset, state.companyHistoryLoading);