Compare commits

...

6 Commits

Author SHA1 Message Date
mini
b989c50317 feat(pricing): add PricingView + calculator with bilingual i18n
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / golangci-lint (pull_request) Has been cancelled
Security Scan / backend-security (pull_request) Has been cancelled
Security Scan / frontend-security (pull_request) Has been cancelled
Port Pricing.html verbatim to Vue: hero with preview pill, 4-tier grid,
custom-amount slider, PricingCalculator subcomponent, works-everywhere
grid, FAQ accordions, final CTA. Full zh/en pricing namespace (~200 keys
each). SOON chip on Scale priority feature; zero-log FAQ uses inline
parenthetical. Drop $5 bonus line; Enterprise → mailto, Binding/tiers →
/register, docs link → /register/docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:56:18 +08:00
mini
13bdd8f892 fix(docs): restore dashboard link in models.note (was dropped during i18n) 2026-04-21 01:44:04 +08:00
mini
73b3980711 feat(portal): i18n-ify DocsView + auth narrative panels
Extract all Chinese from DocsView.vue into docs.* namespace and add
auth.narrative.* sub-namespace for LoginView/RegisterView narrative slots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:42:32 +08:00
mini
fc7e27671d feat(landing): extract i18n keys + add English translations
Replaces all rendered Chinese strings in LandingView with $t() calls and
<i18n-t> interpolation components; adds landing namespace (62 leaf keys) to
both zh.ts and en.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:30:23 +08:00
mini
6328881801 feat(portal): mount PuroLocaleSwitcher in Landing/Docs/AuthLayout 2026-04-20 21:36:41 +08:00
mini
e711a20373 feat(i18n): add PuroLocaleSwitcher for portal pages 2026-04-20 21:24:51 +08:00
11 changed files with 1687 additions and 115 deletions

View File

@@ -2,6 +2,10 @@
<div class="auth-shell" :class="{ 'auth-shell-split': hasNarrative }"> <div class="auth-shell" :class="{ 'auth-shell-split': hasNarrative }">
<div class="bg-glow soft"></div> <div class="bg-glow soft"></div>
<div v-if="hasNarrative" class="auth-locale-corner">
<PuroLocaleSwitcher />
</div>
<!-- LEFT: Narrative (split mode only, hidden on mobile) --> <!-- LEFT: Narrative (split mode only, hidden on mobile) -->
<aside v-if="hasNarrative" class="auth-narrative"> <aside v-if="hasNarrative" class="auth-narrative">
<slot name="narrative"></slot> <slot name="narrative"></slot>
@@ -42,6 +46,7 @@
import { computed, onMounted, useSlots } from 'vue' import { computed, onMounted, useSlots } from 'vue'
import { useAppStore } from '@/stores' import { useAppStore } from '@/stores'
import { sanitizeUrl } from '@/utils/url' import { sanitizeUrl } from '@/utils/url'
import PuroLocaleSwitcher from '@/components/puro/PuroLocaleSwitcher.vue'
const appStore = useAppStore() const appStore = useAppStore()
@@ -78,6 +83,12 @@ onMounted(() => {
font-family: var(--font-sans); font-family: var(--font-sans);
min-height: 100vh; min-height: 100vh;
} }
.auth-shell-split .auth-locale-corner {
position: absolute;
top: 24px;
right: 24px;
z-index: 20;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.auth-shell-split { .auth-shell-split {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -0,0 +1,117 @@
<template>
<div class="calc">
<div class="calc-left">
<div class="section-kicker">{{ $t('pricing.calc.kicker') }}</div>
<h3>{{ $t('pricing.calc.title') }}</h3>
<p class="sub">{{ $t('pricing.calc.sub') }}</p>
<div class="calc-controls">
<div class="slider-row">
<div class="s-top">
<span>{{ $t('pricing.calc.reqLabel') }}</span>
<span class="val">{{ reqValFormatted }}</span>
</div>
<input type="range" min="500" max="50000" step="500" v-model.number="req">
</div>
<div class="slider-row">
<div class="s-top">
<span>{{ $t('pricing.calc.tokLabel') }}</span>
<span class="val">{{ tokValFormatted }}</span>
</div>
<input type="range" min="500" max="10000" step="500" v-model.number="tok">
</div>
<div class="slider-row">
<div class="s-top">
<span>{{ $t('pricing.calc.mixLabel') }}</span>
<span class="val">{{ mix }}%</span>
</div>
<input type="range" min="0" max="100" step="10" v-model.number="mix">
</div>
</div>
</div>
<div class="calc-right">
<div class="breakdown">
<div class="line"><span class="lab">{{ $t('pricing.calc.monthlyTok') }}</span><span class="v">{{ monthlyTokFmt }}</span></div>
<div class="line"><span class="lab">{{ $t('pricing.calc.officialCost') }}</span><span class="v">${{ officialCostFmt }}</span></div>
<div class="line"><span class="lab">{{ $t('pricing.calc.puroCost') }}</span><span class="v">${{ puroCostFmt }}</span></div>
<div class="line savings"><span class="lab">{{ $t('pricing.calc.savings') }}</span><span class="v">${{ saveFmt }} · {{ savePct }}%</span></div>
</div>
<div class="total-line">
<div>
<div class="lab">{{ $t('pricing.calc.recLabel') }}</div>
<div class="rec-note">{{ recNote }}</div>
</div>
<div class="big"><span class="curr">$</span>{{ recAmt }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const req = ref(5000)
const tok = ref(3000)
const mix = ref(50)
const reqValFormatted = computed(() => req.value.toLocaleString())
const tokValFormatted = computed(() => tok.value.toLocaleString())
const monthlyTok = computed(() => req.value * tok.value * 30)
const official = computed(() => {
const avg = (mix.value / 100) * 6 + (1 - mix.value / 100) * 3
return (monthlyTok.value / 1e6) * avg
})
const puro = computed(() => official.value * 0.3)
const save = computed(() => official.value - puro.value)
const savePct = computed(() => Math.round((save.value / official.value) * 100))
function fmtNum(n: number): string {
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B'
if (n >= 1e6) return (n / 1e6).toFixed(0) + 'M'
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k'
return String(n)
}
function fmtMoney(n: number): string {
return Math.round(n).toLocaleString('en-US')
}
const monthlyTokFmt = computed(() => fmtNum(monthlyTok.value))
const officialCostFmt = computed(() => fmtMoney(official.value))
const puroCostFmt = computed(() => fmtMoney(puro.value))
const saveFmt = computed(() => fmtMoney(save.value))
const recAmt = computed(() => Math.ceil(puro.value))
const recNote = computed(() => {
if (puro.value < 30) return t('pricing.calc.recStarter')
if (puro.value < 80) return t('pricing.calc.recPro')
return t('pricing.calc.recScale')
})
</script>
<style scoped>
.calc { border: 1px solid var(--border); border-radius: var(--r-xl); background: radial-gradient(600px 300px at 0% 0%, rgba(34,211,238,0.06), transparent 60%), radial-gradient(600px 300px at 100% 100%, rgba(168,85,247,0.06), transparent 60%), rgba(15, 23, 42, 0.4); padding: 32px 36px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center; }
.calc-left h3 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 8px; }
.calc-left .sub { color: var(--text-2); font-size: 14px; line-height: 1.55; margin-bottom: 22px; }
.calc-controls { display: flex; flex-direction: column; gap: 14px; }
.slider-row .s-top { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; align-items: baseline; }
.slider-row .s-top .val { font-family: var(--font-mono); font-weight: 700; color: var(--cyan); }
.slider-row input[type=range] { -webkit-appearance: none; width: 100%; height: 4px; background: var(--border); border-radius: 2px; outline: none; }
.slider-row input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--cyan); cursor: pointer; box-shadow: 0 0 0 4px rgba(34,211,238,0.15); }
.calc-right { background: rgba(2, 6, 23, 0.6); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 28px; }
.calc-right .breakdown { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
.calc-right .line { display: flex; justify-content: space-between; font-size: 13px; }
.calc-right .line .lab { color: var(--text-2); }
.calc-right .line .v { font-family: var(--font-mono); color: var(--text-0); }
.calc-right .line.savings .v { color: var(--green); }
.calc-right .total-line { padding-top: 14px; border-top: 1px dashed var(--border); display: flex; justify-content: space-between; align-items: baseline; }
.calc-right .total-line .lab { font-size: 12px; color: var(--text-3); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.14em; }
.calc-right .rec-note { font-size: 12px; color: var(--text-3); margin-top: 4px; }
.calc-right .total-line .big { font-family: var(--font-mono); font-size: 28px; font-weight: 800; color: var(--cyan); letter-spacing: -0.02em; }
.calc-right .total-line .big .curr { font-size: 14px; color: var(--text-3); font-weight: 500; margin-right: 2px; }
@media (max-width: 960px) {
.calc { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div class="puro-locale" ref="dropdownRef">
<button
class="puro-locale-btn"
type="button"
:disabled="switching"
:aria-expanded="isOpen"
:title="currentLocale?.name"
@click="toggleDropdown"
>
<span class="puro-locale-code">{{ currentLocale?.code.toUpperCase() }}</span>
<svg class="puro-locale-chev" :class="{ open: isOpen }" viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
<path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<transition name="puro-locale-pop">
<div v-if="isOpen" class="puro-locale-menu" role="listbox">
<button
v-for="option in availableLocales"
:key="option.code"
type="button"
role="option"
:aria-selected="option.code === currentLocaleCode"
class="puro-locale-option"
:class="{ active: option.code === currentLocaleCode }"
:disabled="switching"
@click="selectLocale(option.code)"
>
<span class="puro-locale-option-code">{{ option.code.toUpperCase() }}</span>
<span class="puro-locale-option-name">{{ option.name }}</span>
<span v-if="option.code === currentLocaleCode" class="puro-locale-option-dot" aria-hidden="true"></span>
</button>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { setLocale, availableLocales } from '@/i18n'
const { locale } = useI18n()
const isOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
const switching = ref(false)
const currentLocaleCode = computed(() => locale.value)
const currentLocale = computed(() => availableLocales.find((l) => l.code === locale.value))
function toggleDropdown() {
isOpen.value = !isOpen.value
}
async function selectLocale(code: string) {
if (switching.value || code === currentLocaleCode.value) {
isOpen.value = false
return
}
switching.value = true
try {
await setLocale(code)
isOpen.value = false
} finally {
switching.value = false
}
}
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
isOpen.value = false
}
}
onMounted(() => document.addEventListener('click', handleClickOutside))
onBeforeUnmount(() => document.removeEventListener('click', handleClickOutside))
</script>
<style scoped>
.puro-locale { position: relative; display: inline-flex; }
.puro-locale-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 10px;
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 12px; font-weight: 500;
color: var(--text-1, #cbd5e1);
background: rgba(2, 6, 23, 0.4);
border: 1px solid var(--border, rgba(148,163,184,0.18));
border-radius: 8px;
cursor: pointer;
transition: border-color .15s, color .15s, background .15s;
}
.puro-locale-btn:hover:not(:disabled) {
border-color: var(--border-2, rgba(148,163,184,0.32));
color: var(--cyan, #22d3ee);
}
.puro-locale-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.puro-locale-code { letter-spacing: 0.08em; }
.puro-locale-chev { color: var(--text-3, #64748b); transition: transform .15s; }
.puro-locale-chev.open { transform: rotate(180deg); color: var(--cyan, #22d3ee); }
.puro-locale-menu {
position: absolute; top: calc(100% + 6px); right: 0;
min-width: 140px;
background: rgba(15, 23, 42, 0.95);
border: 1px solid var(--border, rgba(148,163,184,0.18));
border-radius: 10px;
padding: 4px;
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
backdrop-filter: blur(8px);
z-index: 50;
}
.puro-locale-option {
width: 100%;
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
background: transparent; border: none; border-radius: 6px;
font-size: 13px; color: var(--text-1, #cbd5e1);
cursor: pointer;
text-align: left;
transition: background .12s, color .12s;
}
.puro-locale-option:hover:not(:disabled) {
background: rgba(34, 211, 238, 0.08);
color: var(--text-0, #f8fafc);
}
.puro-locale-option.active {
color: var(--cyan, #22d3ee);
}
.puro-locale-option-code {
font-family: var(--font-mono);
font-size: 11px; letter-spacing: 0.08em;
color: var(--text-3, #64748b);
}
.puro-locale-option.active .puro-locale-option-code { color: var(--cyan, #22d3ee); }
.puro-locale-option-name { flex: 1; }
.puro-locale-option-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--cyan, #22d3ee);
box-shadow: 0 0 0 3px rgba(34,211,238,0.2);
}
.puro-locale-pop-enter-active,
.puro-locale-pop-leave-active { transition: opacity .12s, transform .12s; }
.puro-locale-pop-enter-from,
.puro-locale-pop-leave-to { opacity: 0; transform: translateY(-4px) scale(0.97); }
</style>

View File

@@ -499,7 +499,39 @@ export default {
invalidResetLink: 'Invalid Reset Link', invalidResetLink: 'Invalid Reset Link',
invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.', invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.',
requestNewResetLink: 'Request New Reset Link', requestNewResetLink: 'Request New Reset Link',
invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.' invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.',
narrative: {
common: {
statusLive: 'live',
},
login: {
kicker: '// you already paid for your subscriptions',
headlineN: 'N',
headlineOne: '1',
headlineSep: 'subscriptions →',
headlineSuffix: 'key',
sub1: 'No more juggling accounts,',
sub2: 'no more paying twice for overlapping subscriptions.',
tagline: 'PURO — AI calls, back to basics.',
},
register: {
kicker: '// up and running in 5 minutes',
headlineN: 'N',
headlineOne: '1',
headlineSep: 'subscriptions →',
headlineSuffix: 'key',
sub1: 'No more juggling accounts,',
sub2: 'no more paying twice for overlapping subscriptions.',
tagline: 'PURO — AI calls, back to basics.',
stepsTitle: '// next steps',
step1Title: 'Create account',
step1Desc: 'Email + password, or LinuxDO OAuth',
step2Title: 'Connect subscription',
step2Desc: 'OAuth into your existing Claude Pro / ChatGPT Plus',
step3Title: 'Get your API key',
step3Desc: 'Grab your sk-puro-… and swap the SDK\'s base_url',
},
},
}, },
// Dashboard // Dashboard
@@ -5598,4 +5630,348 @@ export default {
}, },
}, },
landing: {
nav: {
products: 'Products',
pricing: 'Pricing',
docs: 'Docs',
login: 'Sign in',
signup: 'Free trial →',
},
hero: {
badgeNew: 'NEW',
eyebrow: 'Unified access to multiple AI platforms · Zero code change',
title1: 'Your AI subscriptions,',
title2: 'are already paid for.',
sub1: 'Claude Pro · ChatGPT Plus · Codex · Gemini subscriptions',
sub2: 'Aggregated into one unified API — drop-in {openai} / {anthropic} SDK',
ctaLogin: 'Sign in →',
ctaContact: 'Contact us',
micro: 'Verified with Codex CLI · Claude Code · curl · Server egress: Singapore',
},
models: {
kicker: '// providers',
title: 'Reuse your subscriptions directly via OAuth',
sub: 'No official API key needed. No account switching.',
more: 'More',
morePlanned: 'Planned',
},
features: {
kicker: '// capabilities',
title1: 'One subscription,',
title2: 'one unified model pool',
sub: 'Consolidate subscriptions scattered across platforms into infrastructure developers can actually use',
f1Title: 'One key for all models',
f1Desc: 'No more requesting API keys or configuring base_url per provider. One {sk} routes to Claude / GPT / Gemini, auto-dispatched by model to the right account pool.',
f1b1: 'OpenAI Responses API compatible',
f1b2: 'Anthropic Messages API compatible',
f1b3: 'Smart model → provider routing',
f2Title: 'Highly available account pool',
f2Desc: 'Multi-account auto-scheduling with failover. When an upstream hits rate limits or cooldown, traffic switches to the next healthy account — token refresh is fully automatic.',
f2b1: 'Rate-limit / 5xx auto failover',
f2b2: 'OAuth token auto-refresh',
f2b3: 'Weighted round-robin · least connections',
f3Title: 'Usage dashboard',
f3Desc: 'Tokens, cost, upstream account, and latency visualized per request. Model distribution pie + trend curve + top rankings.',
f3b1: 'Per-request audit log',
f3b2: 'Multi-dimension tokens / cost stats',
f3b3: 'Export CSV / Webhook integration',
},
codeDemo: {
kicker: '// integration',
title: 'Change base_url. That\'s it.',
sub: 'Compatible with OpenAI / Anthropic / Gemini SDK — {highlight}',
subHighlight: 'zero code changes',
foot: 'Supports OpenAI Responses API · Anthropic Messages API · Gemini generateContent · Streaming SSE &amp; WebSocket',
},
dashboard: {
kicker: '// observability',
title: 'Every request, fully visible',
sub: 'Unlike opaque third-party API pools — see exactly which account was charged, which model ran, how many tokens were used, and upstream response time at a glance.',
statToday: "Today's requests",
statTokensIn: 'Input tokens',
statTokensOut: 'Output tokens',
statCost: "Today's cost",
chartTrend: 'Usage trend — last 30 days',
tableTime: 'Time',
tableModel: 'Model',
tableUpstream: 'Upstream',
tableStatus: 'Status',
tableUsage: 'Usage',
},
footer: {
tagline1: 'Aggregate multiple AI subscriptions into one unified API.',
tagline2: 'Put your already-paid subscriptions to work.',
colProducts: 'Products',
colAccount: 'Account',
colContact: 'Contact',
linkDocs: 'Docs',
linkFeatures: 'Features',
linkChangelog: 'Changelog',
linkLogin: 'Sign in',
linkRegister: 'Register',
},
},
pricing: {
nav: {
products: 'Products',
pricing: 'Pricing',
docs: 'Docs',
login: 'Sign in',
signup: 'Free trial →',
},
hero: {
kicker: '// pricing · top up · pay as you go · never expires',
previewPill: '// preview · final pricing TBD at launch',
title1: 'Top up once,',
titleAccent: 'works across',
title2: 'all platforms',
sub: 'One credit balance works across Claude / ChatGPT / Gemini pools. We turn your subscription quota into real API credits — {discount} cheaper than official APIs.',
subDiscount: 'up to 70%',
underline: 'Credits never expire · Alipay / WeChat / USDT supported · No hidden fees',
},
soonChip: 'SOON',
tiers: {
starter: {
flag: 'STARTER',
tierLabel: 'tier · 01',
headline: 'Dip your toes in, get connected',
credit: 'Top up ${creditAmount} → get {creditBonus} credits',
creditAmount: '9.9',
creditBonus: '$12',
discountTag: 'vs. official API · from 5× cheaper',
cta: 'Top up →',
features: {
allModels: 'All models / all pools',
oneKey: 'API key',
rpm60: '60 RPM rate limit',
log7: 'Basic logs (7-day retention)',
noBYOS: 'Bring your own subscription',
noTeam: 'Team / multi-user collaboration',
},
},
pro: {
flag: '◆ RECOMMENDED',
tierLabel: 'tier · 02',
headline: 'Power users · Best value',
credit: 'Top up ${creditAmount} → get {creditBonus} credits',
creditAmount: '29.9',
creditBonus: '$45',
discountTag: 'vs. official API · 37× cheaper',
cta: 'Buy Pro →',
features: {
allModels: 'All models / all pools',
threeKeys: 'API keys · separate budgets',
rpm120: '120 RPM rate limit',
log30: 'Call logs (30-day retention)',
byos: 'Bring your own subscription (unlimited)',
failover: 'Multi-account failover scheduling',
},
},
scale: {
flag: '⚡ LIMITED +100%',
tierLabel: 'tier · 03',
headline: 'Small teams / long-haul projects',
credit: 'Top up ${creditAmount} → get {creditBonus} credits',
creditAmount: '99',
creditBonus: '$198',
discountTag: 'vs. official API · 25× cheaper',
cta: 'Top up →',
features: {
proAll: 'All Pro capabilities',
tenKeys: 'API keys · separate budgets',
rpm300: '300 RPM rate limit',
log90: 'Call logs (90-day retention)',
priorityCount: '',
priority: 'Priority-weighted scheduling',
community: 'Slack / Discord community support',
},
},
custom: {
flag: 'CUSTOM',
tierLabel: 'tier · 04',
headline: 'Custom amount · top up on demand',
creditPrefix: 'Get approx.',
bonusPrefix: '+',
discountTag: 'Discount tier matched automatically by amount',
cta: 'Custom amount →',
features: {
neverExpire: 'Credits never expire',
proAll: 'All Pro capabilities',
tiered: 'Tiered +21% ~ +100%',
payment: 'Alipay / WeChat / USDT',
preview: 'Drag slider to preview bonus',
},
},
},
custom: {
enterprise: {
title: 'Enterprise · Custom plans',
desc: 'Dedicated subscription pool, SLA, compliance audit, private deployment, invoice billing. Starting at >$500/mo.',
cta: 'Contact us →',
},
binding: {
title: 'Already subscribed? Connect it.',
desc: 'Have Claude Max / ChatGPT Pro? Register free, bind your subscription, and only pay for PURO routing — {price} per request.',
price: '$0.0008/request',
cta: 'Connect my subscription →',
},
},
calc: {
kicker: '// cost estimator',
previewPill: '// estimated · for reference only',
title: 'How much could you save?',
sub: 'Estimate your monthly spend on PURO vs. official APIs based on your usage. Numbers update as you move the sliders.',
reqLabel: 'Daily requests',
tokLabel: 'Avg tokens per request',
mixLabel: 'Claude share',
monthlyTok: 'Monthly token usage',
officialCost: 'Official API cost',
puroCost: 'PURO cost (incl. +50% bonus)',
savings: 'Savings',
recLabel: 'Suggested top-up',
recStarter: '≈ Starter tier covers it',
recPro: '≈ Pro tier · 1 month',
recScale: '≈ Scale tier · 1 month',
},
works: {
kicker: '// works everywhere',
title: 'One key, every tool',
sub: 'Any tool that supports a custom {baseUrl} or the OpenAI / Anthropic API works with PURO out of the box.',
baseUrl: 'base_url',
tools: {
claudeCode: 'Claude Code',
cursor: 'Cursor',
cline: 'Cline',
rooCode: 'Roo Code',
continueTag: 'Continue',
openaiSdk: 'OpenAI SDK',
anthropicSdk: 'Anthropic SDK',
openWebui: 'Open WebUI',
langchain: 'LangChain',
llamaIndex: 'LlamaIndex',
zed: 'Zed',
more: 'More…',
},
tags: {
claudeCode: 'ANTHROPIC_BASE_URL',
cursor: 'Custom model',
cline: 'OpenAI compat.',
rooCode: 'OpenAI compat.',
continueTag: 'config.yaml',
openaiSdk: 'Python / Node',
anthropicSdk: 'Native Claude',
openWebui: 'Custom base',
langchain: 'LLM node',
llamaIndex: 'Model router',
zed: 'Assistant',
more: '60+ tools',
},
},
faq: {
kicker: '// frequently asked',
title: 'You might be wondering',
noAnswer: "Can't find an answer? {contact} · We usually reply within 2 hours.",
contact: 'Email us ↗',
q1: 'How is PURO different from an API relay or proxy?',
a1: 'A relay just forwards official API requests — the price depends on how much balance you prepay. PURO is different: we let you turn your existing Claude Pro / ChatGPT Plus subscription into an API. The $20/month you\'re already paying no longer has to live in the official chat UI — it feeds Cursor, Claude Code, and any SDK through a unified API. We also offer a pay-per-use official API fallback pool, and you can mix both modes freely.',
q2: 'Will running API calls through my subscription get me banned?',
a2: 'We automatically pace requests per subscription and failover to other pool members if rate limits trigger. In practice, PURO\'s call pattern is less likely to flag risk controls than copy-pasting large conversations in the official client. When you bind multiple subscriptions, each account\'s RPM stays well within safe thresholds. All credentials are AES-256 encrypted, and requests never transit third-party infrastructure.',
q3: 'Do credits expire? Can I get a refund?',
a3Part1: 'Credits never expire.',
a3Part2: "You can save them up and use them months later. Full refund within 7 days of first top-up if no calls were made; after that, 85% of remaining credits are refunded. See our",
a3Link: 'refund policy',
a3Part3: '.',
q4: 'What payment methods are supported?',
a4: 'Domestic (CN): Alipay · WeChat Pay. International: Stripe credit card · USDT (TRC20 / ERC20) · PayPal. Enterprise top-ups support invoice and bank transfer with CNY receipts.',
q5: 'How many subscriptions can one PURO account bind?',
a5StarterLabel: 'Starter tier:',
a5Starter: 'Binding your own subscriptions is not supported',
a5ProLabel: 'Pro tier and above:',
a5Pro: 'Unlimited — you can bind 10 ChatGPT Plus + 3 Claude Pro accounts and schedule them all together',
a5EnterpriseLabel: 'Enterprise:',
a5Enterprise: 'Supports cross-team shared pools with org-level isolation',
q6: 'What happens if a subscription hits its rate limit?',
a6: "PURO's scheduler marks the throttled subscription as cooling and temporarily removes it from the pool. The same request is immediately failed over to another healthy subscription — callers typically experience no interruption. You can see each subscription's current status and remaining quota in the Dashboard.",
q7: 'How precise is billing? What if I go over my limit?',
a7: 'Billed per actual token count × model rate, accurate to 4 decimal places. Each API key can have an independent monthly budget cap — once hit, requests return 402 Payment Required and no further charges accumulate. The same 402 applies when your account balance is exhausted. Dashboard sends 80% / 95% reminder emails.',
q8: 'Will my data be used for training?',
a8Part1: 'No.',
a8Part2: 'All requests are used solely for routing — no content is stored or persisted (only metadata like model, token count, and latency is retained for billing and logs). Pro tier and above can optionally enable "zero-log mode" (planned), where we record nothing, not even request IDs.',
q9: 'Can I self-host PURO?',
a9: 'Enterprise tier supports Docker / K8s private deployment with separate control plane and data plane. Licensed as an annual subscription with upgrades and technical support included.',
a9Link: 'Contact us →',
q10: 'What models are supported? Will new models be added?',
a10: 'Currently covers Claude (Sonnet 4.5 / Opus 4 / Haiku 4.5), ChatGPT (GPT-5 / GPT-5 Codex / GPT-4.1), Gemini (2.5 Pro / 2.5 Flash). When official providers release new models, we typically go live within',
a10Link: 'docs',
a10Part2: '. Full model list available in the docs.',
},
finalCta: {
kicker: '// ready to start',
title: 'Get your first sk-puro-* key in 5 minutes',
subtitle: 'Connect your first subscription and you\'re ready.',
ctaPrimary: 'Sign up free →',
ctaDocs: 'View docs',
},
},
docs: {
nav: {
products: 'Product',
pricing: 'Pricing',
docs: 'Docs',
login: 'Sign in',
signup: 'Sign up',
},
hero: {
title: 'Quickstart — PURO AI',
subtitle: 'Three steps: get a key → set base_url → send a request',
},
sections: {
getKey: {
heading: '1. Get your API key',
desc: 'PURO AI is currently invite-only. Contact the admin to get access:',
note: 'Subscription self-purchase via the iShare portal is coming soon.',
},
codex: {
heading: '2. Codex CLI setup',
configIntro: 'Edit ~/.codex/config.toml:',
authIntro: 'Then ~/.codex/auth.json:',
verifyIntro: 'Verify:',
copy: 'Copy',
copied: 'Copied',
},
claudeCode: {
heading: '3. Claude Code setup',
configIntro: 'Edit ~/.claude/settings.json:',
note: 'Claude Code calls the Anthropic-compatible API via the /v1/messages endpoint.',
copy: 'Copy',
copied: 'Copied',
},
curl: {
heading: '4. curl quick test',
openaiIntro: 'OpenAI Responses API:',
anthropicIntro: 'Anthropic Messages API:',
copy: 'Copy',
copied: 'Copied',
},
models: {
heading: '5. Available models',
colModel: 'Model',
colPlatform: 'Platform / source',
colContext: 'Context',
colStatus: 'Status',
codexDedicated: 'OpenAI Codex dedicated',
note: 'Pricing tracks {repo} live. Full list available in the {dashboard} after signing in.',
noteRepo: 'model-price-repo',
noteDashboard: 'dashboard',
},
feedback: {
heading: '6. Feedback',
desc: 'Run into an issue or want a new platform added:',
},
},
},
} }

View File

@@ -505,6 +505,38 @@ export default {
puroRegisterTitle: '创建账户', puroRegisterTitle: '创建账户',
puroRegisterSub: '5 分钟开始用 PURO AI', puroRegisterSub: '5 分钟开始用 PURO AI',
confirmPasswordLabel: '确认密码', confirmPasswordLabel: '确认密码',
narrative: {
common: {
statusLive: 'live',
},
login: {
kicker: '// 你的订阅,已经付过钱了',
headlineN: 'N',
headlineOne: '1',
headlineSep: '个订阅 →',
headlineSuffix: '个 key',
sub1: '省去切换账号的繁琐,',
sub2: '省去为多个高昂订阅重复买单。',
tagline: 'PURO纯粹—— 让 AI 调用回归本质。',
},
register: {
kicker: '// 5 分钟开始用',
headlineN: 'N',
headlineOne: '1',
headlineSep: '个订阅 →',
headlineSuffix: '个 key',
sub1: '省去切换账号的繁琐,',
sub2: '省去为多个高昂订阅重复买单。',
tagline: 'PURO纯粹—— 让 AI 调用回归本质。',
stepsTitle: '// 下一步',
step1Title: '创建账户',
step1Desc: '邮箱 + 密码,或用 LinuxDO OAuth',
step2Title: '绑定订阅',
step2Desc: 'OAuth 接入你现有的 Claude Pro / ChatGPT Plus',
step3Title: '生成 key',
step3Desc: '拿到 sk-puro-…,换掉 SDK 的 base_url',
},
},
}, },
// Dashboard // Dashboard
@@ -5791,4 +5823,348 @@ export default {
}, },
}, },
landing: {
nav: {
products: '产品',
pricing: '定价',
docs: '文档',
login: '登录',
signup: '免费试用 →',
},
hero: {
badgeNew: 'NEW',
eyebrow: '统一接入多个 AI 平台 · 零改动切换',
title1: '你的 AI 订阅,',
title2: '已经付过钱了。',
sub1: 'Claude Pro · ChatGPT Plus · Codex · Gemini 订阅',
sub2: '聚合成统一 API零改动接入 {openai} / {anthropic} SDK',
ctaLogin: '登录 →',
ctaContact: '联系咨询',
micro: '已验证可用 Codex CLI · Claude Code · curl · 服务器出口新加坡',
},
models: {
kicker: '// providers',
title: '通过 OAuth 直接复用你的订阅',
sub: '无需申请官方 API key也无需切换账号',
more: '更多',
morePlanned: '规划中',
},
features: {
kicker: '// capabilities',
title1: '付一次订阅,',
title2: '用起一整个模型池',
sub: '把散落在各个平台的订阅,整合成开发者真正能用的基础设施',
f1Title: '一个 key 接所有模型',
f1Desc: '不再为每个 provider 申请 API key、配置 base_url。统一 {sk} 走 Claude / GPT / Gemini按 model 自动路由到对应账号池。',
f1b1: 'OpenAI Responses API 兼容',
f1b2: 'Anthropic Messages API 兼容',
f1b3: '智能 model → provider 路由',
f2Title: '账号池高可用',
f2Desc: '支持多账号自动调度与 failover。某个上游触发限流 / 冷却时流量切到下一个健康账号token 刷新全自动。',
f2b1: '限流/5xx 自动 failover',
f2b2: 'OAuth token 自动刷新',
f2b3: '加权轮询 · 最少连接',
f3Title: '用量看板',
f3Desc: '每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。',
f3b1: '逐请求审计日志',
f3b2: '多维度 tokens / cost 统计',
f3b3: '导出 CSV / 接 Webhook',
},
codeDemo: {
kicker: '// integration',
title: '把 base_url 一改,就能用',
sub: '兼容 OpenAI / Anthropic / Gemini SDK{highlight}',
subHighlight: '零代码改动',
foot: '支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE &amp; WebSocket',
},
dashboard: {
kicker: '// observability',
title: '每条请求都看得见',
sub: '不像第三方 API 池子那种"扣了多少不告诉你"——扣哪个账号、跑哪个模型、用了多少 tokens、上游响应几秒一目了然。',
statToday: '今日请求',
statTokensIn: '输入 Tokens',
statTokensOut: '输出 Tokens',
statCost: '今日费用',
chartTrend: '近 30 天用量趋势',
tableTime: '时间',
tableModel: '模型',
tableUpstream: '上游',
tableStatus: '状态',
tableUsage: '用量',
},
footer: {
tagline1: '把多个 AI 订阅聚合成统一 API。',
tagline2: '让「已经付过钱」的订阅真正为你工作。',
colProducts: '产品',
colAccount: '账户',
colContact: '联系',
linkDocs: '文档',
linkFeatures: '功能',
linkChangelog: '更新日志',
linkLogin: '登录',
linkRegister: '注册',
},
},
pricing: {
nav: {
products: '产品',
pricing: '定价',
docs: '文档',
login: '登录',
signup: '免费试用 →',
},
hero: {
kicker: '// pricing · 充多少 · 用多少 · 永不过期',
previewPill: '// preview · 最终定价以开售为准',
title1: '一次充值,',
titleAccent: '全平台',
title2: '通用',
sub: '同一份积分可以用在 Claude / ChatGPT / Gemini 任意池上。我们把你的订阅额度变成真正的 API 余额 —— 相比官方 API 便宜 {discount}。',
subDiscount: '至多 70%',
underline: '余额永不过期 · 支持支付宝 / 微信 / USDT · 无隐藏订阅费',
},
soonChip: 'SOON',
tiers: {
starter: {
flag: 'STARTER',
tierLabel: 'tier · 01',
headline: '先尝尝鲜,跑通接入',
credit: '充 ${creditAmount} → 得 {creditBonus} 积分',
creditAmount: '9.9',
creditBonus: '$12',
discountTag: '相当于官方 API · 0.5 折起',
cta: '充值 →',
features: {
allModels: '可用所有模型 / 所有池',
oneKey: '个 API Key',
rpm60: '60 RPM 速率限制',
log7: '基础日志(7 天保留)',
noBYOS: '自带订阅接入',
noTeam: '团队 / 多人协作',
},
},
pro: {
flag: '◆ 推荐',
tierLabel: 'tier · 02',
headline: '个人重度用户 · 最划算',
credit: '充 ${creditAmount} → 得 {creditBonus} 积分',
creditAmount: '29.9',
creditBonus: '$45',
discountTag: '相当于官方 API · 3-7 折',
cta: '立即充值 →',
features: {
allModels: '可用所有模型 / 所有池',
threeKeys: '个 API Key · 独立预算',
rpm120: '120 RPM 速率限制',
log30: '调用日志(30 天保留)',
byos: '自带订阅接入(无限个)',
failover: '多账号 failover 调度',
},
},
scale: {
flag: '⚡ 限时 +100%',
tierLabel: 'tier · 03',
headline: '小团队 / 长跑项目',
credit: '充 ${creditAmount} → 得 {creditBonus} 积分',
creditAmount: '99',
creditBonus: '$198',
discountTag: '相当于官方 API · 2-5 折',
cta: '充值 →',
features: {
proAll: '所有 Pro 能力',
tenKeys: '个 API Key · 独立预算',
rpm300: '300 RPM 速率限制',
log90: '调用日志(90 天保留)',
priorityCount: '',
priority: '请求优先级加权调度',
community: 'Slack / Discord 群组支持',
},
},
custom: {
flag: 'CUSTOM',
tierLabel: 'tier · 04',
headline: '自定义金额 · 按需充值',
creditPrefix: '得约',
bonusPrefix: '+',
discountTag: '根据金额阶梯自动匹配折扣',
cta: '定制充值 →',
features: {
neverExpire: '积分永不过期',
proAll: 'Pro 全部能力',
tiered: '阶梯 +21% ~ +100%',
payment: '支付宝 / 微信 / USDT',
preview: '拖动滑块预览赠送',
},
},
},
custom: {
enterprise: {
title: 'Enterprise · 企业定制',
desc: '专属订阅池、SLA、合规审计、私有化部署、发票结算。规模 >$500/月起可申请。',
cta: '联系商务 →',
},
binding: {
title: '已有订阅?直接接入',
desc: '有 Claude Max / ChatGPT Pro?免费注册后绑定,只为 PURO 路由费买单 —— 按次 {price}。',
price: '$0.0008/request',
cta: '接入我的订阅 →',
},
},
calc: {
kicker: '// cost estimator',
previewPill: '// estimated · 以实际计费为准',
title: '算算你能省多少?',
sub: '按你的使用场景,对比 PURO 和官方 API 的月度花费差。数字会根据你选的场景自动更新。',
reqLabel: '日均请求数',
tokLabel: '平均每请求 tokens',
mixLabel: 'Claude 占比',
monthlyTok: '月度 tokens 消耗',
officialCost: '官方 API 价格',
puroCost: 'PURO 价格(含 +50% 赠送)',
savings: '节省',
recLabel: '建议充值',
recStarter: '≈ Starter 档够用',
recPro: '≈ Pro 档 1 个月',
recScale: '≈ Scale 档 · 1 个月',
},
works: {
kicker: '// works everywhere',
title: '一个 key,所有工具通用',
sub: '只要支持自定义 {baseUrl} 或 OpenAI / Anthropic API,都能直接接入 PURO。',
baseUrl: 'base_url',
tools: {
claudeCode: 'Claude Code',
cursor: 'Cursor',
cline: 'Cline',
rooCode: 'Roo Code',
continueTag: 'Continue',
openaiSdk: 'OpenAI SDK',
anthropicSdk: 'Anthropic SDK',
openWebui: 'Open WebUI',
langchain: 'LangChain',
llamaIndex: 'LlamaIndex',
zed: 'Zed',
more: '更多…',
},
tags: {
claudeCode: 'ANTHROPIC_BASE_URL',
cursor: '自定义模型',
cline: 'OpenAI 兼容',
rooCode: 'OpenAI 兼容',
continueTag: 'config.yaml',
openaiSdk: 'Python / Node',
anthropicSdk: '原生 Claude',
openWebui: '自定义 base',
langchain: 'LLM 节点',
llamaIndex: '模型路由',
zed: 'Assistant',
more: '60+ 工具',
},
},
faq: {
kicker: '// frequently asked',
title: '你可能想问的',
noAnswer: '没找到答案?{contact} · 通常 2 小时内回复。',
contact: '发邮件给我们 ↗',
q1: 'PURO 和 API 中转站 / API 代理有什么不同?',
a1: '中转站只是把官方 API 请求转一手,价格取决于你预付多少 balance。PURO 的不同是 —— 我们让你把已有的 Claude Pro / ChatGPT Plus 订阅变成 API。你原本就在付的 $20/月,不再只能在官网聊天里用,而是通过统一 API 喂给 Cursor、Claude Code、任何 SDK。同时我们也提供按量充值的官方 API 备用池,两种模式可以混用。',
q2: '用订阅跑 API 会不会被封号?',
a2: '我们会自动控制每个订阅的请求节奏,并在触发限流时把请求 failover 到池子里的其他订阅。实际上 PURO 的调用模式比你在官方客户端直接复制粘贴大段对话更不容易触发风控。你绑定多个订阅时,单个账号的 RPM 会被压到足够安全的阈值内。另外所有凭证用 AES-256 加密存储,请求链路不经过第三方。',
q3: '积分会过期吗?可以退款吗?',
a3Part1: '积分永不过期。',
a3Part2: '你可以攒着慢慢用 —— 包括几个月都不用。首次充值 7 天内未产生任何调用可全额退款,之后按剩余积分 85% 比例退。详见',
a3Link: '退款政策',
a3Part3: '。',
q4: '支持哪些支付方式?',
a4: '国内:支付宝 · 微信支付。国际Stripe 信用卡 · USDT (TRC20 / ERC20) · PayPal。企业充值支持 Invoice 对公打款,人民币开票。',
q5: '一个 PURO 账号可以绑定多少个订阅?',
a5StarterLabel: 'Starter 档:',
a5Starter: '不支持绑定自带订阅',
a5ProLabel: 'Pro 档及以上:',
a5Pro: '无限制,你可以把 10 个 ChatGPT Plus + 3 个 Claude Pro 一起绑上去,统一调度',
a5EnterpriseLabel: 'Enterprise',
a5Enterprise: '支持跨团队共享池,按组织维度隔离',
q6: '如果某个订阅触发限流了会怎样?',
a6: 'PURO 的调度器会把受限的订阅自动标记为 cooling 状态,暂时从池子里摘除。同一请求会立刻被 failover 到池内其他健康订阅上 —— 调用方通常感受不到中断。你可以在 Dashboard 看到每个订阅的当前状态和剩余配额。',
q7: '计费精度?超量会怎么办?',
a7: '按实际 token 数 + 模型单价计费,精度到 4 位小数。每个 API Key 可设置独立月度预算,达到后返回 402 Payment Required不会继续扣费。账户总余额不足时同样会返回 402且 Dashboard 有 80% / 95% 两级提醒邮件。',
q8: '数据会被用于训练吗?',
a8Part1: '不会。',
a8Part2: '所有请求仅用于路由转发,不入库、不留存内容(仅保留元数据如模型、token 数、延迟,用于计费和日志)。Pro 档及以上可选开启"零日志模式"(规划中),我们连请求 ID 都不记录。',
q9: '可以私有化部署吗?',
a9: 'Enterprise 档支持 Docker / K8s 私有化部署,控制面和数据面可以分开。授权按年订阅,包含升级和技术支持。',
a9Link: '联系商务 →',
q10: '支持哪些模型?会跟进新模型吗?',
a10: '当前覆盖 Claude(Sonnet 4.5 / Opus 4 / Haiku 4.5)、ChatGPT(GPT-5 / GPT-5 Codex / GPT-4.1)、Gemini(2.5 Pro / 2.5 Flash)。每当官方发布新模型,我们通常在 24 小时内上线。完整模型列表见',
a10Link: '文档',
a10Part2: '。',
},
finalCta: {
kicker: '// ready to start',
title: '5 分钟,拿到你第一个 sk-puro-* key',
subtitle: '绑定你的第一个订阅即可开始。',
ctaPrimary: '免费注册 →',
ctaDocs: '查看文档',
},
},
docs: {
nav: {
products: '产品',
pricing: '定价',
docs: '文档',
login: '登录',
signup: '注册',
},
hero: {
title: '快速接入 PURO AI',
subtitle: '三步走:拿 key → 配 base_url → 发请求',
},
sections: {
getKey: {
heading: '1. 获取 API key',
desc: '当前 PURO AI 不开放自助注册付费。联系管理员获取:',
note: '未来通过 iShare 入口开放订阅购买。',
},
codex: {
heading: '2. Codex CLI 接入',
configIntro: '修改 ~/.codex/config.toml',
authIntro: '然后 ~/.codex/auth.json',
verifyIntro: '验证:',
copy: '复制',
copied: '已复制',
},
claudeCode: {
heading: '3. Claude Code 接入',
configIntro: '修改 ~/.claude/settings.json',
note: 'Claude Code 通过 /v1/messages endpoint 调用 Anthropic 兼容 API。',
copy: '复制',
copied: '已复制',
},
curl: {
heading: '4. curl 直连测试',
openaiIntro: 'OpenAI Responses API',
anthropicIntro: 'Anthropic Messages API',
copy: '复制',
copied: '已复制',
},
models: {
heading: '5. 支持的模型',
colModel: '模型',
colPlatform: '平台 / 来源',
colContext: '上下文',
colStatus: '状态',
codexDedicated: 'OpenAI Codex 专用',
note: '后端 pricing 表实时跟进 {repo},完整清单登录后在 {dashboard} 查看。',
noteRepo: 'model-price-repo',
noteDashboard: '控制台',
},
feedback: {
heading: '6. 问题反馈',
desc: '遇到问题或希望补接某个平台:',
},
},
},
} }

View File

@@ -129,6 +129,12 @@ const routes: RouteRecordRaw[] = [
title: 'PURO AI · 文档' title: 'PURO AI · 文档'
} }
}, },
{
path: '/pricing',
name: 'pricing',
component: () => import('@/views/pricing/PricingView.vue'),
meta: { requiresAuth: false, title: 'Pricing · PURO AI' }
},
// ==================== User Routes ==================== // ==================== User Routes ====================
{ {

View File

@@ -11,15 +11,17 @@
</router-link> </router-link>
<div> <div>
<div class="n-kicker">// 你的订阅,已经付过钱了</div> <div class="n-kicker">{{ t('auth.narrative.login.kicker') }}</div>
<div class="auth-narrative-headline" style="margin-top: 12px;"> <div class="auth-narrative-headline" style="margin-top: 12px;">
<span class="num-n">N</span> 个订阅<br> <span class="num-n">{{ t('auth.narrative.login.headlineN') }}</span>
<span class="num-1">1</span> key {{ ' ' + t('auth.narrative.login.headlineSep') + ' ' }}
<span class="num-1">{{ t('auth.narrative.login.headlineOne') }}</span>
{{ ' ' + t('auth.narrative.login.headlineSuffix') }}
</div> </div>
<p class="auth-narrative-sub"> <p class="auth-narrative-sub">
省去切换账号的繁琐<br> {{ t('auth.narrative.login.sub1') }}<br>
省去为多个高昂订阅重复买单<br> {{ t('auth.narrative.login.sub2') }}<br>
<span class="auth-narrative-tagline">PURO纯粹 AI 调用回归本质</span> <span class="auth-narrative-tagline">{{ t('auth.narrative.login.tagline') }}</span>
</p> </p>
</div> </div>

View File

@@ -11,31 +11,33 @@
</router-link> </router-link>
<div> <div>
<div class="n-kicker">// 5 分钟开始用</div> <div class="n-kicker">{{ t('auth.narrative.register.kicker') }}</div>
<div class="auth-narrative-headline" style="margin-top: 12px;"> <div class="auth-narrative-headline" style="margin-top: 12px;">
<span class="num-n">N</span> 个订阅<br> <span class="num-n">{{ t('auth.narrative.register.headlineN') }}</span>
<span class="num-1">1</span> key {{ ' ' + t('auth.narrative.register.headlineSep') + ' ' }}
<span class="num-1">{{ t('auth.narrative.register.headlineOne') }}</span>
{{ ' ' + t('auth.narrative.register.headlineSuffix') }}
</div> </div>
<p class="auth-narrative-sub"> <p class="auth-narrative-sub">
省去切换账号的繁琐<br> {{ t('auth.narrative.register.sub1') }}<br>
省去为多个高昂订阅重复买单<br> {{ t('auth.narrative.register.sub2') }}<br>
<span class="auth-narrative-tagline">PURO纯粹 AI 调用回归本质</span> <span class="auth-narrative-tagline">{{ t('auth.narrative.register.tagline') }}</span>
</p> </p>
</div> </div>
<div class="steps"> <div class="steps">
<div class="steps-title">// 下一步</div> <div class="steps-title">{{ t('auth.narrative.register.stepsTitle') }}</div>
<div class="step active"> <div class="step active">
<div class="step-num">1</div> <div class="step-num">1</div>
<div class="step-text"><b>创建账户</b> · 邮箱 + 密码或用 LinuxDO OAuth</div> <div class="step-text"><b>{{ t('auth.narrative.register.step1Title') }}</b> · {{ t('auth.narrative.register.step1Desc') }}</div>
</div> </div>
<div class="step"> <div class="step">
<div class="step-num">2</div> <div class="step-num">2</div>
<div class="step-text"><b>绑定订阅</b> · OAuth 接入你现有的 Claude Pro / ChatGPT Plus</div> <div class="step-text"><b>{{ t('auth.narrative.register.step2Title') }}</b> · {{ t('auth.narrative.register.step2Desc') }}</div>
</div> </div>
<div class="step"> <div class="step">
<div class="step-num">3</div> <div class="step-num">3</div>
<div class="step-text"><b>生成 key</b> · 拿到 <span class="k">sk-puro-</span>换掉 SDK <span class="k">base_url</span></div> <div class="step-text"><b>{{ t('auth.narrative.register.step3Title') }}</b> · {{ t('auth.narrative.register.step3Desc') }}</div>
</div> </div>
</div> </div>

View File

@@ -11,35 +11,36 @@
<span>PURO AI</span> <span>PURO AI</span>
</router-link> </router-link>
<div class="nav-links"> <div class="nav-links">
<router-link to="/">首页</router-link> <router-link to="/">{{ $t('docs.nav.products') }}</router-link>
<a href="#codex">Codex</a> <router-link to="/pricing">{{ $t('docs.nav.pricing') }}</router-link>
<a href="#claude-code">Claude Code</a> <router-link to="/docs" class="active">{{ $t('docs.nav.docs') }}</router-link>
<a href="#curl">curl</a>
</div> </div>
<div class="nav-cta"> <div class="nav-cta">
<router-link to="/login" class="btn btn-primary">登录 </router-link> <PuroLocaleSwitcher />
<router-link to="/login" class="btn btn-ghost">{{ $t('docs.nav.login') }}</router-link>
<router-link to="/register" class="btn btn-primary">{{ $t('docs.nav.signup') }}</router-link>
</div> </div>
</div> </div>
</nav> </nav>
<section class="docs-hero container"> <section class="docs-hero container">
<h1>快速接入 PURO AI</h1> <h1>{{ $t('docs.hero.title') }}</h1>
<p class="subtitle">三步走 key base_url 发请求</p> <p class="subtitle">{{ $t('docs.hero.subtitle') }}</p>
</section> </section>
<div class="container docs-body"> <div class="container docs-body">
<section id="get-key" class="docs-section"> <section id="get-key" class="docs-section">
<h2>1. 获取 API key</h2> <h2>{{ $t('docs.sections.getKey.heading') }}</h2>
<p>当前 PURO AI 不开放自助注册付费联系管理员获取</p> <p>{{ $t('docs.sections.getKey.desc') }}</p>
<div class="callout"> <div class="callout">
<a href="mailto:admin@puro.im">admin@puro.im</a> <a href="mailto:admin@puro.im">admin@puro.im</a>
</div> </div>
<p class="note">未来通过 iShare 入口开放订阅购买</p> <p class="note">{{ $t('docs.sections.getKey.note') }}</p>
</section> </section>
<section id="codex" class="docs-section"> <section id="codex" class="docs-section">
<h2>2. Codex CLI 接入</h2> <h2>{{ $t('docs.sections.codex.heading') }}</h2>
<p>修改 <code class="mono">~/.codex/config.toml</code></p> <p>{{ $t('docs.sections.codex.configIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -53,7 +54,7 @@
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.codex.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code>model_provider = <span class="str">"OpenAI"</span> <pre class="mono"><code>model_provider = <span class="str">"OpenAI"</span>
@@ -66,7 +67,7 @@ base_url = <span class="str">"https://ai.puro.im"</span>
wire_api = <span class="str">"responses"</span> wire_api = <span class="str">"responses"</span>
requires_openai_auth = <span class="kw">true</span></code></pre> requires_openai_auth = <span class="kw">true</span></code></pre>
</div> </div>
<p>然后 <code class="mono">~/.codex/auth.json</code></p> <p>{{ $t('docs.sections.codex.authIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -80,14 +81,14 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.codex.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code>{ <pre class="mono"><code>{
<span class="str">"OPENAI_API_KEY"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span> <span class="str">"OPENAI_API_KEY"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span>
}</code></pre> }</code></pre>
</div> </div>
<p>验证</p> <p>{{ $t('docs.sections.codex.verifyIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -101,7 +102,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.codex.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code><span class="cm">$</span> codex exec --sandbox read-only <span class="str">"say hi"</span></code></pre> <pre class="mono"><code><span class="cm">$</span> codex exec --sandbox read-only <span class="str">"say hi"</span></code></pre>
@@ -109,8 +110,8 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</section> </section>
<section id="claude-code" class="docs-section"> <section id="claude-code" class="docs-section">
<h2>3. Claude Code 接入</h2> <h2>{{ $t('docs.sections.claudeCode.heading') }}</h2>
<p>修改 <code class="mono">~/.claude/settings.json</code></p> <p>{{ $t('docs.sections.claudeCode.configIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -124,7 +125,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.claudeCode.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code>{ <pre class="mono"><code>{
@@ -132,12 +133,12 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<span class="str">"api_key"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span> <span class="str">"api_key"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span>
}</code></pre> }</code></pre>
</div> </div>
<p class="note">Claude Code 通过 <code class="mono">/v1/messages</code> endpoint 调用 Anthropic 兼容 API</p> <p class="note">{{ $t('docs.sections.claudeCode.note') }}</p>
</section> </section>
<section id="curl" class="docs-section"> <section id="curl" class="docs-section">
<h2>4. curl 直连测试</h2> <h2>{{ $t('docs.sections.curl.heading') }}</h2>
<p>OpenAI Responses API</p> <p>{{ $t('docs.sections.curl.openaiIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -151,7 +152,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.curl.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/responses \ <pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/responses \
@@ -159,7 +160,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
-H <span class="str">"Content-Type: application/json"</span> \ -H <span class="str">"Content-Type: application/json"</span> \
-d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre> -d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre>
</div> </div>
<p>Anthropic Messages API</p> <p>{{ $t('docs.sections.curl.anthropicIntro') }}</p>
<div class="code-panel"> <div class="code-panel">
<div class="code-head"> <div class="code-head">
<div class="traffic"> <div class="traffic">
@@ -173,7 +174,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/> <path d="M4 1.5h5a.5.5 0 0 1 .5.5v1h1V2a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 2v8A1.5 1.5 0 0 0 4 11.5h1v-1H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z"/>
<path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/> <path d="M7 4.5A1.5 1.5 0 0 1 8.5 3h5A1.5 1.5 0 0 1 15 4.5v9a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 7 13.5v-9z"/>
</svg> </svg>
复制 {{ $t('docs.sections.curl.copy') }}
</button> </button>
</div> </div>
<pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/v1/messages \ <pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/v1/messages \
@@ -185,15 +186,15 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</section> </section>
<section id="models" class="docs-section"> <section id="models" class="docs-section">
<h2>5. 支持的模型</h2> <h2>{{ $t('docs.sections.models.heading') }}</h2>
<div class="table-wrap"> <div class="table-wrap">
<table class="models-table mono"> <table class="models-table mono">
<thead> <thead>
<tr> <tr>
<th>模型</th> <th>{{ $t('docs.sections.models.colModel') }}</th>
<th>平台 / 来源</th> <th>{{ $t('docs.sections.models.colPlatform') }}</th>
<th>上下文</th> <th>{{ $t('docs.sections.models.colContext') }}</th>
<th>状态</th> <th>{{ $t('docs.sections.models.colStatus') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -205,7 +206,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</tr> </tr>
<tr> <tr>
<td><code>gpt-5.4-codex</code></td> <td><code>gpt-5.4-codex</code></td>
<td><span class="provider gpt"><span class="dot"></span>OpenAI Codex 专用</span></td> <td><span class="provider gpt"><span class="dot"></span>{{ $t('docs.sections.models.codexDedicated') }}</span></td>
<td>272K</td> <td>272K</td>
<td><span class="badge-ok">OK</span></td> <td><span class="badge-ok">OK</span></td>
</tr> </tr>
@@ -236,12 +237,15 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</tbody> </tbody>
</table> </table>
</div> </div>
<p class="note">后端 pricing 表实时跟进 <code class="mono">model-price-repo</code>完整清单登录后在 <router-link to="/dashboard">控制台</router-link> 查看</p> <i18n-t tag="p" class="note" keypath="docs.sections.models.note">
<template #repo><code class="mono">{{ $t('docs.sections.models.noteRepo') }}</code></template>
<template #dashboard><router-link to="/dashboard">{{ $t('docs.sections.models.noteDashboard') }}</router-link></template>
</i18n-t>
</section> </section>
<section id="feedback" class="docs-section"> <section id="feedback" class="docs-section">
<h2>6. 问题反馈</h2> <h2>{{ $t('docs.sections.feedback.heading') }}</h2>
<p>遇到问题或希望补接某个平台</p> <p>{{ $t('docs.sections.feedback.desc') }}</p>
<div class="callout"> <div class="callout">
<a href="mailto:admin@puro.im">admin@puro.im</a> <a href="mailto:admin@puro.im">admin@puro.im</a>
</div> </div>
@@ -251,8 +255,11 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// DocsView — public quickstart documentation import { useI18n } from 'vue-i18n'
// Route: /docs (no auth required) import PuroLocaleSwitcher from '@/components/puro/PuroLocaleSwitcher.vue'
const { t } = useI18n()
async function copyCode(ev: MouseEvent) { async function copyCode(ev: MouseEvent) {
const button = ev.currentTarget as HTMLButtonElement const button = ev.currentTarget as HTMLButtonElement
const panel = button.closest('.code-panel') const panel = button.closest('.code-panel')
@@ -261,7 +268,7 @@ async function copyCode(ev: MouseEvent) {
try { try {
await navigator.clipboard.writeText(codeEl.innerText) await navigator.clipboard.writeText(codeEl.innerText)
const original = button.innerHTML const original = button.innerHTML
button.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg> 已复制' button.innerHTML = `<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg> ${t('docs.sections.codex.copied')}`
button.classList.add('copied') button.classList.add('copied')
setTimeout(() => { setTimeout(() => {
button.innerHTML = original button.innerHTML = original

View File

@@ -13,12 +13,14 @@
<span>PURO AI</span> <span>PURO AI</span>
</router-link> </router-link>
<div class="nav-links"> <div class="nav-links">
<a href="#features">产品</a> <a href="#features">{{ $t('landing.nav.products') }}</a>
<a href="/docs">文档</a> <router-link to="/pricing">{{ $t('landing.nav.pricing') }}</router-link>
<router-link to="/docs">{{ $t('landing.nav.docs') }}</router-link>
</div> </div>
<div class="nav-cta"> <div class="nav-cta">
<router-link to="/login" class="btn btn-ghost">登录</router-link> <PuroLocaleSwitcher />
<router-link to="/register" class="btn btn-primary">免费试用 </router-link> <router-link to="/login" class="btn btn-ghost">{{ $t('landing.nav.login') }}</router-link>
<router-link to="/register" class="btn btn-primary">{{ $t('landing.nav.signup') }}</router-link>
</div> </div>
</div> </div>
</nav> </nav>
@@ -26,32 +28,35 @@
<!-- HERO --> <!-- HERO -->
<section class="hero container"> <section class="hero container">
<div class="hero-eyebrow"> <div class="hero-eyebrow">
<span class="badge">NEW</span> <span class="badge">{{ $t('landing.hero.badgeNew') }}</span>
<span>统一接入多个 AI 平台 · 零改动切换</span> <span>{{ $t('landing.hero.eyebrow') }}</span>
</div> </div>
<h1 class="hero-title"> <h1 class="hero-title">
你的 AI 订阅<br> {{ $t('landing.hero.title1') }}<br>
<span class="text-puro-cyan">已经付过钱了</span> <span class="text-puro-cyan">{{ $t('landing.hero.title2') }}</span>
</h1> </h1>
<p class="hero-sub"> <p class="hero-sub">
Claude Pro · ChatGPT Plus · Codex · Gemini 订阅<br> {{ $t('landing.hero.sub1') }}<br>
聚合成统一 API零改动接入 <span class="pill-inline">OpenAI</span> / <span class="pill-inline">Anthropic</span> SDK <i18n-t keypath="landing.hero.sub2" tag="span">
<template #openai><span class="pill-inline">OpenAI</span></template>
<template #anthropic><span class="pill-inline">Anthropic</span></template>
</i18n-t>
</p> </p>
<div class="hero-cta"> <div class="hero-cta">
<router-link to="/login" class="btn btn-primary btn-lg">登录 </router-link> <router-link to="/login" class="btn btn-primary btn-lg">{{ $t('landing.hero.ctaLogin') }}</router-link>
<a href="mailto:admin@puro.im" class="btn btn-ghost btn-lg">联系咨询</a> <a href="mailto:admin@puro.im" class="btn btn-ghost btn-lg">{{ $t('landing.hero.ctaContact') }}</a>
</div> </div>
<div class="hero-micro"> <div class="hero-micro">
已验证可用 Codex CLI · Claude Code · curl · 服务器出口新加坡 {{ $t('landing.hero.micro') }}
</div> </div>
</section> </section>
<!-- 模型墙 --> <!-- 模型墙 -->
<section class="block container" id="models"> <section class="block container" id="models">
<div class="section-header"> <div class="section-header">
<div class="section-kicker">// providers</div> <div class="section-kicker">{{ $t('landing.models.kicker') }}</div>
<h2 class="section-title">通过 OAuth 直接复用你的订阅</h2> <h2 class="section-title">{{ $t('landing.models.title') }}</h2>
<p class="section-sub">无需申请官方 API key也无需切换账号</p> <p class="section-sub">{{ $t('landing.models.sub') }}</p>
</div> </div>
<div class="model-wall"> <div class="model-wall">
<div class="model-card"> <div class="model-card">
@@ -99,8 +104,8 @@
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.6" stroke-linecap="round"><circle cx="5" cy="12" r="1.5" fill="#94a3b8"/><circle cx="12" cy="12" r="1.5" fill="#94a3b8"/><circle cx="19" cy="12" r="1.5" fill="#94a3b8"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.6" stroke-linecap="round"><circle cx="5" cy="12" r="1.5" fill="#94a3b8"/><circle cx="12" cy="12" r="1.5" fill="#94a3b8"/><circle cx="19" cy="12" r="1.5" fill="#94a3b8"/></svg>
</div> </div>
<div> <div>
<div class="model-name">更多</div> <div class="model-name">{{ $t('landing.models.more') }}</div>
<div class="model-meta">规划中</div> <div class="model-meta">{{ $t('landing.models.morePlanned') }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -109,39 +114,43 @@
<!-- 三特性 --> <!-- 三特性 -->
<section class="block container" id="features"> <section class="block container" id="features">
<div class="section-header"> <div class="section-header">
<div class="section-kicker">// capabilities</div> <div class="section-kicker">{{ $t('landing.features.kicker') }}</div>
<h2 class="section-title">付一次订阅<br>用起一整个模型池</h2> <h2 class="section-title">{{ $t('landing.features.title1') }}<br>{{ $t('landing.features.title2') }}</h2>
<p class="section-sub">把散落在各个平台的订阅整合成开发者真正能用的基础设施</p> <p class="section-sub">{{ $t('landing.features.sub') }}</p>
</div> </div>
<div class="features"> <div class="features">
<div class="feature"> <div class="feature">
<div class="feature-icon"></div> <div class="feature-icon"></div>
<h3>一个 key 接所有模型</h3> <h3>{{ $t('landing.features.f1Title') }}</h3>
<p>不再为每个 provider 申请 API key配置 base_url统一 <code class="mono">sk-</code> Claude / GPT / Gemini model 自动路由到对应账号池</p> <p>
<i18n-t keypath="landing.features.f1Desc" tag="span">
<template #sk><code class="mono">sk-</code></template>
</i18n-t>
</p>
<ul class="feature-bullets"> <ul class="feature-bullets">
<li>OpenAI Responses API 兼容</li> <li>{{ $t('landing.features.f1b1') }}</li>
<li>Anthropic Messages API 兼容</li> <li>{{ $t('landing.features.f1b2') }}</li>
<li>智能 model provider 路由</li> <li>{{ $t('landing.features.f1b3') }}</li>
</ul> </ul>
</div> </div>
<div class="feature"> <div class="feature">
<div class="feature-icon">🔄</div> <div class="feature-icon">🔄</div>
<h3>账号池高可用</h3> <h3>{{ $t('landing.features.f2Title') }}</h3>
<p>支持多账号自动调度与 failover某个上游触发限流 / 冷却时流量切到下一个健康账号token 刷新全自动</p> <p>{{ $t('landing.features.f2Desc') }}</p>
<ul class="feature-bullets"> <ul class="feature-bullets">
<li>限流/5xx 自动 failover</li> <li>{{ $t('landing.features.f2b1') }}</li>
<li>OAuth token 自动刷新</li> <li>{{ $t('landing.features.f2b2') }}</li>
<li>加权轮询 · 最少连接</li> <li>{{ $t('landing.features.f2b3') }}</li>
</ul> </ul>
</div> </div>
<div class="feature"> <div class="feature">
<div class="feature-icon">📊</div> <div class="feature-icon">📊</div>
<h3>用量看板</h3> <h3>{{ $t('landing.features.f3Title') }}</h3>
<p>每条请求的 tokens费用上游账号延迟全可视化模型分布饼图 + 趋势曲线 + Top 排行</p> <p>{{ $t('landing.features.f3Desc') }}</p>
<ul class="feature-bullets"> <ul class="feature-bullets">
<li>逐请求审计日志</li> <li>{{ $t('landing.features.f3b1') }}</li>
<li>多维度 tokens / cost 统计</li> <li>{{ $t('landing.features.f3b2') }}</li>
<li>导出 CSV / Webhook</li> <li>{{ $t('landing.features.f3b3') }}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -150,9 +159,13 @@
<!-- Code Demo --> <!-- Code Demo -->
<section class="block container" id="code"> <section class="block container" id="code">
<div class="section-header"> <div class="section-header">
<div class="section-kicker">// integration</div> <div class="section-kicker">{{ $t('landing.codeDemo.kicker') }}</div>
<h2 class="section-title"> base_url 一改就能用</h2> <h2 class="section-title">{{ $t('landing.codeDemo.title') }}</h2>
<p class="section-sub">兼容 OpenAI / Anthropic / Gemini SDK<span class="text-puro-cyan">零代码改动</span></p> <p class="section-sub">
<i18n-t keypath="landing.codeDemo.sub" tag="span">
<template #highlight><span class="text-puro-cyan">{{ $t('landing.codeDemo.subHighlight') }}</span></template>
</i18n-t>
</p>
</div> </div>
<div class="code-demo"> <div class="code-demo">
<div class="code-block"> <div class="code-block">
@@ -185,15 +198,15 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
-d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre> -d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre>
</div> </div>
</div> </div>
<div class="code-foot">支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE &amp; WebSocket</div> <div class="code-foot">{{ $t('landing.codeDemo.foot') }}</div>
</section> </section>
<!-- Dashboard mockup --> <!-- Dashboard mockup -->
<section class="block container" id="dashboard"> <section class="block container" id="dashboard">
<div class="section-header"> <div class="section-header">
<div class="section-kicker">// observability</div> <div class="section-kicker">{{ $t('landing.dashboard.kicker') }}</div>
<h2 class="section-title">每条请求都看得见</h2> <h2 class="section-title">{{ $t('landing.dashboard.title') }}</h2>
<p class="section-sub">不像第三方 API 池子那种"扣了多少不告诉你"扣哪个账号跑哪个模型用了多少 tokens上游响应几秒一目了然</p> <p class="section-sub">{{ $t('landing.dashboard.sub') }}</p>
</div> </div>
<div class="dash-mock"> <div class="dash-mock">
<!-- browser chrome header --> <!-- browser chrome header -->
@@ -224,15 +237,15 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
<!-- main content --> <!-- main content -->
<div class="dash-main"> <div class="dash-main">
<div class="stat-row"> <div class="stat-row">
<div class="stat"><div class="stat-label">今日请求</div><div class="stat-value">1,842</div><div class="stat-delta">+12.3%</div></div> <div class="stat"><div class="stat-label">{{ $t('landing.dashboard.statToday') }}</div><div class="stat-value">1,842</div><div class="stat-delta">+12.3%</div></div>
<div class="stat"><div class="stat-label">输入 Tokens</div><div class="stat-value">2.1M</div><div class="stat-delta">+8.1%</div></div> <div class="stat"><div class="stat-label">{{ $t('landing.dashboard.statTokensIn') }}</div><div class="stat-value">2.1M</div><div class="stat-delta">+8.1%</div></div>
<div class="stat"><div class="stat-label">输出 Tokens</div><div class="stat-value">485K</div><div class="stat-delta">+15.6%</div></div> <div class="stat"><div class="stat-label">{{ $t('landing.dashboard.statTokensOut') }}</div><div class="stat-value">485K</div><div class="stat-delta">+15.6%</div></div>
<div class="stat"><div class="stat-label">今日费用</div><div class="stat-value">$1.23</div><div class="stat-delta down">-4.2%</div></div> <div class="stat"><div class="stat-label">{{ $t('landing.dashboard.statCost') }}</div><div class="stat-value">$1.23</div><div class="stat-delta down">-4.2%</div></div>
</div> </div>
<div class="chart-grid"> <div class="chart-grid">
<div class="chart-card"> <div class="chart-card">
<div class="chart-title"> <div class="chart-title">
30 天用量趋势 {{ $t('landing.dashboard.chartTrend') }}
<div class="chart-legend"> <div class="chart-legend">
<span><span class="sw" style="background: var(--cyan)"></span> Claude</span> <span><span class="sw" style="background: var(--cyan)"></span> Claude</span>
<span><span class="sw" style="background: #a855f7"></span> GPT</span> <span><span class="sw" style="background: #a855f7"></span> GPT</span>
@@ -277,7 +290,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</div> </div>
<table class="log-table mono"> <table class="log-table mono">
<thead> <thead>
<tr><th>时间</th><th>模型</th><th>上游</th><th>状态</th><th>用量</th></tr> <tr><th>{{ $t('landing.dashboard.tableTime') }}</th><th>{{ $t('landing.dashboard.tableModel') }}</th><th>{{ $t('landing.dashboard.tableUpstream') }}</th><th>{{ $t('landing.dashboard.tableStatus') }}</th><th>{{ $t('landing.dashboard.tableUsage') }}</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>12:34:07</td><td>gpt-5.4</td><td><span class="provider gpt"><span class="dot"></span>ChatGPT #1</span></td><td class="status-200">200</td><td>2,341</td></tr> <tr><td>12:34:07</td><td>gpt-5.4</td><td><span class="provider gpt"><span class="dot"></span>ChatGPT #1</span></td><td class="status-200">200</td><td>2,341</td></tr>
@@ -301,24 +314,24 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</svg> </svg>
<span>PURO AI</span> <span>PURO AI</span>
</div> </div>
<p class="footer-tagline">把多个 AI 订阅聚合成统一 API<br>已经付过钱的订阅真正为你工作</p> <p class="footer-tagline">{{ $t('landing.footer.tagline1') }}<br>{{ $t('landing.footer.tagline2') }}</p>
<p class="footer-meta">© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api</p> <p class="footer-meta">© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api</p>
<div class="footer-status"><span class="dot-green"></span>all systems operational</div> <div class="footer-status"><span class="dot-green"></span>all systems operational</div>
</div> </div>
<div class="footer-col"> <div class="footer-col">
<div class="footer-col-title">产品</div> <div class="footer-col-title">{{ $t('landing.footer.colProducts') }}</div>
<a href="/docs">文档</a> <a href="/docs">{{ $t('landing.footer.linkDocs') }}</a>
<a href="#features">功能</a> <a href="#features">{{ $t('landing.footer.linkFeatures') }}</a>
<a href="https://git.puro.im/purovps/sub2api/commits/branch/main" target="_blank" rel="noopener noreferrer">更新日志</a> <a href="https://git.puro.im/purovps/sub2api/commits/branch/main" target="_blank" rel="noopener noreferrer">{{ $t('landing.footer.linkChangelog') }}</a>
</div> </div>
<div class="footer-col"> <div class="footer-col">
<div class="footer-col-title">账户</div> <div class="footer-col-title">{{ $t('landing.footer.colAccount') }}</div>
<router-link to="/login">登录</router-link> <router-link to="/login">{{ $t('landing.footer.linkLogin') }}</router-link>
<router-link to="/register">注册</router-link> <router-link to="/register">{{ $t('landing.footer.linkRegister') }}</router-link>
<a href="/dashboard">Dashboard</a> <a href="/dashboard">Dashboard</a>
</div> </div>
<div class="footer-col"> <div class="footer-col">
<div class="footer-col-title">联系</div> <div class="footer-col-title">{{ $t('landing.footer.colContact') }}</div>
<a href="mailto:admin@puro.im">admin@puro.im</a> <a href="mailto:admin@puro.im">admin@puro.im</a>
<a href="https://git.puro.im" target="_blank" rel="noopener noreferrer">git.puro.im</a> <a href="https://git.puro.im" target="_blank" rel="noopener noreferrer">git.puro.im</a>
<a href="https://git.puro.im/purovps/sub2api" target="_blank" rel="noopener noreferrer">GitHub </a> <a href="https://git.puro.im/purovps/sub2api" target="_blank" rel="noopener noreferrer">GitHub </a>
@@ -329,8 +342,7 @@ requires_openai_auth = <span class="kw">true</span></code></pre>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// LandingView — public marketing landing page for PURO AI import PuroLocaleSwitcher from '@/components/puro/PuroLocaleSwitcher.vue'
// Rendered at `/` when user is unauthenticated (see router/index.ts)
</script> </script>
<style scoped> <style scoped>

View File

@@ -0,0 +1,513 @@
<template>
<div class="puro-page">
<div class="bg-glow"></div>
<div class="grain"></div>
<!-- NAV -->
<nav class="nav">
<div class="container nav-inner">
<router-link to="/" class="brand">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</router-link>
<div class="nav-links">
<router-link to="/">{{ $t('pricing.nav.products') }}</router-link>
<a href="#" class="active">{{ $t('pricing.nav.pricing') }}</a>
<router-link to="/docs">{{ $t('pricing.nav.docs') }}</router-link>
</div>
<div class="nav-cta">
<PuroLocaleSwitcher />
<router-link to="/login" class="btn btn-ghost">{{ $t('pricing.nav.login') }}</router-link>
<router-link to="/register" class="btn btn-primary">{{ $t('pricing.nav.signup') }}</router-link>
</div>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="section-kicker" style="margin-bottom:14px;">{{ $t('pricing.hero.kicker') }}</div>
<div class="preview-pill">{{ $t('pricing.hero.previewPill') }}</div>
<h1>{{ $t('pricing.hero.title1') }}<span class="accent">{{ $t('pricing.hero.titleAccent') }}</span>{{ $t('pricing.hero.title2') }}</h1>
<p class="sub">
<i18n-t keypath="pricing.hero.sub" tag="span">
<template #discount><b class="text-cyan">{{ $t('pricing.hero.subDiscount') }}</b></template>
</i18n-t>
</p>
<div class="underline">
<span class="dot"></span>
{{ $t('pricing.hero.underline') }}
</div>
</section>
<!-- PRICING GRID -->
<div class="pricing-wrap">
<div class="pricing-grid">
<!-- STARTER -->
<div class="tier">
<span class="flag muted">{{ $t('pricing.tiers.starter.flag') }}</span>
<div class="tier-name">{{ $t('pricing.tiers.starter.tierLabel') }}</div>
<div class="tier-headline">{{ $t('pricing.tiers.starter.headline') }}</div>
<div class="price-row"><span class="price"><span class="curr">$</span>{{ $t('pricing.tiers.starter.creditAmount') }}</span></div>
<div class="credit-line">
<i18n-t keypath="pricing.tiers.starter.credit" tag="span">
<template #creditAmount>{{ $t('pricing.tiers.starter.creditAmount') }}</template>
<template #creditBonus><b>{{ $t('pricing.tiers.starter.creditBonus') }}</b> <span class="bonus">+21%</span></template>
</i18n-t>
</div>
<span class="discount-tag">{{ $t('pricing.tiers.starter.discountTag') }}</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.starter.features.allModels') }}</div>
<div class="feat"><span class="tick"></span><b>1</b> {{ $t('pricing.tiers.starter.features.oneKey') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.starter.features.rpm60') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.starter.features.log7') }}</div>
<div class="feat muted"><span class="tick"></span>{{ $t('pricing.tiers.starter.features.noBYOS') }}</div>
<div class="feat muted"><span class="tick"></span>{{ $t('pricing.tiers.starter.features.noTeam') }}</div>
</div>
<router-link to="/register" class="btn btn-ghost btn-lg tier-cta">{{ $t('pricing.tiers.starter.cta') }}</router-link>
</div>
<!-- PRO -->
<div class="tier popular">
<span class="flag">{{ $t('pricing.tiers.pro.flag') }}</span>
<div class="tier-name">{{ $t('pricing.tiers.pro.tierLabel') }}</div>
<div class="tier-headline">{{ $t('pricing.tiers.pro.headline') }}</div>
<div class="price-row"><span class="price"><span class="curr">$</span>{{ $t('pricing.tiers.pro.creditAmount') }}</span></div>
<div class="credit-line">
<i18n-t keypath="pricing.tiers.pro.credit" tag="span">
<template #creditAmount>{{ $t('pricing.tiers.pro.creditAmount') }}</template>
<template #creditBonus><b>{{ $t('pricing.tiers.pro.creditBonus') }}</b> <span class="bonus">+50%</span></template>
</i18n-t>
</div>
<span class="discount-tag">{{ $t('pricing.tiers.pro.discountTag') }}</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.pro.features.allModels') }}</div>
<div class="feat"><span class="tick"></span><b>3</b> {{ $t('pricing.tiers.pro.features.threeKeys') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.pro.features.rpm120') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.pro.features.log30') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.pro.features.byos') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.pro.features.failover') }}</div>
</div>
<router-link to="/register" class="btn btn-primary btn-lg tier-cta">{{ $t('pricing.tiers.pro.cta') }}</router-link>
</div>
<!-- SCALE -->
<div class="tier">
<span class="flag amber">{{ $t('pricing.tiers.scale.flag') }}</span>
<div class="tier-name">{{ $t('pricing.tiers.scale.tierLabel') }}</div>
<div class="tier-headline">{{ $t('pricing.tiers.scale.headline') }}</div>
<div class="price-row"><span class="price"><span class="curr">$</span>{{ $t('pricing.tiers.scale.creditAmount') }}</span></div>
<div class="credit-line">
<i18n-t keypath="pricing.tiers.scale.credit" tag="span">
<template #creditAmount>{{ $t('pricing.tiers.scale.creditAmount') }}</template>
<template #creditBonus><b>{{ $t('pricing.tiers.scale.creditBonus') }}</b> <span class="bonus">+100%</span></template>
</i18n-t>
</div>
<span class="discount-tag">{{ $t('pricing.tiers.scale.discountTag') }}</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.scale.features.proAll') }}</div>
<div class="feat"><span class="tick"></span><b>10</b> {{ $t('pricing.tiers.scale.features.tenKeys') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.scale.features.rpm300') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.scale.features.log90') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.scale.features.priority') }}<span class="soon-chip">{{ $t('pricing.soonChip') }}</span></div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.scale.features.community') }}</div>
</div>
<router-link to="/register" class="btn btn-ghost btn-lg tier-cta">{{ $t('pricing.tiers.scale.cta') }}</router-link>
</div>
<!-- CUSTOM -->
<div class="tier">
<span class="flag muted">{{ $t('pricing.tiers.custom.flag') }}</span>
<div class="tier-name">{{ $t('pricing.tiers.custom.tierLabel') }}</div>
<div class="tier-headline">{{ $t('pricing.tiers.custom.headline') }}</div>
<div class="price-row"><span class="price"><span class="curr">$</span>{{ customAmt }}</span></div>
<div class="credit-line">{{ $t('pricing.tiers.custom.creditPrefix') }} <b>${{ customCredit }}</b> <span class="bonus">{{ $t('pricing.tiers.custom.bonusPrefix') }}{{ customBonus }}%</span></div>
<input type="range" min="10" max="500" value="50" step="10" v-model.number="customAmt" style="-webkit-appearance:none; width:100%; height:4px; background:var(--border); border-radius:2px; margin-bottom:12px;">
<span class="discount-tag">{{ $t('pricing.tiers.custom.discountTag') }}</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.custom.features.neverExpire') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.custom.features.proAll') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.custom.features.tiered') }}</div>
<div class="feat"><span class="tick"></span>{{ $t('pricing.tiers.custom.features.payment') }}</div>
<div class="feat muted"><span class="tick"></span>{{ $t('pricing.tiers.custom.features.preview') }}</div>
</div>
<router-link to="/register" class="btn btn-ghost btn-lg tier-cta">{{ $t('pricing.tiers.custom.cta') }}</router-link>
</div>
</div>
<!-- CUSTOM ROW -->
<div class="custom-row">
<div class="custom-card">
<div class="icon purple">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 20h20"/>
<path d="M4 20V10l8-6 8 6v10"/>
<path d="M9 20v-7h6v7"/>
</svg>
</div>
<div style="flex:1;">
<h3>{{ $t('pricing.custom.enterprise.title') }}</h3>
<p>{{ $t('pricing.custom.enterprise.desc') }}</p>
</div>
<a href="mailto:contact@puro.im" class="btn btn-ghost">{{ $t('pricing.custom.enterprise.cta') }}</a>
</div>
<div class="custom-card">
<div class="icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
</div>
<div style="flex:1;">
<h3>{{ $t('pricing.custom.binding.title') }}</h3>
<p>
<i18n-t keypath="pricing.custom.binding.desc" tag="span">
<template #price><code class="pill">{{ $t('pricing.custom.binding.price') }}</code></template>
</i18n-t>
</p>
</div>
<router-link to="/register" class="btn btn-ghost">{{ $t('pricing.custom.binding.cta') }}</router-link>
</div>
</div>
</div>
<!-- CALCULATOR -->
<section class="calc-section">
<div class="calc-preview-pill">{{ $t('pricing.calc.previewPill') }}</div>
<PricingCalculator />
</section>
<!-- WORKS EVERYWHERE -->
<section class="works">
<div class="section-head">
<div class="kicker">{{ $t('pricing.works.kicker') }}</div>
<h2>{{ $t('pricing.works.title') }}</h2>
<p>
<i18n-t keypath="pricing.works.sub" tag="span">
<template #baseUrl><code class="pill">{{ $t('pricing.works.baseUrl') }}</code></template>
</i18n-t>
</p>
</div>
<div class="tools-grid">
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.claudeCode') }}</div>
<div class="tag">{{ $t('pricing.works.tags.claudeCode') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18L18 6M8 6h10v10"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.cursor') }}</div>
<div class="tag">{{ $t('pricing.works.tags.cursor') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.cline') }}</div>
<div class="tag">{{ $t('pricing.works.tags.cline') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8"/><path d="m8 12 2 2 6-6"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.rooCode') }}</div>
<div class="tag">{{ $t('pricing.works.tags.rooCode') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.continueTag') }}</div>
<div class="tag">{{ $t('pricing.works.tags.continueTag') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.openaiSdk') }}</div>
<div class="tag">{{ $t('pricing.works.tags.openaiSdk') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.anthropicSdk') }}</div>
<div class="tag">{{ $t('pricing.works.tags.anthropicSdk') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M8 5v14"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.openWebui') }}</div>
<div class="tag">{{ $t('pricing.works.tags.openWebui') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7v10l10 5 10-5V7z"/><path d="M12 22V12M2 7l10 5 10-5"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.langchain') }}</div>
<div class="tag">{{ $t('pricing.works.tags.langchain') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><path d="M6 9v3l6 3M18 9v3l-6 3"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.llamaIndex') }}</div>
<div class="tag">{{ $t('pricing.works.tags.llamaIndex') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9z"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.zed') }}</div>
<div class="tag">{{ $t('pricing.works.tags.zed') }}</div>
</div>
<div class="tool">
<div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z"/></svg></div>
<div class="name">{{ $t('pricing.works.tools.more') }}</div>
<div class="tag">{{ $t('pricing.works.tags.more') }}</div>
</div>
</div>
</section>
<!-- FAQ -->
<section class="faq-section">
<div class="section-head">
<div class="kicker">{{ $t('pricing.faq.kicker') }}</div>
<h2>{{ $t('pricing.faq.title') }}</h2>
<p>
<i18n-t keypath="pricing.faq.noAnswer" tag="span">
<template #contact><a href="mailto:contact@puro.im" style="color:var(--cyan)">{{ $t('pricing.faq.contact') }}</a></template>
</i18n-t>
</p>
</div>
<details class="faq" open>
<summary><span class="num">01</span>{{ $t('pricing.faq.q1') }}</summary>
<div class="answer">{{ $t('pricing.faq.a1') }}</div>
</details>
<details class="faq">
<summary><span class="num">02</span>{{ $t('pricing.faq.q2') }}</summary>
<div class="answer">
{{ $t('pricing.faq.a2') }}
</div>
</details>
<details class="faq">
<summary><span class="num">03</span>{{ $t('pricing.faq.q3') }}</summary>
<div class="answer">
<b>{{ $t('pricing.faq.a3Part1') }}</b>
{{ $t('pricing.faq.a3Part2') }} <a href="#">{{ $t('pricing.faq.a3Link') }}</a>{{ $t('pricing.faq.a3Part3') }}
</div>
</details>
<details class="faq">
<summary><span class="num">04</span>{{ $t('pricing.faq.q4') }}</summary>
<div class="answer">{{ $t('pricing.faq.a4') }}</div>
</details>
<details class="faq">
<summary><span class="num">05</span>{{ $t('pricing.faq.q5') }}</summary>
<div class="answer">
<ul>
<li><b>{{ $t('pricing.faq.a5StarterLabel') }}</b> {{ $t('pricing.faq.a5Starter') }}</li>
<li><b>{{ $t('pricing.faq.a5ProLabel') }}</b> {{ $t('pricing.faq.a5Pro') }}</li>
<li><b>{{ $t('pricing.faq.a5EnterpriseLabel') }}</b> {{ $t('pricing.faq.a5Enterprise') }}</li>
</ul>
</div>
</details>
<details class="faq">
<summary><span class="num">06</span>{{ $t('pricing.faq.q6') }}</summary>
<div class="answer">
{{ $t('pricing.faq.a6') }}
</div>
</details>
<details class="faq">
<summary><span class="num">07</span>{{ $t('pricing.faq.q7') }}</summary>
<div class="answer">{{ $t('pricing.faq.a7') }}</div>
</details>
<details class="faq">
<summary><span class="num">08</span>{{ $t('pricing.faq.q8') }}</summary>
<div class="answer">
<b>{{ $t('pricing.faq.a8Part1') }}</b> {{ $t('pricing.faq.a8Part2') }}
</div>
</details>
<details class="faq">
<summary><span class="num">09</span>{{ $t('pricing.faq.q9') }}</summary>
<div class="answer">
{{ $t('pricing.faq.a9') }} <a href="mailto:contact@puro.im">{{ $t('pricing.faq.a9Link') }}</a>
</div>
</details>
<details class="faq">
<summary><span class="num">10</span>{{ $t('pricing.faq.q10') }}</summary>
<div class="answer">
{{ $t('pricing.faq.a10') }} 24 {{ $t('pricing.faq.a10Part2') }} <a href="/docs">{{ $t('pricing.faq.a10Link') }}</a>.
</div>
</details>
</section>
<!-- FINAL CTA -->
<section class="final-cta">
<div class="final-cta-inner">
<div class="section-kicker" style="margin-bottom:12px;">{{ $t('pricing.finalCta.kicker') }}</div>
<h2>{{ $t('pricing.finalCta.title') }}</h2>
<p>{{ $t('pricing.finalCta.subtitle') }}</p>
<div style="display:inline-flex; gap:12px;">
<router-link to="/register" class="btn btn-primary btn-lg">{{ $t('pricing.finalCta.ctaPrimary') }}</router-link>
<router-link to="/docs" class="btn btn-ghost btn-lg">{{ $t('pricing.finalCta.ctaDocs') }}</router-link>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import PuroLocaleSwitcher from '@/components/puro/PuroLocaleSwitcher.vue'
import PricingCalculator from '@/components/puro/PricingCalculator.vue'
const customAmt = ref(50)
const customBonus = computed(() => {
const v = customAmt.value
if (v >= 500) return 120
if (v >= 200) return 110
if (v >= 99) return 100
if (v >= 50) return 70
if (v >= 30) return 50
if (v >= 20) return 35
return 21
})
const customCredit = computed(() => Math.round(customAmt.value * (1 + customBonus.value / 100)))
</script>
<style scoped>
.hero { max-width: 1180px; margin: 0 auto; padding: 80px 32px 40px; text-align: center; }
.hero h1 { font-size: 54px; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 18px; }
.hero h1 .accent { color: var(--cyan); }
.hero .sub { color: var(--text-2); font-size: 17px; max-width: 620px; margin: 0 auto 14px; line-height: 1.6; }
.hero .underline { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 12px; color: var(--text-3); padding: 6px 14px; background: rgba(2, 6, 23, 0.5); border: 1px solid var(--border); border-radius: 100px; }
.hero .underline .dot { width: 6px; height: 6px; background: var(--green); border-radius: 50%; box-shadow: 0 0 0 3px rgba(52,211,153,0.15); }
.pricing-wrap { max-width: 1180px; margin: 0 auto; padding: 20px 32px 40px; }
.pricing-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.tier { position: relative; border: 1px solid var(--border); border-radius: var(--r-xl); background: rgba(15, 23, 42, 0.5); padding: 28px 24px; display: flex; flex-direction: column; transition: all .2s; }
.tier:hover { border-color: var(--border-2); transform: translateY(-3px); }
.tier.popular { border-color: rgba(34, 211, 238, 0.4); background: radial-gradient(500px 300px at 50% 0%, rgba(34,211,238,0.08), transparent 60%), rgba(15, 23, 42, 0.7); box-shadow: 0 0 0 1px rgba(34,211,238,0.15), 0 20px 40px -20px rgba(34,211,238,0.2); transform: translateY(-6px); }
.tier.popular:hover { transform: translateY(-9px); }
.tier .flag { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; padding: 4px 12px; border-radius: 100px; background: var(--cyan); color: #042f2e; font-weight: 700; white-space: nowrap; }
.tier .flag.amber { background: var(--amber); color: #422006; }
.tier .flag.muted { background: rgba(100, 116, 139, 0.2); color: var(--text-2); border: 1px solid var(--border); }
.tier-name { font-size: 13px; font-family: var(--font-mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3); margin-bottom: 8px; }
.tier-headline { font-size: 16px; font-weight: 600; color: var(--text-0); margin-bottom: 22px; line-height: 1.35; min-height: 44px; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin-bottom: 4px; }
.price { font-family: var(--font-mono); font-size: 42px; font-weight: 800; letter-spacing: -0.03em; color: var(--text-0); }
.tier.popular .price { color: var(--cyan); }
.price .curr { font-size: 18px; font-weight: 600; color: var(--text-3); margin-right: 2px; vertical-align: super; }
.credit-line { font-family: var(--font-mono); font-size: 12px; color: var(--cyan); margin-bottom: 14px; }
.credit-line .arrow { margin: 0 6px; color: var(--text-3); }
.credit-line .bonus { padding: 2px 8px; background: rgba(34,211,238,0.08); border: 1px solid rgba(34,211,238,0.25); border-radius: 4px; font-weight: 600; margin-left: 6px; }
.discount-tag { display: inline-block; font-family: var(--font-mono); font-size: 11px; color: var(--amber); background: rgba(251,191,36,0.08); border: 1px solid rgba(251,191,36,0.25); border-radius: 4px; padding: 3px 8px; margin-bottom: 18px; }
.tier hr { border: none; border-top: 1px dashed var(--border); margin: 4px 0 18px; }
.feat { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; color: var(--text-1); padding: 4px 0; line-height: 1.55; }
.feat .tick { color: var(--cyan); flex-shrink: 0; margin-top: 2px; }
.feat.muted { color: var(--text-3); }
.feat.muted .tick { color: var(--text-3); }
.feat b { color: var(--text-0); font-weight: 600; }
.feats { display: flex; flex-direction: column; gap: 2px; margin-bottom: 24px; flex: 1; }
.tier-cta { width: 100%; justify-content: center; }
.custom-row { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.custom-card { padding: 24px; border: 1px solid var(--border); border-radius: var(--r-xl); background: linear-gradient(135deg, rgba(168,85,247,0.05), transparent 50%), rgba(15, 23, 42, 0.4); display: flex; align-items: center; gap: 22px; }
.custom-card .icon { width: 48px; height: 48px; border-radius: 10px; background: rgba(34,211,238,0.1); color: var(--cyan); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.custom-card .icon.purple { background: rgba(168,85,247,0.1); color: var(--purple); }
.custom-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
.custom-card p { font-size: 13px; color: var(--text-2); line-height: 1.5; }
.custom-card .btn { margin-left: auto; flex-shrink: 0; }
.works { max-width: 1180px; margin: 0 auto; padding: 80px 32px 40px; }
.section-head { text-align: center; margin-bottom: 32px; }
.section-head .kicker { font-family: var(--font-mono); font-size: 12px; color: var(--cyan); letter-spacing: 0.14em; margin-bottom: 10px; }
.section-head h2 { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 8px; }
.section-head p { color: var(--text-2); font-size: 15px; max-width: 560px; margin: 0 auto; line-height: 1.55; }
.tools-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.tool { padding: 18px 14px; border: 1px solid var(--border); border-radius: var(--r-md); background: rgba(15, 23, 42, 0.4); text-align: center; transition: all .15s; }
.tool:hover { border-color: var(--border-2); background: rgba(15, 23, 42, 0.7); }
.tool .logo { width: 28px; height: 28px; margin: 0 auto 10px; display: flex; align-items: center; justify-content: center; color: var(--text-1); }
.tool .name { font-size: 12px; font-weight: 500; color: var(--text-1); }
.tool .tag { font-size: 10px; color: var(--text-3); margin-top: 2px; font-family: var(--font-mono); }
.calc-section { max-width: 1180px; margin: 0 auto; padding: 40px 32px; }
.faq-section { max-width: 880px; margin: 0 auto; padding: 60px 32px 100px; }
.faq { border: 1px solid var(--border); border-radius: var(--r-md); background: rgba(15, 23, 42, 0.4); margin-bottom: 8px; overflow: hidden; transition: all .15s; }
.faq:hover { border-color: var(--border-2); }
.faq summary { padding: 18px 22px; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 14px; font-size: 15px; font-weight: 500; color: var(--text-0); position: relative; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; margin-left: auto; font-family: var(--font-mono); font-size: 18px; color: var(--text-3); transition: transform .2s; }
.faq[open] summary::after { content: ""; color: var(--cyan); }
.faq summary .num { font-family: var(--font-mono); font-size: 11px; color: var(--cyan); letter-spacing: 0.1em; min-width: 26px; }
.faq .answer { padding: 0 22px 20px 62px; color: var(--text-2); font-size: 14px; line-height: 1.7; }
.faq .answer code { font-family: var(--font-mono); background: rgba(2, 6, 23, 0.6); padding: 1px 6px; border-radius: 3px; color: var(--cyan); font-size: 12.5px; }
.faq .answer a { color: var(--cyan); }
.faq .answer ul { padding-left: 20px; margin-top: 8px; }
.final-cta { max-width: 1180px; margin: 40px auto 80px; padding: 0 32px; }
.final-cta-inner { padding: 48px; border: 1px solid var(--border); border-radius: var(--r-xl); background: radial-gradient(800px 400px at 50% 0%, rgba(34,211,238,0.08), transparent 60%), rgba(15, 23, 42, 0.6); text-align: center; }
.final-cta-inner h2 { font-size: 32px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 10px; }
.final-cta-inner p { color: var(--text-2); font-size: 15px; margin-bottom: 26px; }
.preview-pill {
display: inline-block;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--amber, #fbbf24);
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.25);
border-radius: 100px;
padding: 4px 12px;
margin-bottom: 14px;
}
.calc-preview-pill {
display: inline-block;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--amber, #fbbf24);
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.25);
border-radius: 100px;
padding: 4px 12px;
margin-bottom: 14px;
}
.soon-chip {
display: inline-block;
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.1em;
color: var(--amber, #fbbf24);
background: rgba(251, 191, 36, 0.1);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: 3px;
padding: 1px 5px;
margin-left: 6px;
vertical-align: middle;
}
/* pill inline code */
.pill { font-family: var(--font-mono); background: rgba(2, 6, 23, 0.6); padding: 1px 6px; border-radius: 3px; color: var(--cyan); font-size: 12.5px; }
/* nav brand */
.brand { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text-0); font-weight: 700; font-size: 15px; }
.hex { width: 24px; height: 24px; color: var(--cyan); }
.nav-links .active { color: var(--cyan); }
@media (max-width: 960px) {
.pricing-grid { grid-template-columns: 1fr 1fr; }
.custom-row { grid-template-columns: 1fr; }
.tools-grid { grid-template-columns: repeat(3, 1fr); }
}
</style>