feat(pricing): add read-only model cards
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user