Add distributor group reference pricing
This commit is contained in:
1 parent
3e2585e241
commit
bd787f0bc7
5 files changed
+388
-15
No files matched your search
@@ -57,6 +57,7 @@ export const getSiteModels = () => api.get('/api/dist/site/models');
|
||||
export const getSitePricing = () => api.get('/api/dist/site/pricing');
|
||||
export const getSitePackages = () => api.get('/api/dist/site/packages');
|
||||
export const getSiteKeyGroups = () => api.get('/api/dist/site/key-groups');
|
||||
export const getSiteKeyGroupPricing = (id) => api.get(`/api/dist/site/key-groups/${id}/pricing`);
|
||||
export const getSubDistributorInfo = () => api.get('/api/dist/site/sub-distributor/info');
|
||||
|
||||
// ===== Auth =====
|
||||
|
||||
@@ -180,6 +180,21 @@
|
||||
"tokens.selectedGroup": "Selected Group",
|
||||
"tokens.quickStart": "Quick Start",
|
||||
"tokens.quickStartComment": "# Use your API key with OpenAI-compatible endpoint",
|
||||
"tokens.viewGroupPricing": "View Reference Pricing",
|
||||
"tokens.groupPricingTitle": "Reference Pricing",
|
||||
"tokens.groupPricingSubtitle": "Shows anonymous reference prices and price ranges for this group without exposing provider names.",
|
||||
"tokens.groupPricingNotice": "Actual billing dynamically selects from currently available routes in this group. Prices below are for reference only.",
|
||||
"tokens.groupPricingSearchPlaceholder": "Search models in this group...",
|
||||
"tokens.groupPricingLoading": "Loading group reference pricing...",
|
||||
"tokens.groupPricingNoData": "No reference pricing is available for this group yet.",
|
||||
"tokens.groupPricingNoMatch": "No models match your search.",
|
||||
"tokens.groupPricingReferencePrice": "Reference input / call price",
|
||||
"tokens.groupPricingLines": "Routes",
|
||||
"tokens.groupPricingLineUnitSingle": "route",
|
||||
"tokens.groupPricingLineUnit": "routes",
|
||||
"tokens.groupPricingSamePrice": "same price",
|
||||
"tokens.groupPricingAvailableLines": "Available routes",
|
||||
"tokens.groupPricingAvailableModels": "Available models",
|
||||
|
||||
"config.title": "Config File Export",
|
||||
"config.subtitle": "Select your API Key and model to generate config files",
|
||||
|
||||
@@ -180,6 +180,21 @@
|
||||
"tokens.selectedGroup": "已选分组",
|
||||
"tokens.quickStart": "快速开始",
|
||||
"tokens.quickStartComment": "# 使用 OpenAI 兼容端点调用 API",
|
||||
"tokens.viewGroupPricing": "查看参考价",
|
||||
"tokens.groupPricingTitle": "分组参考价",
|
||||
"tokens.groupPricingSubtitle": "仅展示该分组下的匿名参考价与价格区间,不展示商家名称。",
|
||||
"tokens.groupPricingNotice": "实际计费会在该分组当前可用线路中动态选择,以下价格仅作为参考。",
|
||||
"tokens.groupPricingSearchPlaceholder": "搜索分组内模型...",
|
||||
"tokens.groupPricingLoading": "正在加载分组参考价...",
|
||||
"tokens.groupPricingNoData": "当前分组暂无可展示的参考价。",
|
||||
"tokens.groupPricingNoMatch": "没有匹配的模型。",
|
||||
"tokens.groupPricingReferencePrice": "参考输入价 / 按次价",
|
||||
"tokens.groupPricingLines": "线路",
|
||||
"tokens.groupPricingLineUnitSingle": "条线路",
|
||||
"tokens.groupPricingLineUnit": "条线路",
|
||||
"tokens.groupPricingSamePrice": "同价",
|
||||
"tokens.groupPricingAvailableLines": "可用线路",
|
||||
"tokens.groupPricingAvailableModels": "可用模型",
|
||||
|
||||
"config.title": "配置文件导出",
|
||||
"config.subtitle": "选择您的 API Key 和模型,一键生成配置文件",
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ function getProviderSummary(other) {
|
||||
function getBillingSourceLabel(other, t) {
|
||||
if (!other?.billing_source) return '';
|
||||
if (other.billing_source === 'subscription') {
|
||||
if (other.subscription_source === 'dist_package') return t('分站套餐');
|
||||
if (other.subscription_source === 'dist_package') return t('dashboard.packages');
|
||||
if (other.subscription_source === 'order') return t('主站订阅');
|
||||
if (other.subscription_source === 'admin') return t('后台订阅');
|
||||
return t('订阅');
|
||||
|
||||
+356
-14
@@ -1,11 +1,21 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTokens, createToken, updateToken, deleteToken, getSiteKeyGroups, getTokenSupportedModels } from '../api';
|
||||
import {
|
||||
getTokens,
|
||||
createToken,
|
||||
updateToken,
|
||||
deleteToken,
|
||||
getSiteKeyGroups,
|
||||
getSiteKeyGroupPricing,
|
||||
getTokenSupportedModels,
|
||||
} from '../api';
|
||||
import ConfigExporter from '../components/ConfigExporter';
|
||||
import { useCurrency } from '../context/SiteContext';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Tokens() {
|
||||
const { t } = useTranslation();
|
||||
const { symbol, rate } = useCurrency();
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copiedId, setCopiedId] = useState(null);
|
||||
@@ -16,6 +26,10 @@ export default function Tokens() {
|
||||
|
||||
// Key groups
|
||||
const [keyGroups, setKeyGroups] = useState([]);
|
||||
const [activePricingGroup, setActivePricingGroup] = useState(null);
|
||||
const [groupPricingCache, setGroupPricingCache] = useState({});
|
||||
const [loadingGroupPricingId, setLoadingGroupPricingId] = useState(0);
|
||||
const [groupPricingSearch, setGroupPricingSearch] = useState('');
|
||||
|
||||
// Create modal
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
@@ -62,6 +76,30 @@ export default function Tokens() {
|
||||
setShowCreate(true);
|
||||
};
|
||||
|
||||
const openGroupPricing = async (group) => {
|
||||
setActivePricingGroup(group);
|
||||
setGroupPricingSearch('');
|
||||
if (groupPricingCache[group.id] || loadingGroupPricingId === group.id) {
|
||||
return;
|
||||
}
|
||||
setLoadingGroupPricingId(group.id);
|
||||
try {
|
||||
const res = await getSiteKeyGroupPricing(group.id);
|
||||
if (res.data.success) {
|
||||
setGroupPricingCache((prev) => ({
|
||||
...prev,
|
||||
[group.id]: res.data.data || { items: [], summary: null, group },
|
||||
}));
|
||||
}
|
||||
} catch (e) { /* interceptor */ }
|
||||
setLoadingGroupPricingId(0);
|
||||
};
|
||||
|
||||
const closeGroupPricing = () => {
|
||||
setActivePricingGroup(null);
|
||||
setGroupPricingSearch('');
|
||||
};
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!createName.trim()) {
|
||||
@@ -170,6 +208,26 @@ export default function Tokens() {
|
||||
}
|
||||
};
|
||||
|
||||
const hasGroups = keyGroups.length > 0;
|
||||
const activeGroupPricing = activePricingGroup
|
||||
? groupPricingCache[activePricingGroup.id] || null
|
||||
: null;
|
||||
const filteredGroupPricingItems = useMemo(() => {
|
||||
const items = activeGroupPricing?.items || [];
|
||||
const keyword = groupPricingSearch.trim().toLowerCase();
|
||||
if (!keyword) return items;
|
||||
return items.filter((item) => {
|
||||
const modelName = (item.model_name || '').toLowerCase();
|
||||
const displayName = (item.display_name || '').toLowerCase();
|
||||
const category = (item.category || '').toLowerCase();
|
||||
return (
|
||||
modelName.includes(keyword) ||
|
||||
displayName.includes(keyword) ||
|
||||
category.includes(keyword)
|
||||
);
|
||||
});
|
||||
}, [activeGroupPricing, groupPricingSearch]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
@@ -178,8 +236,6 @@ export default function Tokens() {
|
||||
);
|
||||
}
|
||||
|
||||
const hasGroups = keyGroups.length > 0;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-6 py-10">
|
||||
|
||||
@@ -231,6 +287,7 @@ export default function Tokens() {
|
||||
group={group}
|
||||
parseTags={parseTags}
|
||||
onSelect={openCreateFromGroup}
|
||||
onViewPricing={openGroupPricing}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
@@ -279,6 +336,20 @@ export default function Tokens() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GroupPricingModal
|
||||
open={!!activePricingGroup}
|
||||
group={activePricingGroup}
|
||||
pricingData={activeGroupPricing}
|
||||
items={filteredGroupPricingItems}
|
||||
loading={loadingGroupPricingId === activePricingGroup?.id}
|
||||
search={groupPricingSearch}
|
||||
onSearchChange={setGroupPricingSearch}
|
||||
onClose={closeGroupPricing}
|
||||
symbol={symbol}
|
||||
rate={rate}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* ========== New Key Reveal Modal ========== */}
|
||||
{newKey && (
|
||||
<div className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
@@ -458,7 +529,7 @@ export default function Tokens() {
|
||||
}
|
||||
|
||||
/* ========== Key Group Card ========== */
|
||||
function KeyGroupCard({ group, parseTags, onSelect, t }) {
|
||||
function KeyGroupCard({ group, parseTags, onSelect, onViewPricing, t }) {
|
||||
const tags = parseTags(group.tags);
|
||||
const isUnavailable = group.is_unavailable;
|
||||
|
||||
@@ -466,7 +537,7 @@ function KeyGroupCard({ group, parseTags, onSelect, t }) {
|
||||
<div
|
||||
className={`glass-sm rounded-xl p-4 border border-page-divider transition-all ${
|
||||
isUnavailable
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
? 'opacity-75'
|
||||
: 'hover:border-brand-500/40 cursor-pointer group'
|
||||
}`}
|
||||
onClick={() => !isUnavailable && onSelect(group)}
|
||||
@@ -513,16 +584,287 @@ function KeyGroupCard({ group, parseTags, onSelect, t }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create arrow */}
|
||||
{!isUnavailable && (
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="text-xs font-medium text-brand-500">{t('tokens.create')}</span>
|
||||
<svg className="w-4 h-4 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-end gap-2 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onViewPricing(group);
|
||||
}}
|
||||
className="px-3 py-1.5 text-xs rounded-lg border border-page-divider text-page-secondary hover:bg-page-surface-hover hover:text-page transition-colors"
|
||||
>
|
||||
{t('tokens.viewGroupPricing')}
|
||||
</button>
|
||||
|
||||
{!isUnavailable && (
|
||||
<div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="text-xs font-medium text-brand-500">{t('tokens.create')}</span>
|
||||
<svg className="w-4 h-4 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupPricingModal({
|
||||
open,
|
||||
group,
|
||||
pricingData,
|
||||
items,
|
||||
loading,
|
||||
search,
|
||||
onSearchChange,
|
||||
onClose,
|
||||
symbol,
|
||||
rate,
|
||||
t,
|
||||
}) {
|
||||
if (!open || !group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayGroup = pricingData?.group || group;
|
||||
const summary = pricingData?.summary;
|
||||
const hasItems = (pricingData?.items || []).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="glass rounded-2xl w-full max-w-6xl max-h-[88vh] overflow-hidden"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="px-6 py-5 border-b border-page-divider">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-heading font-semibold text-page">
|
||||
{displayGroup.name} · {t('tokens.groupPricingTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-page-secondary mt-1 max-w-3xl">
|
||||
{t('tokens.groupPricingSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-page-divider text-page-secondary hover:bg-page-surface-hover transition-colors"
|
||||
>
|
||||
{t('tokens.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mt-4">
|
||||
{displayGroup.discount_label && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs font-semibold bg-green-500/10 text-page-success">
|
||||
{displayGroup.discount_label}
|
||||
</span>
|
||||
)}
|
||||
{displayGroup.rmb_per_usd > 0 && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs bg-page-surface text-page-secondary">
|
||||
{displayGroup.rmb_per_usd} {t('tokens.rmbPerUsd')}
|
||||
</span>
|
||||
)}
|
||||
{summary && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs bg-page-surface text-page-secondary">
|
||||
{t('tokens.groupPricingAvailableLines')}: {summary.provider_count}
|
||||
</span>
|
||||
)}
|
||||
{summary && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs bg-page-surface text-page-secondary">
|
||||
{t('tokens.groupPricingAvailableModels')}: {summary.model_count}
|
||||
</span>
|
||||
)}
|
||||
{summary?.provider_limited && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs bg-brand-500/10 text-brand-500">
|
||||
{t('tokens.restrictedByProviders')}
|
||||
</span>
|
||||
)}
|
||||
{summary?.model_limited && (
|
||||
<span className="px-2.5 py-1 rounded-full text-xs bg-brand-500/10 text-brand-500">
|
||||
{t('tokens.restrictedByModels')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayGroup.description && (
|
||||
<p className="text-sm text-page-secondary mt-3">
|
||||
{displayGroup.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-b border-page-divider bg-page-surface/40">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<p className="text-sm text-page-secondary">
|
||||
{t('tokens.groupPricingNotice')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(event) => onSearchChange(event.target.value)}
|
||||
className="input lg:max-w-xs"
|
||||
placeholder={t('tokens.groupPricingSearchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-5 overflow-y-auto max-h-[58vh]">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-page-secondary">
|
||||
<div className="w-4 h-4 border-2 border-brand-500/30 border-t-brand-500 rounded-full animate-spin" />
|
||||
<span>{t('tokens.groupPricingLoading')}</span>
|
||||
</div>
|
||||
) : !hasItems ? (
|
||||
<div className="text-sm text-page-secondary">
|
||||
{t('tokens.groupPricingNoData')}
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="text-sm text-page-secondary">
|
||||
{t('tokens.groupPricingNoMatch')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[860px]">
|
||||
<thead>
|
||||
<tr className="border-b border-page-divider">
|
||||
<th className="text-left px-4 py-3 font-medium text-page-secondary">{t('pricing.model')}</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-page-secondary">{t('定价方式')}</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-page-secondary">{t('tokens.groupPricingReferencePrice')}</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-page-secondary">{t('pricing.outputPrice')}</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-page-secondary">{t('pricing.cacheReadPrice')}</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-page-secondary">{t('pricing.cacheCreationPrice')}</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-page-secondary">{t('tokens.groupPricingLines')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr key={`${item.model_name}:${item.billing_type}`} className="border-b border-page-divider last:border-0 align-top">
|
||||
<td className="px-4 py-3.5">
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-page">{item.display_name || item.model_name}</div>
|
||||
{(item.display_name || item.model_name) !== item.model_name && (
|
||||
<div className="text-xs text-page-muted font-mono mt-1">{item.model_name}</div>
|
||||
)}
|
||||
{item.category && (
|
||||
<div className="text-xs text-page-muted mt-1 uppercase tracking-wide">{item.category}</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-page-secondary">
|
||||
{item.billing_type === 'per_call' ? t('pricing.perCall') : t('按量计费')}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right font-mono text-page-label whitespace-nowrap">
|
||||
{item.status !== 'healthy'
|
||||
? t('pricing.unknown')
|
||||
: item.billing_type === 'per_call'
|
||||
? formatGroupPriceRange(item.fixed_price_min, item.fixed_price_max, symbol, rate, true, t)
|
||||
: formatGroupPriceRange(item.input_price_min, item.input_price_max, symbol, rate, false, t)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right font-mono text-page-label whitespace-nowrap">
|
||||
{item.status !== 'healthy'
|
||||
? '-'
|
||||
: item.billing_type === 'per_call'
|
||||
? '-'
|
||||
: formatGroupPriceRange(item.output_price_min, item.output_price_max, symbol, rate, false, t)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right font-mono text-page-label whitespace-nowrap">
|
||||
{item.status !== 'healthy'
|
||||
? '-'
|
||||
: item.billing_type === 'per_call'
|
||||
? '-'
|
||||
: formatGroupPriceRange(item.cache_read_price_min, item.cache_read_price_max, symbol, rate, false, t)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right font-mono text-page-label whitespace-nowrap">
|
||||
{item.status !== 'healthy'
|
||||
? '-'
|
||||
: item.billing_type === 'per_call'
|
||||
? '-'
|
||||
: formatGroupCachePriceRange(item, symbol, rate, t)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-center">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-page-surface text-page-secondary">
|
||||
{formatRouteCount(item.route_count, item.has_range, t)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatGroupPriceRange(min, max, symbol, rate, perCall, t) {
|
||||
if (min == null && max == null) {
|
||||
return '-';
|
||||
}
|
||||
const low = Number(min ?? max ?? 0);
|
||||
const high = Number(max ?? min ?? 0);
|
||||
const factor = perCall ? rate : rate * 1000;
|
||||
const suffix = perCall ? `/${t('pricing.perCallUnit')}` : '';
|
||||
const lowText = `${symbol}${(low * factor).toFixed(4)}`;
|
||||
const highText = `${symbol}${(high * factor).toFixed(4)}`;
|
||||
if (Math.abs(low - high) <= 1e-9) {
|
||||
return `${lowText}${suffix}`;
|
||||
}
|
||||
return `${lowText} - ${highText}${suffix}`;
|
||||
}
|
||||
|
||||
function formatGroupCachePriceRange(item, symbol, rate, t) {
|
||||
const base = formatGroupPriceRange(
|
||||
item.cache_creation_price_min,
|
||||
item.cache_creation_price_max,
|
||||
symbol,
|
||||
rate,
|
||||
false,
|
||||
t,
|
||||
);
|
||||
if (
|
||||
item.cache_creation_price_1h_min == null &&
|
||||
item.cache_creation_price_1h_max == null
|
||||
) {
|
||||
return base;
|
||||
}
|
||||
const baseMin = Number(item.cache_creation_price_min ?? 0);
|
||||
const baseMax = Number(item.cache_creation_price_max ?? 0);
|
||||
const oneHourMin = Number(item.cache_creation_price_1h_min ?? 0);
|
||||
const oneHourMax = Number(item.cache_creation_price_1h_max ?? 0);
|
||||
if (
|
||||
Math.abs(baseMin - oneHourMin) <= 1e-9 &&
|
||||
Math.abs(baseMax - oneHourMax) <= 1e-9
|
||||
) {
|
||||
return base;
|
||||
}
|
||||
const oneHour = formatGroupPriceRange(
|
||||
item.cache_creation_price_1h_min,
|
||||
item.cache_creation_price_1h_max,
|
||||
symbol,
|
||||
rate,
|
||||
false,
|
||||
t,
|
||||
);
|
||||
return `${t('pricing.cacheCreation5m')} ${base} / ${t('pricing.cacheCreation1h')} ${oneHour}`;
|
||||
}
|
||||
|
||||
function formatRouteCount(routeCount, hasRange, t) {
|
||||
if (!routeCount) {
|
||||
return t('pricing.unknown');
|
||||
}
|
||||
if (routeCount === 1) {
|
||||
return `1 ${t('tokens.groupPricingLineUnitSingle')}`;
|
||||
}
|
||||
if (!hasRange) {
|
||||
return `${routeCount} ${t('tokens.groupPricingLineUnit')} · ${t('tokens.groupPricingSamePrice')}`;
|
||||
}
|
||||
return `${routeCount} ${t('tokens.groupPricingLineUnit')}`;
|
||||
}
|
||||
Reference in new issue
Block a user