diff --git a/internal/web/app/features/pricing.js b/internal/web/app/features/pricing.js index fdc33f5..f586674 100644 --- a/internal/web/app/features/pricing.js +++ b/internal/web/app/features/pricing.js @@ -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(); diff --git a/internal/web/styles/pricing.css b/internal/web/styles/pricing.css index 3bb9488..8e27ec6 100644 --- a/internal/web/styles/pricing.css +++ b/internal/web/styles/pricing.css @@ -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; } diff --git a/internal/web/styles/responsive.css b/internal/web/styles/responsive.css index cb950d5..28eafc3 100644 --- a/internal/web/styles/responsive.css +++ b/internal/web/styles/responsive.css @@ -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; } } diff --git a/internal/web/ui.html b/internal/web/ui.html index dc4ad2f..64f2b13 100644 --- a/internal/web/ui.html +++ b/internal/web/ui.html @@ -69,9 +69,8 @@
-

价格配置

- +