feat(web): 官网 ui_kits/website → Astro 纯静态 SSG 迁移 (tsk_acMYQ-Z-EIF_)
新建 web/website/(Astro 零 SSR): - 组件迁移:交互块保留 .jsx 经 @astrojs/react(Header/AnnouncementBar/SignupForm/PricingPlans),纯展示块转 .astro 去运行时 JS;像素以原型为基准。 - 令牌同源:build-tokens.mjs 从 design/colors_and_type.css 生成 tokens.gen.css,仅剔除第三方 Google Fonts @import,数值不改。 - 字体自托管:@fontsource(Sora/Manrope/Noto Sans SC/JetBrains Mono),无第三方 CDN。 - 图标:Lucide 构建期内联 SVG(替代 unpkg CDN),零运行时、零 CDN。 - i18n:/(zh)与 /en/(en)双路由单显,语言切换组件;文案沿用 ui_kits 脱敏文案。 - 安全:public/_headers 严格 CSP(全 self + 自托管资源 + 内联片段 sha256,无 unsafe-inline)+ HSTS;无支付表单。 - CI:.gitea/workflows/website.yml 构建(红线扫描+lint+CSP 哈希)→ 同时发布 Cloudflare Pages 主站与镜像。 - 灾备:README 写明干净环境 npm ci && npm run build 可直接部署到任意静态托管;dist 指纹确定性,主站镜像一致。 测试:npm run lint(0 error)/ npm test(build+CSP 哈希注入+红线扫描 0 命中)/ 两次干净构建 dist 指纹一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
---
|
||||
import Icon from './Icon.astro';
|
||||
import PricingPlans from './PricingPlans.jsx';
|
||||
import { PRICES, type T } from '../i18n/strings';
|
||||
|
||||
interface Props { t: T }
|
||||
const { t } = Astro.props;
|
||||
|
||||
const plansData = {
|
||||
monthly: t('price.monthly'),
|
||||
yearly: t('price.yearly'),
|
||||
save: t('price.save'),
|
||||
permo: t('price.permo'),
|
||||
popular: t('price.popular'),
|
||||
plans: [
|
||||
{
|
||||
key: 'free',
|
||||
name: t('price.free'),
|
||||
price: PRICES.free,
|
||||
desc: t('price.freedesc'),
|
||||
feats: [t('pf.free1'), t('pf.free2'), t('pf.free3'), t('pf.free4')],
|
||||
cta: t('price.cta_free'),
|
||||
ctaClass: 'pcta-out',
|
||||
highlight: false,
|
||||
},
|
||||
{
|
||||
key: 'pro',
|
||||
name: t('price.pro'),
|
||||
price: PRICES.pro,
|
||||
desc: t('price.prodesc'),
|
||||
feats: [t('pf.pro1'), t('pf.pro2'), t('pf.pro3'), t('pf.pro4'), t('pf.pro5')],
|
||||
cta: t('price.cta_pro'),
|
||||
ctaClass: 'pcta-white',
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
key: 'team',
|
||||
name: t('price.team'),
|
||||
price: PRICES.team,
|
||||
desc: t('price.teamdesc'),
|
||||
feats: [t('pf.team1'), t('pf.team2'), t('pf.team3'), t('pf.team4')],
|
||||
cta: t('price.cta_team'),
|
||||
ctaClass: 'pcta-fill',
|
||||
highlight: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 功能对比表行:[名称键, free, pro, team];值可为字符串或 'check' / 'dash'。
|
||||
const rows = [
|
||||
{ label: 'cmp.locations', free: '1', pro: '80+', team: '80+' },
|
||||
{ label: 'cmp.data', free: t('cmp.daily'), pro: t('cmp.unlimited'), team: t('cmp.unlimited') },
|
||||
{ label: 'cmp.time', free: t('cmp.time_free'), pro: t('cmp.unlimited'), team: t('cmp.unlimited') },
|
||||
{ label: 'cmp.devices', free: '1', pro: '5', team: '10' },
|
||||
{ label: 'cmp.speed', free: 'dash', pro: 'check', team: 'check' },
|
||||
{ label: 'cmp.stream', free: 'dash', pro: 'check', team: 'check' },
|
||||
{ label: 'cmp.kill', free: 'dash', pro: 'check', team: 'check' },
|
||||
{ label: 'cmp.support', free: t('cmp.basic'), pro: t('cmp.basic'), team: t('cmp.priority') },
|
||||
];
|
||||
|
||||
function cell(v: string) {
|
||||
return v === 'check' ? 'yes' : v === 'dash' ? 'no' : 'tx';
|
||||
}
|
||||
---
|
||||
<section id="pricing">
|
||||
<div class="wrap center">
|
||||
<div class="eyebrow">{t('price.eyebrow')}</div>
|
||||
<h2 class="h-sec">{t('price.h')}</h2>
|
||||
<p class="sub-sec">{t('price.sub')}</p>
|
||||
</div>
|
||||
|
||||
<PricingPlans client:visible data={plansData} />
|
||||
|
||||
<div class="wrap">
|
||||
<!-- payment note -->
|
||||
<div class="pay-note">
|
||||
<Icon name="shield-check" />
|
||||
<div>
|
||||
<b>{t('pay.title')}</b><br>
|
||||
<span>{t('pay.body')}</span>
|
||||
<div class="chips">
|
||||
<span class="chip"><Icon name="shopping-bag" /><span>{t('pay.store')}</span></span>
|
||||
<span class="chip"><Icon name="credit-card" /><span>{t('pay.usdt')}</span></span>
|
||||
<span class="chip"><Icon name="send" />Telegram @PangolinVPN_bot</span>
|
||||
<span class="chip"><Icon name="message-circle" />LINE @pangolinvpn</span>
|
||||
<span class="chip"><Icon name="mail" />buy@pangolin.vpn</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- comparison table -->
|
||||
<h3 class="cmp-h">{t('cmp.h')}</h3>
|
||||
<div class="cmp">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('cmp.feature')}</th>
|
||||
<th>{t('price.free')}</th>
|
||||
<th>{t('price.pro')}</th>
|
||||
<th>{t('price.team')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr>
|
||||
<td>{t(r.label)}</td>
|
||||
{[r.free, r.pro, r.team].map((v) => (
|
||||
<td class={cell(v)}>
|
||||
{v === 'check' ? <Icon name="check" /> : v === 'dash' ? '—' : v}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user