Files
sub2api/frontend/src/components/puro/PricingCalculator.vue
mini b989c50317
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
feat(pricing): add PricingView + calculator with bilingual i18n
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

118 lines
5.9 KiB
Vue

<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>