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

View File

@@ -36,10 +36,14 @@
}
async function api(path, options = {}) {
const allowUnauthorized = Boolean(options.allowUnauthorized);
const requestOptions = { ...options };
delete requestOptions.allowUnauthorized;
const headers = new Headers(options.headers || {});
if (options.method && options.method !== "GET") headers.set("X-ARR-CSRF", csrf);
const response = await fetch(path, { ...options, headers, credentials: "same-origin" });
const response = await fetch(path, { ...requestOptions, headers, credentials: "same-origin" });
if (response.status === 401) {
if (allowUnauthorized) return null;
redirectToLogin();
throw new Error(I18N?.errorMessage("SESSION_INVALID", "登录状态已失效") || "登录状态已失效");
}
@@ -130,7 +134,7 @@
async function load() {
const month = $("#h5-month").value || monthNow();
try {
render(await api(`/api/analytics?month=${encodeURIComponent(month)}`));
render(await api(`/api/public/h5/analytics?month=${encodeURIComponent(month)}`));
} catch (error) {
reset(I18N?.t("bi.empty") || "该月份暂无可用看板数据");
notify(error.message);
@@ -145,10 +149,10 @@
async function boot() {
const connection = $("#h5-connection");
try {
const session = await api("/api/session");
csrf = session.csrf_token;
const session = await api("/api/session", { allowUnauthorized: true });
csrf = session?.csrf_token || "";
const logout = $("#h5-logout");
if (session.username) {
if (session?.username) {
logout.hidden = false;
logout.removeAttribute("aria-disabled");
logout.classList.remove("is-busy");
@@ -169,11 +173,14 @@
});
}
} catch (_) {
return;
// Session lookup is optional for the public read-only dashboard.
}
try {
const health = await api("/api/health");
const databaseReady = Boolean(health.database_ready);
const healthResponse = await fetch("/healthz", {
credentials: "same-origin",
cache: "no-store",
});
const databaseReady = healthResponse.ok;
connection.classList.add(databaseReady ? "ready" : "error");
connection.lastChild.textContent = databaseReady
? ""
@@ -190,7 +197,7 @@
connection.setAttribute("aria-label", I18N?.t("status.service_disconnected") || "数据服务未连接");
}
let months = [];
try { months = await api("/api/months"); } catch (_) { /* handled by load */ }
try { months = await api("/api/public/h5/months"); } catch (_) { /* handled by load */ }
const select = $("#h5-month");
select.innerHTML = months.length
? months.map((item) => `<option value="${escapeHtml(item.month_key)}">${escapeHtml(item.month_key)}</option>`).join("")

View File

@@ -265,6 +265,15 @@
"company.period_not_finished": ["周期未结束", "Period not complete", "รอบยังไม่สิ้นสุด"],
"company.period_finished": ["周期已结束", "Period complete", "รอบสิ้นสุดแล้ว"],
"company.source_error": ["当前 Excel 数据源暂时无法读取", "The current Excel source is temporarily unavailable", "ไม่สามารถอ่านแหล่ง Excel ปัจจุบันได้ชั่วคราว"],
"company.current_source": ["当前启用来源", "Active source", "แหล่งข้อมูลที่เปิดใช้งาน"],
"company.source_active": ["已启用", "Active", "เปิดใช้งานแล้ว"],
"company.source_meta": ["批次 #{batch} · {rows} 条记录 · {rooms} 间 · 启用于 {activated}", "Batch #{batch} · {rows} records · {rooms} rooms · Activated {activated}", "ชุดข้อมูล #{batch} · {rows} รายการ · {rooms} ห้อง · เปิดใช้งานเมื่อ {activated}"],
"company.source_file_missing": ["未记录文件名", "File name not recorded", "ไม่ได้บันทึกชื่อไฟล์"],
"company.source_already_active": ["这份 Excel 已是当前启用来源;请在下方按月份查看或生成对应报表", "This Excel is already the active source. Review or generate the corresponding report by month below.", "Excel นี้เป็นแหล่งข้อมูลที่เปิดใช้งานอยู่แล้ว ตรวจสอบหรือสร้างรายงานตามเดือนได้ด้านล่าง"],
"company.job_source": ["来源 Excel提交时", "Source Excel (at submission)", "แหล่ง Excel (ขณะส่งงาน)"],
"company.history_source": ["来源 Excel", "Source Excel", "แหล่ง Excel"],
"company.job_source_missing": ["历史任务未记录来源", "Source not recorded for this historical job", "งานย้อนหลังนี้ไม่ได้บันทึกแหล่งข้อมูล"],
"company.job_source_meta": ["批次 #{batch} · 启用于 {activated}", "Batch #{batch} · Activated {activated}", "ชุดข้อมูล #{batch} · เปิดใช้งานเมื่อ {activated}"],
"company.draft_error": ["当前提取结果暂时无法读取", "The current extraction is temporarily unavailable", "ไม่สามารถอ่านผลการแยกปัจจุบันได้ชั่วคราว"],
"company.file_xlsx": ["请选择 .xlsx 格式的 Excel 文件", "Choose an .xlsx Excel file", "เลือกไฟล์ Excel รูปแบบ .xlsx"],
"company.file_empty": ["Excel 文件为空", "The Excel file is empty", "ไฟล์ Excel ว่างเปล่า"],
@@ -404,6 +413,7 @@
"error.review_request_invalid": ["人工确认请求无效", "The review request is invalid", "คำขอตรวจสอบไม่ถูกต้อง"],
"error.review_item_invalid": ["待确认记录编号无效", "Invalid review item ID", "รหัสรายการตรวจสอบไม่ถูกต้อง"],
"error.company_request_invalid": ["渠道明细请求字段无效", "Invalid company detail request fields", "ฟิลด์คำขอรายละเอียดบริษัทไม่ถูกต้อง"],
"error.company_source_already_active": ["这份 Excel 已经启用,无需重复提取", "This Excel is already active; no duplicate extraction is needed", "Excel นี้เปิดใช้งานอยู่แล้ว ไม่จำเป็นต้องแยกข้อมูลซ้ำ"],
"error.company_period_invalid": ["期间必须是 01-10、11-20 或 21-month-end", "Period must be 01-10, 11-20 or 21-month-end", "ช่วงเวลาต้องเป็น 01-10, 11-20 หรือ 21-month-end"],
"error.company_job_not_found": ["任务不存在", "The job does not exist", "ไม่พบงาน"],
"error.download_too_large": ["文件超过下载限制", "The file exceeds the download limit", "ไฟล์มีขนาดเกินขีดจำกัดการดาวน์โหลด"],
@@ -504,6 +514,7 @@
BOOKING_EXCEL_PROCESSING_FAILED: "error.booking_extract_failed",
BOOKING_EXCEL_REVIEW_OPEN: "error.company_review_open",
BOOKING_EXCEL_SOURCE_REQUIRED: "error.company_source_required",
BOOKING_EXCEL_SOURCE_ALREADY_ACTIVATED: "error.company_source_already_active",
COMPANY_REPORT_MONTH_IN_FUTURE: "error.company_future",
COMPANY_REPORT_ALREADY_RUNNING: "error.company_running",
COMPANY_REPORT_GENERATOR_UNAVAILABLE: "error.company_unavailable",

View File

@@ -224,6 +224,14 @@
<div class="company-card-heading">
<h3 id="company-source-title">上传 Excel 报表</h3>
</div>
<div class="company-source-current" id="company-source-current" hidden>
<div class="company-source-current-top">
<span class="company-source-current-label">当前启用来源</span>
<span class="status-chip success">已启用</span>
</div>
<strong id="company-source-current-filename"></strong>
<span id="company-source-current-meta"></span>
</div>
<div class="company-source-action">
<label class="company-excel-dropzone" id="company-excel-dropzone" for="company-excel-file">
<input id="company-excel-file" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
@@ -241,6 +249,7 @@
</button>
</div>
<p class="company-source-error" id="company-source-error" role="alert" hidden></p>
<p class="company-source-status" id="company-source-status" role="status" aria-live="polite" hidden></p>
</section>
<button class="company-period-button" type="button" data-company-period="01-10" disabled>
@@ -329,6 +338,13 @@
<strong id="company-report-success-count">0 / 5</strong>
</div>
</div>
<div class="company-job-source" id="company-report-job-source">
<span class="company-job-source-label">来源 Excel提交时</span>
<div>
<strong id="company-report-job-source-file">历史任务未记录来源</strong>
<small id="company-report-job-source-meta"></small>
</div>
</div>
<div class="company-progress-wrap">
<div><span id="company-report-progress-label">等待生成</span><strong id="company-report-progress-percent">0%</strong></div>
<div class="company-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="company-report-progress-track"><span id="company-report-progress-bar"></span></div>
@@ -366,8 +382,8 @@
</div>
<div class="table-scroll">
<table class="company-history-table">
<thead><tr><th>提交时间(曼谷)</th><th>报表月份</th><th>截止期间</th><th>状态</th><th>已生成</th><th>需复核</th><th>详情</th></tr></thead>
<tbody id="company-history-rows"><tr><td class="empty-cell" colspan="7">正在读取公司渠道明细任务…</td></tr></tbody>
<thead><tr><th>提交时间(曼谷)</th><th>报表月份</th><th>截止期间</th><th>状态</th><th>已生成</th><th>需复核</th><th>来源 Excel</th><th>详情</th></tr></thead>
<tbody id="company-history-rows"><tr><td class="empty-cell" colspan="8">正在读取公司渠道明细任务…</td></tr></tbody>
</table>
</div>
<nav class="pagination-bar" aria-label="生成记录分页">

View File

@@ -366,6 +366,11 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.company-report-card-row > * { min-width: 0; }
.company-source-card { min-width: 0; min-height: 224px; padding: 20px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid #dbe3ef; border-radius: 16px; background: var(--surface); box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 18px 44px rgba(31, 51, 82, .07); }
.company-card-heading h3 { margin: 0; font-size: 17px; line-height: 1.3; letter-spacing: -.025em; }
.company-source-current { margin: 14px 0 12px; padding: 10px 12px; display: grid; gap: 5px; border: 1px solid #d7e8dc; border-radius: 10px; background: #f7fbf8; }
.company-source-current-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.company-source-current-label { color: var(--muted); font-size: 10px; font-weight: 750; }
.company-source-current strong { color: var(--ink); font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
.company-source-current > span:last-child { color: var(--muted); font-size: 10px; line-height: 1.4; }
.company-source-action { width: 100%; margin-top: auto; display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
.company-excel-dropzone { position: relative; min-width: 0; min-height: 92px; padding: 17px 18px; display: flex; align-items: center; gap: 14px; border: 1px dashed #9fb5dc; border-radius: 12px; background: #fff; cursor: pointer; transition: border-color .16s ease, background-color .16s ease, box-shadow .16s ease; }
.company-excel-dropzone:hover, .company-excel-dropzone.is-over { border-color: var(--blue); background: #f6f9ff; box-shadow: inset 0 0 0 1px rgba(37, 99, 235, .08); }
@@ -383,6 +388,7 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.company-upload-button:active:not(:disabled) { box-shadow: 0 4px 12px rgba(37, 99, 235, .16); transform: translateY(0) scale(.985); }
.company-upload-button:disabled { border-color: #d7dde7; color: #667085; background: #e9edf3; box-shadow: none; cursor: not-allowed; }
.company-source-error { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; color: #b42318; background: #feeceb; font-size: 11px; font-weight: 650; }
.company-source-status { margin: 12px 0 0; padding: 10px 12px; border: 1px solid #cfe1f9; border-radius: 9px; color: #1554a0; background: #f3f8ff; font-size: 11px; font-weight: 650; line-height: 1.45; }
.company-review-panel { margin: 0 24px 24px; overflow: hidden; border: 1px solid #d7dfec; border-radius: 14px; background: #fff; box-shadow: 0 1px 2px rgba(16, 24, 40, .04); }
.company-review-header { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line); background: #fbfcfe; }
.company-review-title-block { min-width: 0; flex: 1 1 auto; display: grid; gap: 6px; }
@@ -508,6 +514,11 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.company-progress-wrap > div:first-child strong { color: var(--ink); }
.company-progress-track { height: 7px; margin-top: 7px; overflow: hidden; border-radius: 99px; background: #edf1f6; }
.company-progress-track span { width: 100%; height: 100%; display: block; border-radius: inherit; background: var(--blue); transform: scaleX(0); transform-origin: left; transition: transform .25s ease; }
.company-job-source { margin-top: 16px; padding: 11px 13px; display: flex; align-items: flex-start; gap: 10px; border: 1px solid #dbe3ef; border-radius: 10px; background: #fbfcfe; }
.company-job-source.is-missing { background: #fafafa; }
.company-job-source-label { flex: 0 0 auto; padding-top: 2px; color: var(--muted); font-size: 10px; font-weight: 750; }
.company-job-source strong { display: block; color: var(--ink); font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
.company-job-source small { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; line-height: 1.35; }
.company-job-meta { margin: 17px 0 0; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.company-job-meta > div { min-width: 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-soft); }
.company-job-meta dt { color: var(--muted); font-size: 10px; }
@@ -534,7 +545,10 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.company-result-row.is-failed:hover td { background: #feeceb; }
.company-history-panel { margin-top: 20px; }
.company-history-count { color: var(--muted); font-size: 11px; }
.company-history-table { min-width: 820px; }
.company-history-table { min-width: 980px; }
.company-history-source { min-width: 150px; display: grid; gap: 2px; }
.company-history-source strong { font-size: 10px; line-height: 1.35; overflow-wrap: anywhere; }
.company-history-source small { color: var(--muted); font-size: 9px; line-height: 1.35; }
.company-detail-button { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-strong); border-radius: 7px; color: var(--blue); background: #fff; font-size: 10px; font-weight: 750; cursor: pointer; }
.company-detail-button:hover { border-color: #a9c1f5; background: var(--blue-soft); }
.page-footer { width: min(1400px, calc(100% - 48px)); margin: -38px auto 28px; display: flex; justify-content: flex-start; gap: 20px; color: var(--muted); font-size: 10px; }
@@ -692,6 +706,7 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.company-job-panel { padding-left: 18px; padding-right: 18px; }
.company-job-header { align-items: flex-start; flex-direction: column; gap: 14px; }
.company-job-summary { justify-items: start; }
.company-job-source { align-items: flex-start; }
.company-job-meta { grid-template-columns: 1fr 1fr; }
.company-result-scroll { margin-left: 0; margin-right: 0; overflow: visible; }
.company-result-table, .company-result-table tbody { min-width: 0; display: block; }