feat(pricing): add read-only model cards

This commit is contained in:
2026-08-19 19:52:21 +08:00
parent 2933d077b3
commit 48c0ebd9ed
4 changed files with 87 additions and 5 deletions
+67 -1
View File
@@ -37,7 +37,7 @@ export async function loadPricing() {
if (selected) fillPriceForm(selected);
else if (selectedPriceModel) clearPriceForm(false);
else showPriceEditor(false);
statusNode.textContent = currentPrices.length ? `已配置 ${currentPrices.length} 个模型` : "尚未配置模型价格";
statusNode.textContent = "";
if (isManagementAuthorized()) await loadCatalogStatus();
else {
currentCatalog = null;
@@ -53,6 +53,7 @@ function updateAccessState(authorized) {
document.querySelectorAll("#view-pricing .price-editor input, #view-pricing .price-editor select").forEach(node => {
if (node.id !== "catalog-query") node.disabled = !authorized;
});
renderPrices();
syncLongSectionVisibility();
}
@@ -104,6 +105,10 @@ function renderPrices() {
listNode.replaceChildren(Object.assign(document.createElement("div"), { className: "empty", textContent: "尚未配置模型" }));
return;
}
if (!isManagementAuthorized()) {
listNode.replaceChildren(...currentPrices.map(renderPriceCard));
return;
}
listNode.replaceChildren(...currentPrices.map(price => {
const row = document.createElement("div");
row.className = "price-row" + (price.model === selectedPriceModel ? " selected" : "");
@@ -129,6 +134,67 @@ function renderPrices() {
}));
}
function renderPriceCard(price) {
const card = document.createElement("article");
card.className = "model-price-card";
const header = document.createElement("div");
header.className = "model-price-card-header";
const name = document.createElement("h2");
name.textContent = price.model;
const meta = document.createElement("div");
meta.className = "model-price-meta";
if (price.long_context) meta.append(priceBadge("长上下文"));
if (price.fast_pricing_enabled) meta.append(priceBadge(`Fast ×${price.fast_multiplier || "2.5"}`));
header.append(name, meta);
const table = document.createElement("div");
table.className = "model-price-table";
["", "输入", "缓存读取", "缓存写入", "输出"].forEach(label => table.append(priceCell(label, "model-price-column")));
appendPriceRow(table, "标准", price.base);
if (price.long_context) {
const comparison = price.long_context.comparison === "gte" ? "≥" : ">";
appendPriceRow(table, `长上下文\n${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`, price.long_context, true);
} else {
table.append(
priceCell("长上下文", "model-price-row-label model-price-last-row"),
priceCell("未配置", "model-price-unavailable model-price-last-row")
);
}
card.append(header, table);
return card;
}
function appendPriceRow(table, labelText, values, last = false) {
const lastClass = last ? " model-price-last-row" : "";
table.append(priceCell(labelText, "model-price-row-label" + lastClass));
["input_per_1m", "cache_read_per_1m", "cache_write_per_1m", "output_per_1m"].forEach(field => {
table.append(priceCell(`$${values[field] ?? "0"}`, "model-price-value" + lastClass));
});
}
function priceCell(text, className) {
const cell = document.createElement("div");
cell.className = className;
cell.textContent = text;
return cell;
}
function priceBadge(text) {
const badge = document.createElement("span");
badge.textContent = text;
return badge;
}
function compactTokens(value) {
const number = Number(value);
if (!Number.isFinite(number)) return "-";
if (number >= 1000000) return `${number / 1000000}M`;
if (number >= 1000) return `${number / 1000}K`;
return String(number);
}
async function savePrice() {
try {
const payload = pricePayload();
+17
View File
@@ -34,3 +34,20 @@
.fast-row { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(130px, 220px); align-items: end; gap: 16px; }
.fast-row .inline-check { align-self: center; color: var(--text); }
.price-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 3px; }
.read-only #view-pricing .price-layout { display: block; min-height: 0; border: 0; background: transparent; box-shadow: none; }
.read-only #view-pricing .price-sidebar { padding: 0; border: 0; background: transparent; }
.read-only #view-pricing .price-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.read-only #view-pricing .price-editor { display: none; }
.model-price-card { min-width: 0; overflow: hidden; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-primary); }
.model-price-card-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 49px; padding: 12px 14px; border-bottom: 1px solid var(--border-soft); }
.model-price-card-header h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.model-price-meta { display: flex; flex: 0 0 auto; gap: 5px; }
.model-price-meta span { padding: 3px 6px; border: 1px solid var(--border); border-radius: 5px; color: var(--text-secondary); font-size: 8px; white-space: nowrap; }
.model-price-table { display: grid; grid-template-columns: minmax(72px, .9fr) repeat(4, minmax(65px, 1fr)); }
.model-price-table > div { display: flex; align-items: center; min-width: 0; min-height: 35px; padding: 8px 10px; border-right: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); }
.model-price-table > div:nth-child(5n) { border-right: 0; }
.model-price-column { color: var(--muted); font-size: 8px; }
.model-price-row-label { color: var(--muted); font-size: 8px; line-height: 1.35; white-space: pre-line; }
.model-price-value { justify-content: flex-end; font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; }
.model-price-last-row { min-height: 39px !important; border-bottom: 0 !important; }
.model-price-unavailable { grid-column: span 4; justify-content: center; border-right: 0 !important; color: var(--muted); font-size: 9px; }
+2 -2
View File
@@ -1,3 +1,3 @@
@media (max-width: 1100px) { .usage-filters { grid-template-columns: repeat(3, minmax(140px, 1fr)); } }
@media (max-width: 900px) { .price-layout { grid-template-columns: 220px minmax(0, 1fr); } .price-grid, .price-grid.long-grid { grid-template-columns: repeat(2, minmax(130px, 1fr)); } .chart-grid, .user-card-grid { grid-template-columns: 1fr; } }
@media (max-width: 680px) { .app-shell { padding-top: 14px; } .workspace-bar, body.demo-mode .workspace-bar { align-items: stretch; flex-wrap: wrap; padding: 0 12px; } .workspace-nav { order: 2; width: 100%; padding: 0; } .topbar-actions { order: 1; width: 100%; justify-content: flex-end; padding-top: 8px; } .demo-perspective button { padding: 6px 8px; } .auth-box { flex-wrap: wrap; justify-content: flex-end; } #key { width: 128px; } main { padding: 14px 12px 22px; } .section-heading { align-items: stretch; flex-direction: column; } .section-heading .tools { justify-content: flex-start; } .user-management-header { align-items: flex-start; } .price-layout { grid-template-columns: 1fr; } .price-sidebar { border-right: 0; border-bottom: 1px solid var(--border-soft); } .price-grid, .price-grid.long-grid, .field-row, .fast-row { grid-template-columns: 1fr; } .drawer { width: 100vw; } .pagination { align-items: flex-start; flex-direction: column; } .usage-filters { grid-template-columns: 1fr 1fr; } .usage-filter.request-filter, .usage-filter-actions { grid-column: span 2; } }
@media (max-width: 900px) { .price-layout { grid-template-columns: 220px minmax(0, 1fr); } .price-grid, .price-grid.long-grid { grid-template-columns: repeat(2, minmax(130px, 1fr)); } .chart-grid, .user-card-grid, .read-only #view-pricing .price-list { grid-template-columns: 1fr; } }
@media (max-width: 680px) { .app-shell { padding-top: 14px; } .workspace-bar, body.demo-mode .workspace-bar { align-items: stretch; flex-wrap: wrap; padding: 0 12px; } .workspace-nav { order: 2; width: 100%; padding: 0; } .topbar-actions { order: 1; width: 100%; justify-content: flex-end; padding-top: 8px; } .demo-perspective button { padding: 6px 8px; } .auth-box { flex-wrap: wrap; justify-content: flex-end; } #key { width: 128px; } main { padding: 14px 12px 22px; } .section-heading { align-items: stretch; flex-direction: column; } .section-heading .tools { justify-content: flex-start; } .user-management-header { align-items: flex-start; } .price-layout { grid-template-columns: 1fr; } .price-sidebar { border-right: 0; border-bottom: 1px solid var(--border-soft); } .price-grid, .price-grid.long-grid, .field-row, .fast-row { grid-template-columns: 1fr; } .model-price-card-header { align-items: flex-start; flex-direction: column; gap: 7px; } .model-price-table { grid-template-columns: minmax(62px, .9fr) repeat(4, minmax(54px, 1fr)); } .model-price-table > div { padding: 7px 6px; } .drawer { width: 100vw; } .pagination { align-items: flex-start; flex-direction: column; } .usage-filters { grid-template-columns: 1fr 1fr; } .usage-filter.request-filter, .usage-filter-actions { grid-column: span 2; } }
+1 -2
View File
@@ -69,9 +69,8 @@
</section>
<section id="view-pricing" class="view">
<div class="section-heading"><div><h1>价格配置</h1></div><div id="price-status" class="status"></div></div>
<div class="surface price-panel price-layout">
<aside class="price-sidebar"><div class="price-sidebar-header"><h2>已配置模型</h2><button id="new-price" class="small admin-only" type="button">新增</button></div><div id="price-list" class="price-list"></div></aside>
<aside class="price-sidebar"><div class="price-sidebar-header admin-only"><div><h2>已配置模型</h2><div id="price-status" class="status"></div></div><button id="new-price" class="small" type="button">新增</button></div><div id="price-list" class="price-list"></div></aside>
<section class="price-editor">
<div id="price-editor-content" class="hidden">
<div class="price-editor-header"><h2>模型价格</h2></div>