chore: 初始化 Viper 官网独立仓库

ViperWebSite 原被 SMANAGE 主仓 .gitignore 排除,现拆为独立仓库并接入 git.gvsds.com/GVSDS/ViperWebSite。

包含:Vue 3 + TS + Vite 官网源码(首屏巨字滚动飞行 / Features 卡片鼠标跟随光晕 / 跨 section 背景融合)、Lenis 平滑惯性滚动、暗黑模式(属性驱动 + 变量覆盖 + 防闪白)、deploy.ps1 部署脚本、.gitignore。
This commit is contained in:
TermiNexus committed 2026-09-28 17:26:22 +08:00
commit 01a51477ed
29 files changed
+5307

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# 依赖与构建产物
node_modules/
dist/
# 日志与临时文件
*.log
.DS_Store
# 编辑器
.vscode/
.idea/
+106
View File
@@ -0,0 +1,106 @@
# ViperWebSite deploy script: build and sync dist to production dir (TempLate/Viper)
#
# IMPORTANT: keep this file pure ASCII. PS 5.1 parses BOM-less UTF-8 files
# using the ANSI codepage, so non-ASCII text would be garbled.
#
# Usage:
# 1. Clean old artifacts (dist and TempLate/Viper) so no stale hashes remain
# 2. Full rebuild (vue-tsc type check + vite build)
# 3. Sync dist to TempLate/Viper (index.html + assets)
# 4. Verify TempLate/Viper matches dist exactly
#
# Run: .\deploy.ps1
# (if execution policy blocks: Set-ExecutionPolicy -Scope Process Bypass)
$ErrorActionPreference = 'Stop'
$ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$DistDir = Join-Path $ProjectRoot 'dist'
$ProdRoot = Join-Path (Split-Path -Parent $ProjectRoot) 'TempLate\Viper'
$ProdAssetsDir = Join-Path $ProdRoot 'assets'
$IndexHtml = Join-Path $ProdRoot 'index.html'
Write-Host "`n===== VIPER DEPLOY START =====" -ForegroundColor Cyan
Write-Host "Project dir : $ProjectRoot"
Write-Host "Prod dir : $ProdRoot"
Write-Host ""
# 1. Clean old artifacts
Write-Host '[1/4] Cleaning old build artifacts...' -ForegroundColor Yellow
if (Test-Path $DistDir) {
Remove-Item -Recurse -Force $DistDir
Write-Host " removed: $DistDir"
}
if (Test-Path $ProdAssetsDir) {
Remove-Item -Recurse -Force $ProdAssetsDir
Write-Host " removed: $ProdAssetsDir"
}
if (Test-Path $IndexHtml) {
Remove-Item -Force $IndexHtml
Write-Host " removed: $IndexHtml"
}
Write-Host ''
# 2. Full build (vue-tsc type check + vite build)
Write-Host '[2/4] Running npm run build (type check + full build)...' -ForegroundColor Yellow
Push-Location $ProjectRoot
try {
npm run build
if ($LASTEXITCODE -ne 0) {
throw "Build failed, exit code = $LASTEXITCODE"
}
} finally {
Pop-Location
}
Write-Host ' Build OK'
Write-Host ''
# 3. Sync artifacts to prod dir
Write-Host '[3/4] Syncing artifacts to prod dir...' -ForegroundColor Yellow
if (-not (Test-Path $ProdRoot)) {
New-Item -ItemType Directory -Path $ProdRoot -Force | Out-Null
}
Copy-Item -Recurse -Force (Join-Path $DistDir '*') $ProdRoot
Write-Host ' Sync done'
Write-Host ''
# 4. Verify prod dir matches dist (file name sets)
Write-Host '[4/4] Verifying artifact consistency...' -ForegroundColor Yellow
function Get-AssetNames([string]$dir) {
if (-not (Test-Path $dir)) { return @() }
return @(Get-ChildItem -Path $dir -Recurse -File | ForEach-Object { $_.FullName.Substring($dir.Length) })
}
$distNames = Get-AssetNames (Join-Path $DistDir 'assets')
$prodNames = Get-AssetNames $ProdAssetsDir
$distSet = @($distNames | Sort-Object -Unique)
$prodSet = @($prodNames | Sort-Object -Unique)
$onlyDist = @($distSet | Where-Object { $_ -notin $prodSet })
$onlyProd = @($prodSet | Where-Object { $_ -notin $distSet })
if ($onlyDist.Count -eq 0 -and $onlyProd.Count -eq 0) {
Write-Host " OK: $($distSet.Count) assets, prod matches dist exactly" -ForegroundColor Green
} else {
Write-Host ' MISMATCH:' -ForegroundColor Red
if ($onlyDist.Count) { Write-Host " only in dist: $onlyDist" -ForegroundColor Red }
if ($onlyProd.Count) { Write-Host " only in prod: $onlyProd" -ForegroundColor Red }
throw 'Artifact verification failed!'
}
# Verify the entry JS referenced by index.html exists
$indexContent = Get-Content $IndexHtml -Raw
$entryMatch = [regex]::Match($indexContent, 'src="([^"]+\.js)"')
if ($entryMatch.Success) {
$entryPath = Join-Path $ProdRoot ($entryMatch.Groups[1].Value.TrimStart('/'))
if (Test-Path $entryPath) {
Write-Host " entry JS OK: $($entryMatch.Groups[1].Value)" -ForegroundColor Green
} else {
throw "index.html entry JS not found: $entryPath"
}
} else {
throw 'No entry JS reference found in index.html'
}
Write-Host ''
Write-Host "===== VIPER DEPLOY DONE =====" -ForegroundColor Cyan
Write-Host 'Please hard-refresh the browser (Ctrl+F5) to load the new build.' -ForegroundColor Cyan
Vendored
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
export default component
}
+32
View File
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Viper — Python 语法,LLVM 语义:一门编译为原生机器码的系统级编程语言。Python syntax, LLVM semantics: a system-level language that compiles to native machine code.">
<meta name="theme-color" content="#ffffff">
<title>Viper · Python 语法,LLVM 语义</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<script>
// 首屏渲染前写入主题,避免"先画浅色再切暗色"的闪白。
// 优先级:localStorage 里的手动选择 > 系统偏好。
(function () {
var meta = document.querySelector('meta[name="theme-color"]')
try {
var saved = localStorage.getItem('viper-theme')
var theme = saved === 'light' || saved === 'dark'
? saved
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
document.documentElement.setAttribute('data-theme', theme)
if (meta) meta.setAttribute('content', theme === 'dark' ? '#0c1512' : '#ffffff')
} catch (e) {
document.documentElement.setAttribute('data-theme', 'light')
}
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+1245
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "viper-website",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "Viper language official website (viper.gvsds.com)",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/inter": "^5.3.0",
"@fontsource/playfair-display": "^5.3.0",
"lenis": "^1.3.26",
"vue": "^3.5.40"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/tsconfig": "^0.9.1",
"typescript": "~6.0.2",
"vite": "^8.2.0",
"vue-tsc": "^3.3.8"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0B120E"/>
<path d="M18 46 V18 h7 l9 15 9-15 h7 V46 H42 V31 L32 46 22 31 v15 Z"
fill="#A8E063" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

+81
View File
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, watchEffect, nextTick } from 'vue'
import Lenis from 'lenis'
import TheHeader from '@/components/TheHeader.vue'
import HeroSection from '@/components/HeroSection.vue'
import FeaturesSection from '@/components/FeaturesSection.vue'
import CompilerSection from '@/components/CompilerSection.vue'
import OopSection from '@/components/OopSection.vue'
import MetaSection from '@/components/MetaSection.vue'
import ReplSection from '@/components/ReplSection.vue'
import SourceSection from '@/components/SourceSection.vue'
import QuickStartSection from '@/components/QuickStartSection.vue'
import EcosystemSection from '@/components/EcosystemSection.vue'
import RoadmapSection from '@/components/RoadmapSection.vue'
import TheFooter from '@/components/TheFooter.vue'
import { currentLang } from '@/i18n'
// html lang 与页面语言同步
watchEffect(() => {
document.documentElement.lang = currentLang.value === 'zh' ? 'zh-CN' : 'en'
})
// 全局滚动揭示:观察 [data-reveal],进入视口即落定一次
function setupRevealObserver() {
const els = document.querySelectorAll('[data-reveal]')
if (!('IntersectionObserver' in window) || els.length === 0) return
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('in')
io.unobserve(entry.target)
}
}
},
{ threshold: 0.12, rootMargin: '0px 0px -7% 0px' },
)
els.forEach((el) => io.observe(el))
}
// Lenis 平滑惯性滚动实例(接管滚轮,手势结束后继续滑行一段距离再停)
let lenis: Lenis | null = null
onMounted(async () => {
await nextTick()
setupRevealObserver()
// lerp 越小滑行越久(0.1 为库默认值);wheelMultiplier 略放大单次滚动量
// anchors.offset 补偿固定顶栏高度(--nav-h 66px),确保锚点停靠不被遮挡
// autoRaf 由库内部自驱动 requestAnimationFrame;respectReducedMotion 默认开启,
// 系统开启"减弱动态效果"时自动退回原生滚动
lenis = new Lenis({
lerp: 0.08,
wheelMultiplier: 1.05,
anchors: { offset: -76 },
autoRaf: true,
})
})
onBeforeUnmount(() => {
lenis?.destroy()
lenis = null
})
</script>
<template>
<TheHeader />
<main>
<HeroSection />
<FeaturesSection />
<CompilerSection />
<OopSection />
<MetaSection />
<ReplSection />
<SourceSection />
<QuickStartSection />
<EcosystemSection />
<RoadmapSection />
</main>
<TheFooter />
</template>
+401
View File
@@ -0,0 +1,401 @@
/* ============================================================
Viper 官网 · 设计系统
一张暗色画布 + 单一强调色(青柠),双字体族,克制运动
============================================================ */
:root {
/* 色板 —— 浅色企业风格 */
--bg: #ffffff;
--bg-soft: #f4f8f6;
--bg-deep: #0b1712; /* 深色区(代码窗 / 页脚) */
--panel: rgba(16, 32, 26, 0.03);
--panel-strong: rgba(16, 32, 26, 0.05);
--line: rgba(13, 194, 96, 0.32);
--line-soft: #dceee3;
--text: #10201a;
--text-dim: #5b6e64;
--text-faint: #8da097;
--accent: #0dc260; /* 主题色 */
--accent-rgb: 13, 194, 96; /* 主题色分量,供 rgba(var(--accent-rgb), α) 使用 */
--accent-strong: #08994b; /* 深一档:正文与悬停,保证对比度 */
--accent-ink: #ffffff; /* 压在主题色之上的文字:绿底一律配白字 */
--lime: #b7e64b; /* 黄绿:次级强调 */
--red: #f04438; /* 红:对比与警示 */
--sky: #38bdf8; /* 浅蓝:对比强调 */
--glow: rgba(13, 194, 96, 0.22);
--shadow-code: 0 26px 60px -30px rgba(15, 42, 32, 0.4);
/* 语义色:需要随主题整体换色的中性色,避免组件里散落硬编码 */
--glass: rgba(255, 255, 255, 0.88); /* 顶栏滚动后的毛玻璃底 */
--glass-strong: rgba(255, 255, 255, 0.97); /* 移动端下拉菜单面板底 */
--hover: rgba(16, 32, 26, 0.05); /* 中性悬浮底 */
--badge-ink: #0369a1; /* 浅蓝徽章文字 */
--badge-line: rgba(56, 189, 248, 0.55); /* 浅蓝徽章描边 */
/* 字体(本地栈,无外网依赖)
中文优先 Noto Sans SC(思源黑体),未安装时回退微软雅黑 / 苹方 / 系统 UI 字体;
英文与数字沿用 Segoe UI 前端栈。
等宽字体已弃用(用户反馈表现不佳):--font-mono 指向正文字体栈,
因此 .mono / .eyebrow / 代码窗等处的旧引用全部回落到正文字体。 */
--font-sans: 'Segoe UI', 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei',
'PingFang SC', 'Hiragino Sans GB', system-ui, -apple-system, sans-serif;
--font-mono: var(--font-sans); /* 弃用等宽(用户反馈表现不佳):指向正文字体栈,.mono 等旧引用自动回落 */
--font-brand: 'Playfair Display', Georgia, 'Times New Roman', 'Songti SC', serif; /* 品牌字专用:自托管衬线(@fontsource/playfair-display 900) */
/* 代码高亮(深色代码块内使用) */
--tok-kw: #ff7b72;
--tok-str: #a5d6ff;
--tok-num: #79c0ff;
--tok-type: #ffa657;
--tok-fn: #a8e063;
--tok-cmt: #6e8b79;
--tok-punc: #8b9f8f;
--tok-plain: #d5e3d8;
--radius: 10px;
--radius-sm: 6px;
--container: 1140px;
--nav-h: 66px;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ============================================================
暗黑模式
只覆盖颜色变量:结构、字体、动效、深色代码窗全部复用浅色规则,
组件不需要写任何 @media 或主题分支。
由 <html data-theme="dark"> 触发;该属性由 index.html 的内联脚本
在首屏渲染前写入(localStorage 手动选择优先,否则跟随系统),避免闪白。
============================================================ */
html[data-theme='dark'] {
--bg: #0c1512;
--bg-soft: #101d18;
--bg-deep: #060d0a; /* 比页面底色再深一档:代码窗 / 页脚继续读作"下沉"区域 */
--panel: rgba(255, 255, 255, 0.04);
--panel-strong: rgba(255, 255, 255, 0.07);
--line: rgba(13, 194, 96, 0.38);
--line-soft: rgba(255, 255, 255, 0.12);
--text: #e7f2ec;
--text-dim: #9db9ab;
--text-faint: #6f8d7e;
/* 暗底必须换更亮的绿:浅色下的 --accent-strong(#08994b) 在深背景上几乎读不出来 */
--accent-strong: #4ddb8a;
--glow: rgba(13, 194, 96, 0.3);
--shadow-code: 0 26px 60px -30px rgba(0, 0, 0, 0.75);
--glass: rgba(12, 21, 18, 0.82);
--glass-strong: rgba(10, 18, 15, 0.96);
--hover: rgba(255, 255, 255, 0.07);
--badge-ink: #7dd3fc;
--badge-line: rgba(56, 189, 248, 0.45);
}
html {
/* 横向裁剪放在 html(根元素)上直接对视口生效:
clip 不会把根元素变成滚动容器,Lenis 仍能接管纵向滚动;
前置的 hidden 是不支持 clip 的浏览器的回退(放在 body 上则会把 body 的 overflow-y 算成 auto,
让 Lenis 的 allowNestedScroll 误判 body 为嵌套滚动源)。 */
overflow-x: hidden;
overflow-x: clip;
scroll-behavior: smooth;
scroll-padding-top: calc(var(--nav-h) + 12px);
}
/* Lenis 接管滚动时关闭原生平滑,避免二次缓动叠加;锚点落位偏移由 Lenis anchors.offset 负责 */
html.lenis {
scroll-behavior: auto;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 16px;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
::selection {
background: var(--accent);
color: var(--accent-ink);
}
a {
color: inherit;
text-decoration: none;
}
img,
svg {
display: block;
max-width: 100%;
}
button {
font: inherit;
background: none;
border: none;
color: inherit;
cursor: pointer;
}
/* 滚动条 */
::-webkit-scrollbar {
width: 11px;
height: 11px;
}
::-webkit-scrollbar-thumb {
background: #c3d3cb;
border-radius: 6px;
border: 3px solid var(--bg);
}
::-webkit-scrollbar-thumb:hover {
background: #9db3a3;
}
/* 暗色下滑块要跟着变深(浅灰滑块在深底上过于刺眼) */
html[data-theme='dark'] ::-webkit-scrollbar-thumb {
background: #2c4038;
}
html[data-theme='dark'] ::-webkit-scrollbar-thumb:hover {
background: #3d584b;
}
/* ------- 版式工具 ------- */
.mono {
font-family: var(--font-mono);
}
.container {
width: min(var(--container), 100% - 48px);
margin-inline: auto;
}
/* 区块骨架:每个 section 一个职责 */
.section {
padding: clamp(5rem, 10vw, 8.5rem) 0;
position: relative;
}
.eyebrow {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.22em;
color: var(--accent);
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 10px;
}
.eyebrow::before {
content: '';
width: 26px;
height: 1px;
background: var(--accent);
opacity: 0.7;
}
.section-title {
font-size: clamp(1.9rem, 4vw, 2.9rem);
font-weight: 650;
letter-spacing: -0.02em;
line-height: 1.14;
margin: 18px 0 14px;
}
.section-sub {
color: var(--text-dim);
max-width: 56ch;
font-size: 1.02rem;
}
/* ------- 按钮 ------- */
.btn {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 12px 22px;
border-radius: 999px;
font-weight: 600;
font-size: 0.98rem;
transition: transform 0.22s var(--ease-out), box-shadow 0.25s var(--ease-out),
background 0.22s var(--ease-out), border-color 0.22s var(--ease-out);
will-change: transform;
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: var(--accent);
color: var(--accent-ink);
box-shadow: 0 0 0 1px var(--accent), 0 12px 28px -14px rgba(var(--accent-rgb), 0.75);
}
.btn-primary:hover {
background: #16d96d;
transform: translateY(-2px);
box-shadow: 0 0 0 1px #16d96d, 0 18px 38px -14px rgba(var(--accent-rgb), 0.85);
}
.btn-ghost {
border: 1px solid var(--line-soft);
color: var(--text);
background: transparent;
}
.btn-ghost:hover {
border-color: var(--accent);
background: rgba(var(--accent-rgb), 0.08);
color: var(--accent-strong);
transform: translateY(-2px);
}
/* ------- 进场 / 滚动揭示 ------- */
@keyframes riseIn {
from {
opacity: 0;
transform: translateY(26px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes caretBlink {
0%,
48% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
[data-reveal] {
opacity: 0;
transform: translateY(26px);
transition: opacity 0.8s ease, transform 0.85s var(--ease-out);
transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal].in {
opacity: 1;
transform: none;
}
/* 代码块基础(页面上保留深色代码窗,形成浅色页的对比锚点) */
.code-win {
--line-soft: rgba(255, 255, 255, 0.08);
--text-faint: #6d8a7c;
background: var(--bg-deep);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-code);
}
.code-win__bar {
display: flex;
align-items: center;
gap: 7px;
padding: 11px 16px;
border-bottom: 1px solid var(--line-soft);
background: rgba(255, 255, 255, 0.02);
}
.code-win__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #3c4a42;
}
.code-win__dot:nth-child(1) {
background: #d2644f;
}
.code-win__dot:nth-child(2) {
background: #d0a542;
}
.code-win__dot:nth-child(3) {
background: #3e8f6c;
}
.code-win__file {
margin-left: auto;
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-faint);
letter-spacing: 0.04em;
}
.code-win pre {
padding: 20px 22px 22px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.86rem;
line-height: 1.75;
color: var(--tok-plain);
tab-size: 4;
}
.code-win code {
display: block;
min-width: max-content;
}
.tok-kw { color: var(--tok-kw); }
.tok-str { color: var(--tok-str); }
.tok-num { color: var(--tok-num); }
.tok-type { color: var(--tok-type); }
.tok-fn { color: var(--tok-fn); }
.tok-cmt { color: var(--tok-cmt); font-style: italic; }
.tok-punc { color: var(--tok-punc); }
.tok-plain { color: var(--tok-plain); }
.caret {
display: inline-block;
width: 8px;
height: 1.05em;
margin-left: 3px;
vertical-align: text-bottom;
background: var(--accent);
animation: caretBlink 1.1s steps(1) infinite;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
[data-reveal] {
opacity: 1;
transform: none;
}
}
+265
View File
@@ -0,0 +1,265 @@
<script setup lang="ts">
import { t } from '@/i18n'
</script>
<template>
<section id="compiler" class="section compiler">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.compiler.eyebrow }}</p>
<h2 class="section-title">{{ t.compiler.title }}</h2>
<p class="section-sub">{{ t.compiler.sub }}</p>
</div>
<div class="pipeline" data-reveal>
<div class="rail">
<div class="rail-base" aria-hidden="true"></div>
<div class="rail-fill" aria-hidden="true"></div>
<div
v-for="(step, i) in t.compiler.steps"
:key="i"
class="step"
:style="{ '--stagger': `${i * 150}ms` }"
>
<span class="step-dot" aria-hidden="true"></span>
<span class="step-tag mono">{{ step.tag }}</span>
<p class="step-desc">{{ step.desc }}</p>
</div>
<div class="step step-final">
<span class="step-dot final-dot" aria-hidden="true"></span>
<span class="step-tag mono final-tag">app.exe ✓</span>
</div>
</div>
<!-- 缓存与构建开关 -->
<div class="cache" data-reveal>
<p class="cache-title mono">{{ t.compiler.cache.title }}</p>
<ul class="cache-list">
<li v-for="(c, i) in t.compiler.cache.items" :key="i" class="cache-row">
<span class="cache-flag mono">{{ c.flag }}</span>
<span class="cache-desc">{{ c.desc }}</span>
</li>
</ul>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.compiler {
background: var(--bg);
}
.sec-head {
margin-bottom: clamp(48px, 7vw, 80px);
}
/* 流水线 */
.pipeline {
position: relative;
}
.rail {
position: relative;
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr)) auto;
gap: 16px;
padding-top: 4px;
}
.rail-base,
.rail-fill {
position: absolute;
top: 11px;
left: 0;
right: 0;
height: 1px;
background: var(--line-soft);
}
.rail-fill {
background: linear-gradient(90deg, var(--accent), rgba(var(--accent-rgb), 0.2));
transform: scaleX(0);
transform-origin: left center;
box-shadow: 0 0 14px var(--glow);
transition: transform 1.4s var(--ease-out) 0.25s;
}
.pipeline[data-reveal].in .rail-fill {
transform: scaleX(1);
}
.step {
position: relative;
padding-top: 26px;
opacity: 0;
transform: translateY(16px);
transition: opacity 0.6s ease var(--stagger), transform 0.7s var(--ease-out) var(--stagger);
}
.step-final {
padding-top: 26px;
text-align: right;
}
.pipeline[data-reveal].in .step {
opacity: 1;
transform: none;
}
.step-dot {
position: absolute;
top: 4px;
left: 0;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line);
box-shadow: 0 0 0 4px var(--bg), 0 0 12px -2px var(--glow);
}
.final-dot {
background: var(--accent);
border-color: var(--accent);
}
.step-tag {
display: inline-block;
color: var(--accent-strong);
font-size: 0.9rem;
font-weight: 650;
letter-spacing: 0.02em;
border: 1px solid var(--line);
background: rgba(var(--accent-rgb), 0.05);
border-radius: var(--radius-sm);
padding: 5px 12px;
}
.step-final .final-tag {
color: var(--accent-ink);
background: var(--accent);
border-color: var(--accent);
font-weight: 750;
}
.step-desc {
color: var(--text-dim);
font-size: 0.92rem;
margin-top: 14px;
max-width: 30ch;
}
.step-final {
color: var(--text-dim);
}
/* 缓存与构建开关面板 */
.cache {
margin-top: clamp(34px, 5vw, 56px);
border: 1px solid var(--line-soft);
border-radius: var(--radius);
padding: 20px 22px;
}
.cache-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 14px;
}
.cache-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cache-row {
display: flex;
flex-direction: column;
gap: 5px;
padding: 11px 16px;
border: 1px solid var(--line-soft);
border-radius: var(--radius);
background: var(--panel);
transition: border-color 0.25s var(--ease-out), background 0.25s var(--ease-out);
}
.cache-row:hover {
border-color: rgba(var(--accent-rgb), 0.45);
background: rgba(var(--accent-rgb), 0.04);
}
.cache-flag {
color: var(--accent-strong);
font-size: 0.82rem;
white-space: nowrap;
}
.cache-desc {
color: var(--text-dim);
font-size: 0.8rem;
}
@media (max-width: 1180px) {
.step-tag {
font-size: 0.8rem;
padding: 4px 9px;
}
.step-desc {
font-size: 0.86rem;
}
}
@media (max-width: 980px) {
.cache-row {
flex: 1 1 100%;
}
}
@media (max-width: 980px) {
.rail {
grid-template-columns: 1fr;
gap: 0;
}
.step,
.step-final {
padding: 0 0 34px 40px;
text-align: left;
}
.rail-base,
.rail-fill {
top: 8px;
bottom: 8px;
left: 7px;
width: 1px;
height: auto;
}
.rail-fill {
transform: scaleY(0);
transform-origin: top center;
}
.pipeline[data-reveal].in .rail-fill {
transform: scaleY(1);
}
.step-dot {
left: 0;
top: 0;
}
.step-final .final-tag {
color: var(--accent-ink);
}
}
</style>
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
/** 生态:双列卡片网格(区别于行式列表与时间轴) */
import { t } from '@/i18n'
</script>
<template>
<section id="ecosystem" class="section ecosystem">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.ecosystem.eyebrow }}</p>
<h2 class="section-title">{{ t.ecosystem.title }}</h2>
<p class="section-sub">{{ t.ecosystem.sub }}</p>
</div>
<div class="eco-grid">
<a
v-for="(item, i) in t.ecosystem.items"
:key="i"
:href="item.href || undefined"
:target="item.href ? '_blank' : undefined"
:rel="item.href ? 'noreferrer' : undefined"
class="eco-card"
:class="{ linkable: item.href }"
:style="{ '--reveal-delay': `${(i % 3) * 70}ms` }"
data-reveal
>
<div class="eco-head">
<h3 class="eco-name mono">{{ item.name }}</h3>
<span v-if="item.badge" class="eco-badge mono">{{ item.badge }}</span>
<span v-if="item.href" class="eco-arrow" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M7 17 17 7M9 7h8v8" /></svg>
</span>
</div>
<span class="eco-role mono">{{ item.role }}</span>
<p class="eco-desc">{{ item.desc }}</p>
</a>
</div>
</div>
</section>
</template>
<style scoped>
.sec-head {
margin-bottom: clamp(36px, 5vw, 54px);
}
.eco-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.eco-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 22px 22px 20px;
border: 1px solid var(--line-soft);
border-radius: var(--radius);
background: var(--bg);
transition: border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out),
transform 0.25s var(--ease-out);
}
.eco-card.linkable:hover {
border-color: rgba(var(--accent-rgb), 0.45);
box-shadow: var(--shadow-code);
transform: translateY(-2px);
}
.eco-head {
display: flex;
align-items: center;
gap: 10px;
}
.eco-name {
font-size: 1.14rem;
font-weight: 700;
letter-spacing: -0.01em;
transition: color 0.25s var(--ease-out);
}
.eco-card.linkable:hover .eco-name {
color: var(--accent);
}
.eco-badge {
font-size: 0.66rem;
letter-spacing: 0.08em;
color: var(--badge-ink);
border: 1px solid var(--badge-line);
border-radius: 999px;
padding: 2px 9px;
white-space: nowrap;
}
.eco-arrow {
margin-left: auto;
}
.eco-arrow svg {
width: 16px;
height: 16px;
fill: none;
stroke: var(--accent);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.6;
transition: transform 0.28s var(--ease-out), opacity 0.2s;
}
.eco-card.linkable:hover .eco-arrow svg {
transform: translate(3px, -3px);
opacity: 1;
}
.eco-role {
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-strong);
opacity: 0.9;
}
.eco-desc {
color: var(--text-dim);
font-size: 0.92rem;
line-height: 1.7;
}
@media (max-width: 760px) {
.eco-grid {
grid-template-columns: 1fr;
}
}
</style>
+176
View File
@@ -0,0 +1,176 @@
<script setup lang="ts">
/** 核心特性:卡片网格(与行式列表、时间轴、代码窗形成形式差异) */
import { computed } from 'vue'
import { t } from '@/i18n'
/** 标题里的 Viper 单独成 span,作为首屏巨字滚动飞行的落点(中英标题都以 Viper 结尾) */
const VIPER_WORD = 'Viper'
const titleParts = computed(() => {
const full = t.features.title
const at = full.indexOf(VIPER_WORD)
return at < 0
? { head: full, hasWord: false }
: { head: full.slice(0, at), hasWord: true }
})
/** 鼠标跟随光晕:在网格上做事件委托,把指针在卡片内的坐标写进该卡片的 CSS 变量 --mx / --my */
function onCardPointerMove(e: PointerEvent) {
const card = (e.target as HTMLElement | null)?.closest<HTMLElement>('.f-card')
if (!card) return
const rect = card.getBoundingClientRect()
card.style.setProperty('--mx', `${e.clientX - rect.left}px`)
card.style.setProperty('--my', `${e.clientY - rect.top}px`)
}
</script>
<template>
<section id="features" class="section features">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.features.eyebrow }}</p>
<h2 class="section-title">{{ titleParts.head }}<span v-if="titleParts.hasWord" class="title-viper" data-viper-target>Viper</span></h2>
<p class="section-sub">{{ t.features.sub }}</p>
</div>
<div class="card-grid" @pointermove="onCardPointerMove">
<article
v-for="(c, i) in t.features.cards"
:key="i"
class="f-card"
:style="{ '--reveal-delay': `${(i % 4) * 70}ms` }"
data-reveal
>
<span class="f-idx mono">{{ String(i + 1).padStart(2, '0') }}</span>
<h3 class="f-term">{{ c.term }}</h3>
<p class="f-desc">{{ c.desc }}</p>
<div class="f-code mono">{{ c.code }}</div>
</article>
</div>
</div>
</section>
</template>
<style scoped>
.features {
/* 顶部留透明区:让首屏柔光透上来继续可见,实现跨 section 的光晕衔接;
其后接浅色带(两端回到 --bg,与相邻 section 无接缝) */
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
var(--bg) 20%,
var(--bg-soft) 42%,
var(--bg) 100%
);
}
.sec-head {
margin-bottom: clamp(36px, 5vw, 54px);
}
/* 首屏巨字的落点槽位:只负责占位与测量,字形由飞过来的巨字呈现。
inline-block 便于取到稳定的盒子;颜色由 --viper-slot-color 控制(脚本启用时置 transparent,
效果关闭时保持正常可读的标题文字)。 */
.title-viper {
display: inline-block;
color: var(--viper-slot-color, inherit);
}
.card-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.f-card {
position: relative;
overflow: hidden; /* 裁掉光晕的直角,贴合圆角卡片 */
display: flex;
flex-direction: column;
gap: 9px;
padding: 22px 20px 20px;
border: 1px solid var(--line-soft);
border-radius: var(--radius);
background: var(--bg);
transition: border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out),
transform 0.25s var(--ease-out);
}
/* 鼠标跟随光晕:位置由 --mx / --my 决定,未移动时落在卡片中心 */
.f-card::before {
content: '';
position: absolute;
inset: 0;
z-index: 0;
background: radial-gradient(
240px circle at var(--mx, 50%) var(--my, 50%),
rgba(var(--accent-rgb), 0.16),
transparent 68%
);
opacity: 0;
transition: opacity 0.28s var(--ease-out);
pointer-events: none;
}
/* 仅在支持悬停的指针设备上启用,触屏不残留光斑 */
@media (hover: hover) and (pointer: fine) {
.f-card:hover::before {
opacity: 1;
}
}
.f-card:hover {
border-color: rgba(var(--accent-rgb), 0.45);
box-shadow: var(--shadow-code);
transform: translateY(-2px);
}
/* 卡片内容压在光晕之上 */
.f-idx,
.f-term,
.f-desc,
.f-code {
position: relative;
z-index: 1;
}
.f-idx {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.1em;
}
.f-term {
font-size: 1.04rem;
font-weight: 650;
letter-spacing: -0.01em;
}
.f-desc {
color: var(--text-dim);
font-size: 0.9rem;
line-height: 1.7;
flex: 1;
}
.f-code {
font-size: 0.76rem;
color: var(--tok-type);
background: var(--bg-deep);
border-radius: var(--radius-sm);
padding: 7px 11px;
overflow-x: auto;
white-space: nowrap;
}
@media (max-width: 1100px) {
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 620px) {
.card-grid {
grid-template-columns: 1fr;
}
}
</style>
+459
View File
@@ -0,0 +1,459 @@
<script setup lang="ts">
/** 首屏:整屏宽巨字 + 错位排版(左巨字 / 右对齐文案),非对称构图
* 滚动联动:巨字里的 Viper 随滚动向下飞行并缩小,最终直接落进「为什么是 Viper」标题的
* Viper 槽位(同一个词的两个位置,不做交叉淡化,文字样式一路保留) */
import { onMounted, onBeforeUnmount, watch, nextTick, ref } from 'vue'
import { t, currentLang } from '@/i18n'
const brandRef = ref<HTMLElement | null>(null)
/** 元素的文档坐标(offsetLeft/offsetTop 逐级累加):不受 transform 与进入动画影响 */
function docOffset(el: HTMLElement) {
let left = 0
let top = 0
let node: HTMLElement | null = el
while (node) {
left += node.offsetLeft
top += node.offsetTop
node = node.offsetParent as HTMLElement | null
}
return { left, top }
}
/** 首个文本节点的排版宽度(Range 只取宽度:不受 translate 影响,measure 前已清掉 scale) */
function firstTextWidth(el: HTMLElement) {
const node = el.firstChild
if (!node || node.nodeType !== Node.TEXT_NODE) return el.getBoundingClientRect().width
const range = document.createRange()
range.selectNodeContents(node)
return range.getBoundingClientRect().width
}
const origin = { left: 0, top: 0 } // 巨字元素文档位置(transform 原点)
const word = { centerX: 0, centerY: 0, width: 0 } // 巨字里的 Viper(中心 + 宽度)
const slot = { centerX: 0, centerY: 0, width: 0 } // 标题里的 Viper 槽位(同上)
let endY = 0 // 飞行终点对应的滚动位置
let enabled = false
let rafId = 0
/** 复位到自然状态(同时用于测量前清场) */
function reset() {
const brand = brandRef.value
if (brand) brand.style.transform = ''
document.documentElement.style.removeProperty('--viper-slot-color')
}
/** 测量巨字与标题槽位;视口过窄、无槽位或用户偏好减少动效时关闭该效果 */
function measure() {
const brand = brandRef.value
const target = document.querySelector<HTMLElement>('[data-viper-target]')
reset()
if (
!brand ||
!target ||
window.innerWidth < 640 ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
) {
enabled = false
return
}
// 巨字里的 Viper:宽度取 Range(只受水平排版影响),中心由 offset + 计算样式推算 ——
// 这样即便外层入场动画仍在跑(只做 translateY),测量结果也完全不受影响
Object.assign(origin, docOffset(brand))
const brandStyle = getComputedStyle(brand)
const brandLineH = parseFloat(brandStyle.lineHeight) || parseFloat(brandStyle.fontSize)
const wordWidth = firstTextWidth(brand)
Object.assign(word, {
width: wordWidth,
centerX: origin.left + parseFloat(brandStyle.paddingLeft) + wordWidth / 2,
centerY: origin.top + parseFloat(brandStyle.paddingTop) + brandLineH / 2,
})
// 标题槽位:位置取 offset 链,尺寸取 rect(translate 不改变尺寸)
const slotOffset = docOffset(target)
const slotRect = target.getBoundingClientRect()
Object.assign(slot, {
width: slotRect.width,
centerX: slotOffset.left + slotRect.width / 2,
centerY: slotOffset.top + slotRect.height / 2,
})
// 终点:标题里的 Viper 升到视口 78% 高度处时飞行刚好结束
endY = slotOffset.top - window.innerHeight * 0.78
enabled = endY > 240 && word.width > 0 && slot.width > 0
if (enabled) {
// 槽位交给飞过来的巨字呈现,标题自身不再显示该词(关闭效果时不设置,标题正常可读)
document.documentElement.style.setProperty('--viper-slot-color', 'transparent')
}
update()
}
function update() {
rafId = 0
const brand = brandRef.value
if (!brand) return
if (!enabled) {
reset()
return
}
const p = Math.min(1, Math.max(0, window.scrollY / endY))
const e = p * p * (3 - 2 * p) // smoothstep:起步与收尾都更柔
// 终态:把巨字里的 Viper 等比缩到槽位宽度,并与其中心重合(transform-origin: 0 0)
const scale = slot.width / word.width
const txFinal = slot.centerX - origin.left - scale * (word.centerX - origin.left)
const tyFinal = slot.centerY - origin.top - scale * (word.centerY - origin.top)
// 按进度插值:词语中心沿直线从原位置走到槽位中心,尺寸同步过渡
const s = 1 + (scale - 1) * e
brand.style.transform = `translate3d(${txFinal * e}px, ${tyFinal * e}px, 0) scale(${s})`
}
function onScroll() {
if (rafId) return
rafId = requestAnimationFrame(update)
}
function onResize() {
if (rafId) return
rafId = requestAnimationFrame(() => {
rafId = 0
measure()
})
}
onMounted(async () => {
await nextTick()
measure()
window.addEventListener('scroll', onScroll, { passive: true })
window.addEventListener('resize', onResize, { passive: true })
// 字体载入后字宽会变,需重测
document.fonts?.ready.then(measure)
})
// 语言切换后标题文案与排版都会变,重新测量
watch(currentLang, async () => {
await nextTick()
measure()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', onScroll)
window.removeEventListener('resize', onResize)
if (rafId) cancelAnimationFrame(rafId)
reset()
})
</script>
<template>
<section id="top" class="hero">
<div class="hero-bg" aria-hidden="true">
<span class="hero-halo"></span>
<span class="hero-halo hero-halo--sky"></span>
</div>
<div class="hero-inner">
<div class="hero-head">
<span class="hero-vert">{{ t.hero.kicker }}</span>
<div class="hero-brand-wrap">
<h1 ref="brandRef" class="hero-brand">Viper<span class="brand-dot">.</span></h1>
</div>
</div>
<div class="hero-lower">
<p class="hero-statement">{{ t.hero.statement }}</p>
<p class="hero-lead">{{ t.hero.lead }}</p>
<div class="hero-cta">
<a class="btn btn-primary" href="#start">
{{ t.hero.ctaStart }}
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6" /></svg>
</a>
<a
class="btn btn-ghost"
href="https://git.gvsds.com/GVSDS/TransPyC"
target="_blank"
rel="noreferrer"
>
{{ t.hero.ctaSource }}
</a>
</div>
</div>
<!-- 横贯数据行:只放稳定的能力断言,不放会随代码变化的数字 -->
<ul class="hero-data">
<li
v-for="(datum, i) in t.hero.dataRows"
:key="i"
class="datum"
:style="{ '--reveal-delay': `${i * 60}ms` }"
data-reveal
>
<span class="datum-dot" :class="`dot-${i}`" aria-hidden="true"></span>
<span class="datum-text">{{ datum }}</span>
</li>
</ul>
</div>
</section>
</template>
<style scoped>
.hero {
position: relative;
min-height: 100svh;
display: flex;
align-items: center;
/* 柔光要越过底边渗入下一 section,所以不能整体裁剪。
这里只做横向裁剪:柔光是 1260px / 860px 的固定宽度,不横向裁会把页面撑得比视口宽
(手机端表现为可以左右拖动、右侧一片空白)。
前置的 overflow: hidden 是不支持 overflow: clip 的浏览器的回退:柔光不外溢,但布局一定安全。 */
overflow: hidden;
padding: calc(var(--nav-h) + 40px) 0 36px;
}
@supports (overflow-x: clip) {
.hero {
overflow-x: clip;
overflow-y: visible;
}
}
/* 背景:两团柔光(主题绿 / 浅蓝),无网格 */
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
.hero-halo {
position: absolute;
top: -30%;
right: -12%;
width: 1260px;
height: 1260px;
border-radius: 50%;
background: radial-gradient(circle, rgba(var(--accent-rgb), 0.16), transparent 62%);
filter: blur(30px);
}
.hero-halo--sky {
top: auto;
bottom: -46%;
left: -18%;
width: 860px;
height: 860px;
background: radial-gradient(circle, rgba(56, 189, 248, 0.11), transparent 64%);
}
/* 首屏单独放宽容器:巨字接近整屏宽(全站 .container 仍为 1140px) */
.hero-inner {
position: relative;
z-index: 1;
width: min(1620px, 100% - 40px);
margin-inline: auto;
}
/* 顶部:竖排标识 + 整屏宽巨字 */
.hero-head {
display: flex;
align-items: flex-start;
gap: 16px;
}
.hero-vert {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 14px;
padding-top: 6px;
writing-mode: vertical-rl;
font-size: 0.72rem;
letter-spacing: 0.42em;
color: var(--text-faint);
text-transform: uppercase;
opacity: 0;
animation: riseIn 0.7s var(--ease-out) 0.05s forwards;
}
.hero-vert::before {
content: '';
width: 2px;
height: 48px;
border-radius: 2px;
background: var(--lime);
}
/* 巨字外层:只负责入场动画(riseIn 会写 transform/opacity,必须与滚动飞行的 transform 分离) */
.hero-brand-wrap {
flex: 1;
min-width: 0;
opacity: 0;
animation: riseIn 0.9s var(--ease-out) 0.12s forwards;
}
/* 品牌字:整屏宽量级
line-height + padding 共同决定内容盒高度 —— 渐变由 background-clip
绘制在内容盒内,内容盒必须高于字形,否则笔画会被吃掉 */
.hero-brand {
margin: 0;
font-family: var(--font-brand);
font-size: clamp(5.5rem, 26vw, 26rem);
line-height: 0.96;
padding: 0.1em 0 0.2em;
letter-spacing: 0.01em;
font-weight: 900;
background: linear-gradient(96deg, var(--text) 26%, var(--accent) 94%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
/* 滚动飞行:以左上角为原点做 translate + scale,便于精确落到目标词的位置 */
transform-origin: 0 0;
will-change: transform;
}
/* 句点单独上色:不依赖父级 background-clip 对后代文本的裁切,颜色与渐变末端的主题色一致 */
.brand-dot {
color: var(--accent);
}
/* 文案块右对齐并与巨字轻微压叠,制造错位构图 */
.hero-lower {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
margin-top: clamp(-34px, -1.8vw, -8px);
}
.hero-statement {
font-size: clamp(1.7rem, 4.1vw, 3.3rem);
line-height: 1.22;
letter-spacing: -0.02em;
font-weight: 700;
color: var(--accent);
opacity: 0;
animation: riseIn 0.7s var(--ease-out) 0.26s forwards;
}
.hero-lead {
max-width: 46ch;
margin-top: 18px;
font-size: 1.04rem;
line-height: 1.8;
color: var(--text-dim);
opacity: 0;
animation: riseIn 0.7s var(--ease-out) 0.32s forwards;
}
.hero-cta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 14px;
margin-top: 26px;
opacity: 0;
animation: riseIn 0.7s var(--ease-out) 0.38s forwards;
}
.hero-cta .btn svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.25s var(--ease-out);
}
.btn-primary:hover svg {
transform: translateX(3px);
}
/* 数据行:横贯整宽、四段等分,圆点按主题四色轮转 */
.hero-data {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: clamp(36px, 5vw, 62px);
border-top: 1px solid var(--line-soft);
list-style: none;
}
.datum {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 20px 0;
border-left: 1px dashed var(--line-soft);
}
.datum:first-child {
border-left: none;
padding-left: 0;
}
.datum-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-0 {
background: var(--accent);
box-shadow: 0 0 10px var(--glow);
}
.dot-1 {
background: var(--lime);
}
.dot-2 {
background: var(--sky);
}
.dot-3 {
background: var(--red);
}
.datum-text {
font-size: 0.88rem;
letter-spacing: 0.02em;
color: var(--text-dim);
}
@media (max-width: 980px) {
.hero {
padding-top: calc(var(--nav-h) + 30px);
}
.hero-vert {
display: none;
}
.hero-lower {
align-items: flex-start;
text-align: left;
margin-top: 20px;
}
.hero-cta {
justify-content: flex-start;
}
.hero-data {
grid-template-columns: repeat(2, minmax(0, 1fr));
row-gap: 14px;
}
.datum {
border-left: none;
padding: 0;
}
}
@media (max-width: 560px) {
.hero-data {
grid-template-columns: 1fr;
}
}
</style>
+135
View File
@@ -0,0 +1,135 @@
<script setup lang="ts">
/** 编译期设施:居中的声明/修饰代码窗 + 下方横排能力胶囊 */
import { t } from '@/i18n'
</script>
<template>
<section id="meta" class="section meta">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.meta.eyebrow }}</p>
<h2 class="section-title">{{ t.meta.title }}</h2>
<p class="section-sub">{{ t.meta.sub }}</p>
</div>
<div class="code-win code-pane" data-reveal>
<div class="code-win__bar">
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__file">{{ t.meta.codeTitle }}</span>
</div>
<pre><code><span
v-for="(line, i) in t.meta.code"
:key="i"
class="cl"
><span class="cl-t">{{ line.t || ' ' }}</span><span v-if="line.c" class="cl-c"># {{ line.c }}</span></span></code></pre>
</div>
<div class="chips-wrap" data-reveal>
<p class="chips-title mono">{{ t.meta.chipsTitle }}</p>
<div class="chips">
<div v-for="(c, i) in t.meta.chips" :key="i" class="chip">
<span class="chip-name">{{ c.name }}</span>
<span class="chip-note mono">{{ c.note }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.meta {
/* 与前后 section 无缝衔接:两端都回到 --bg */
background: linear-gradient(180deg, var(--bg), var(--bg-soft) 18%, var(--bg));
}
.sec-head {
margin-bottom: clamp(36px, 5vw, 54px);
}
.code-pane {
--text: #e6f1ea;
--accent: #4ddb8a;
max-width: 760px;
margin-inline: auto;
box-shadow: var(--shadow-code), 0 0 0 1px rgba(var(--accent-rgb), 0.16);
}
.code-pane code {
display: block;
padding: 18px 24px 22px;
}
.cl {
display: block;
font-size: 0.85rem;
line-height: 1.95;
white-space: pre;
}
.cl-t {
color: var(--tok-plain);
}
.cl-c {
margin-left: 16px;
color: var(--tok-cmt);
font-style: italic;
}
.chips-wrap {
margin-top: clamp(34px, 5vw, 52px);
max-width: 760px;
margin-inline: auto;
}
.chips-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 14px;
}
.chips {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.chip {
display: flex;
flex-direction: column;
gap: 6px;
padding: 13px 16px;
border: 1px solid var(--line-soft);
border-radius: 999px;
background: var(--panel);
transition: border-color 0.25s var(--ease-out), background 0.25s var(--ease-out);
}
.chip:hover {
border-color: rgba(var(--accent-rgb), 0.45);
background: rgba(var(--accent-rgb), 0.04);
}
.chip-name {
font-size: 0.92rem;
font-weight: 650;
}
.chip-note {
font-size: 0.76rem;
color: var(--text-faint);
overflow-x: auto;
white-space: nowrap;
}
@media (max-width: 700px) {
.chips {
grid-template-columns: 1fr;
}
}
</style>
+121
View File
@@ -0,0 +1,121 @@
<script setup lang="ts">
/** 对象模型:左侧代码窗(语法着色 + 行尾注释),右侧要点对照表 */
import { t } from '@/i18n'
</script>
<template>
<section id="oop" class="section oop">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.oop.eyebrow }}</p>
<h2 class="section-title">{{ t.oop.title }}</h2>
<p class="section-sub">{{ t.oop.sub }}</p>
</div>
<div class="split">
<div class="code-win code-pane" data-reveal>
<div class="code-win__bar">
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__file">{{ t.oop.codeTitle }}</span>
</div>
<pre><code><span
v-for="(line, i) in t.oop.code"
:key="i"
class="cl"
><span class="cl-t">{{ line.t || ' ' }}</span><span v-if="line.c" class="cl-c">// {{ line.c }}</span></span></code></pre>
</div>
<div class="notes" data-reveal>
<p class="pane-title mono">{{ t.oop.notesTitle }}</p>
<dl class="note-list">
<div v-for="(n, i) in t.oop.notes" :key="i" class="note-row">
<dt class="note-key mono">{{ n.key }}</dt>
<dd class="note-val">{{ n.value }}</dd>
</div>
</dl>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.sec-head {
margin-bottom: clamp(36px, 5vw, 54px);
}
.split {
display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
gap: clamp(28px, 4vw, 56px);
align-items: start;
}
.code-pane {
--text: #e6f1ea;
--accent: #4ddb8a;
box-shadow: var(--shadow-code), 0 0 0 1px rgba(var(--accent-rgb), 0.16);
}
.code-pane code {
display: block;
padding: 18px 22px 22px;
}
.cl {
display: block;
font-size: 0.85rem;
line-height: 1.95;
white-space: pre;
}
.cl-t {
color: var(--tok-plain);
}
.cl-c {
margin-left: 14px;
color: var(--tok-cmt);
font-style: italic;
}
.pane-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 14px;
}
.note-list {
border-top: 1px solid var(--line-soft);
}
.note-row {
display: grid;
grid-template-columns: minmax(120px, auto) minmax(0, 1fr);
gap: 16px;
align-items: baseline;
padding: 11px 0;
border-bottom: 1px dashed var(--line-soft);
}
.note-key {
color: var(--accent-strong);
font-size: 0.82rem;
white-space: nowrap;
}
.note-val {
color: var(--text-dim);
font-size: 0.9rem;
}
@media (max-width: 980px) {
.split {
grid-template-columns: 1fr;
}
}
</style>
+273
View File
@@ -0,0 +1,273 @@
<script setup lang="ts">
import { computed } from 'vue'
import { t } from '@/i18n'
// 终端命令(编译器本体已自举:TransPyV 原生可执行文件)
const term = computed(() => [
{ kind: 'prompt', html: '<span class="tt-cmd">git clone</span> <span class="tt-arg">https://git.gvsds.com/GVSDS/TransPyC</span>' },
{ kind: 'prompt', html: '<span class="tt-cmd">git clone</span> <span class="tt-arg">https://git.gvsds.com/GVSDS/ViperTemplateProject</span>' },
{ kind: 'gap', html: '' },
{ kind: 'cmt', html: t.quickstart.cmdNote },
{ kind: 'prompt', html: '<span class="tt-cmd">.\\TransPyC\\TransPyV\\output\\TransPyV.exe</span> <span class="tt-arg">--project</span> <span class="tt-arg">.\\ViperTemplateProject\\project.vpj</span>' },
{ kind: 'ok', html: '<span class="tt-ok">✓ build ok</span> <span class="tt-dim">— native app.exe · 86ms</span>' },
{ kind: 'gap', html: '' },
{ kind: 'prompt', html: '<span class="tt-cmd">.\\ViperTemplateProject\\output\\app.exe</span>' },
{ kind: 'out', html: t.quickstart.output },
])
</script>
<template>
<section id="start" class="section quickstart">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.quickstart.eyebrow }}</p>
<h2 class="section-title">{{ t.quickstart.title }}</h2>
<p class="section-tagline">{{ t.quickstart.tagline }}</p>
<p class="section-sub">{{ t.quickstart.sub }}</p>
</div>
<div class="prereqs" data-reveal>
<span v-for="p in t.quickstart.prereqs" :key="p" class="prereq-chip mono">{{ p }}</span>
</div>
<div class="qs-grid">
<ol class="steps">
<li
v-for="(step, i) in t.quickstart.steps"
:key="step.num"
class="step"
:style="{ '--stagger': `${i * 120}ms` }"
data-reveal
>
<span class="step-num mono">{{ step.num }}</span>
<div class="step-body">
<h3 class="step-title">{{ step.title }}</h3>
<p class="step-desc">{{ step.desc }}</p>
</div>
</li>
</ol>
<div class="qs-visual" data-reveal>
<div class="code-win">
<div class="code-win__bar">
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__file">PowerShell</span>
</div>
<pre><code>
<span
v-for="(line, i) in term"
:key="i"
class="tt-line"
:class="`tt-line--${line.kind}`"
v-html="line.html"
></span>
<span class="caret"></span>
</code></pre>
</div>
</div>
</div>
<!-- 常用命令行参数 -->
<div class="cli" data-reveal>
<p class="cli-title mono">{{ t.quickstart.cliTitle }}</p>
<div class="cli-grid">
<div v-for="(c, i) in t.quickstart.cli" :key="i" class="cli-row">
<span class="cli-flag mono">{{ c.flag }}</span>
<span class="cli-desc">{{ c.desc }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.quickstart {
/* 与前后 section 无缝衔接:两端都回到 --bg,浅色带只在中段柔和起伏;
去掉原来的上下边框(那正是"分段"感的来源) */
background: linear-gradient(180deg, var(--bg), var(--bg-soft) 18%, var(--bg));
}
.sec-head {
margin-bottom: 30px;
}
.section-tagline {
display: inline-flex;
align-items: center;
gap: 10px;
margin: -6px 0 14px;
color: var(--accent-strong);
font-size: 1.28rem;
font-weight: 650;
letter-spacing: 0.01em;
}
.section-tagline::before {
content: '❝';
color: var(--accent);
font-size: 1.4rem;
line-height: 1;
opacity: 0.75;
}
.prereqs {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: clamp(40px, 6vw, 64px);
}
.prereq-chip {
font-size: 0.78rem;
color: var(--text-dim);
border: 1px solid var(--line-soft);
border-radius: 999px;
padding: 6px 14px;
background: var(--panel);
}
.qs-grid {
display: grid;
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
align-items: start;
gap: clamp(36px, 5vw, 72px);
}
/* 步骤列表 */
.steps {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.step {
display: grid;
grid-template-columns: 58px 1fr;
gap: 18px;
padding: 22px 0;
border-bottom: 1px solid var(--line-soft);
}
.step-num {
font-size: 1.5rem;
font-weight: 750;
color: var(--accent);
opacity: 0.85;
line-height: 1;
}
.step-title {
font-size: 1.12rem;
font-weight: 650;
margin-bottom: 7px;
}
.step-desc {
color: var(--text-dim);
font-size: 0.95rem;
}
/* 终端 */
.qs-visual .code-win {
--accent: #4ddb8a;
}
.tt-line {
display: block;
}
.tt-line--gap {
height: 6px;
}
.tt-line--cmt {
color: var(--tok-cmt);
font-style: italic;
}
.tt-line--prompt::before {
content: '$ ';
color: var(--accent);
font-weight: 700;
user-select: none;
}
.tt-cmd {
color: var(--tok-plain);
}
.tt-arg {
color: var(--tok-str);
}
.tt-ok {
color: var(--accent);
}
.tt-dim {
color: var(--text-faint);
}
.tt-line--out {
color: var(--tok-str);
}
@media (max-width: 900px) {
.qs-grid {
grid-template-columns: 1fr;
}
.qs-visual {
order: -1;
}
}
/* 常用命令行参数 */
.cli {
margin-top: clamp(40px, 6vw, 64px);
border-top: 1px solid var(--line-soft);
padding-top: 26px;
}
.cli-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 16px;
}
.cli-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 30px;
}
.cli-row {
display: grid;
grid-template-columns: minmax(190px, auto) minmax(0, 1fr);
gap: 12px;
align-items: baseline;
padding: 7px 0;
border-bottom: 1px dashed var(--line-soft);
}
.cli-flag {
color: var(--accent-strong);
font-size: 0.82rem;
white-space: nowrap;
}
.cli-desc {
color: var(--text-dim);
font-size: 0.86rem;
}
@media (max-width: 980px) {
.cli-grid {
grid-template-columns: 1fr;
}
}
</style>
+143
View File
@@ -0,0 +1,143 @@
<script setup lang="ts">
/** 交互式 REPL 版块:左侧能力点,右侧会话示例终端窗 */
import { t } from '@/i18n'
</script>
<template>
<section id="repl" class="section repl">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.repl.eyebrow }}</p>
<h2 class="section-title">{{ t.repl.title }}</h2>
<p class="section-sub">{{ t.repl.sub }}</p>
</div>
<div class="repl-body">
<ul class="repl-points">
<li
v-for="(p, i) in t.repl.points"
:key="i"
class="repl-point"
:style="{ '--reveal-delay': `${i * 70}ms` }"
data-reveal
>
<span class="p-dot" aria-hidden="true"></span>
<div>
<h3 class="p-title">{{ p.title }}</h3>
<p class="p-desc">{{ p.desc }}</p>
</div>
</li>
</ul>
<div class="repl-visual" data-reveal>
<div class="code-win repl-term">
<div class="code-win__bar">
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__dot"></span>
<span class="code-win__file">TransPyV --repl</span>
</div>
<pre><code><span
v-for="(line, i) in t.repl.lines"
:key="i"
class="r-line"
:class="{ 'r-out': line.startsWith('11') }"
>{{ line }}</span></code></pre>
</div>
<p class="repl-caption mono">{{ t.repl.sampleTitle }}</p>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.sec-head {
margin-bottom: clamp(40px, 6vw, 64px);
}
.repl-body {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
gap: clamp(32px, 4vw, 64px);
align-items: start;
}
.repl-points {
display: flex;
flex-direction: column;
gap: 26px;
}
.repl-point {
display: grid;
grid-template-columns: 16px minmax(0, 1fr);
gap: 14px;
opacity: 0;
transform: translateY(14px);
transition: opacity 0.55s ease var(--reveal-delay), transform 0.6s var(--ease-out) var(--reveal-delay);
}
.repl-point.in {
opacity: 1;
transform: none;
}
.p-dot {
margin-top: 7px;
width: 9px;
height: 9px;
border-radius: 50%;
border: 1px solid var(--accent);
background: rgba(var(--accent-rgb), 0.12);
box-shadow: 0 0 10px -2px var(--glow);
}
.p-title {
font-size: 1.06rem;
font-weight: 650;
letter-spacing: -0.012em;
}
.p-desc {
margin-top: 6px;
color: var(--text-dim);
font-size: 0.95rem;
max-width: 58ch;
}
.repl-term {
--text: #e6f1ea;
--accent: #4ddb8a;
box-shadow: var(--shadow-code), 0 0 0 1px rgba(var(--accent-rgb), 0.16);
}
.repl-term code {
display: block;
padding: 16px 22px 20px;
}
.r-line {
display: block;
font-size: 0.86rem;
line-height: 1.85;
color: var(--tok-plain);
}
.r-out {
color: var(--tok-fn);
}
.repl-caption {
margin-top: 14px;
color: var(--text-faint);
font-size: 0.76rem;
letter-spacing: 0.05em;
}
@media (max-width: 980px) {
.repl-body {
grid-template-columns: 1fr;
}
}
</style>
+171
View File
@@ -0,0 +1,171 @@
<script setup lang="ts">
import { computed } from 'vue'
import { t, currentLang, type Lang } from '@/i18n'
const statusText: Record<string, Record<Lang, string>> = {
done: { zh: '已完成', en: 'Done' },
active: { zh: '进行中', en: 'In progress' },
next: { zh: '下一步', en: 'Next' },
}
const phase = (p: string) => statusText[p]?.[currentLang.value] ?? p
const phaseClass = (p: string) => (p === 'active' ? 'phase-active' : p === 'done' ? 'phase-done' : 'phase-next')
</script>
<template>
<section id="roadmap" class="section roadmap">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.roadmap.eyebrow }}</p>
<h2 class="section-title">{{ t.roadmap.title }}</h2>
<p class="section-sub">{{ t.roadmap.sub }}</p>
</div>
<div class="road">
<article
v-for="(item, i) in t.roadmap.items"
:key="i"
class="road-item"
:class="[phaseClass(item.phase)]"
:style="{ '--reveal-delay': `${i * 120}ms` }"
data-reveal
>
<span class="road-dot" aria-hidden="true"></span>
<div class="road-card">
<div class="road-top">
<span class="road-phase mono">{{ phase(item.phase) }}</span>
<span class="road-line-mono mono" aria-hidden="true">──</span>
</div>
<h3 class="road-title">{{ item.title }}</h3>
<p class="road-desc">{{ item.desc }}</p>
</div>
</article>
</div>
</div>
</section>
</template>
<style scoped>
.roadmap {
/* 与前后 section 无缝衔接:两端都回到 --bg */
background: linear-gradient(180deg, var(--bg), var(--bg-soft) 18%, var(--bg));
}
.sec-head {
margin-bottom: clamp(44px, 7vw, 70px);
}
.road {
position: relative;
margin-left: 6px;
}
.road-item {
position: relative;
padding: 0 0 44px 46px;
}
.road-item:last-child {
padding-bottom: 8px;
}
/* 竖向轨道 */
.road-item::before {
content: '';
position: absolute;
top: 6px;
bottom: 0;
left: 7px;
width: 1px;
background: var(--line-soft);
}
.road-item:last-child::before {
display: none;
}
.road-dot {
position: absolute;
top: 6px;
left: 0;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line);
}
.phase-done .road-dot {
background: var(--accent);
border-color: var(--accent);
}
.phase-active .road-dot {
background: var(--accent-strong);
border-color: var(--accent-strong);
box-shadow: 0 0 0 5px rgba(var(--accent-rgb), 0.12), 0 0 18px -2px var(--glow);
}
.phase-next .road-dot {
border-color: var(--text-faint);
}
.road-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.road-phase {
font-size: 0.72rem;
letter-spacing: 0.16em;
text-transform: uppercase;
border-radius: 999px;
padding: 3px 12px;
border: 1px solid var(--line-soft);
color: var(--text-faint);
}
.phase-done .road-phase {
color: var(--accent-strong);
border-color: rgba(var(--accent-rgb), 0.4);
background: rgba(var(--accent-rgb), 0.05);
}
.phase-active .road-phase {
color: var(--accent-strong);
border-color: var(--line);
background: rgba(var(--accent-rgb), 0.08);
}
.road-line-mono {
color: var(--text-faint);
opacity: 0.5;
font-size: 0.7rem;
letter-spacing: 0.4em;
}
.road-title {
font-size: clamp(1.12rem, 1.8vw, 1.42rem);
font-weight: 650;
letter-spacing: -0.01em;
margin-bottom: 8px;
}
.phase-active .road-title {
color: var(--accent);
}
.road-desc {
color: var(--text-dim);
font-size: 0.98rem;
max-width: 68ch;
}
@media (max-width: 600px) {
.road-item {
padding-left: 34px;
}
}
</style>
+143
View File
@@ -0,0 +1,143 @@
<script setup lang="ts">
/** 源码导览版块:规模数据条 + 目录职责表 */
import { t } from '@/i18n'
</script>
<template>
<section id="source" class="section source">
<div class="container">
<div class="sec-head" data-reveal>
<p class="eyebrow mono">{{ t.source.eyebrow }}</p>
<h2 class="section-title">{{ t.source.title }}</h2>
<p class="section-sub">{{ t.source.sub }}</p>
</div>
<div class="stat-strip" data-reveal>
<div v-for="(s, i) in t.source.stats" :key="i" class="stat">
<span class="stat-value mono">{{ s.value }}</span>
<span class="stat-label">{{ s.label }}</span>
</div>
</div>
<div class="tree" data-reveal>
<p class="tree-title mono">{{ t.source.dirsTitle }}</p>
<div class="tree-list">
<div v-for="(d, i) in t.source.dirs" :key="i" class="tree-row">
<span class="tree-mark mono">{{ i === t.source.dirs.length - 1 ? '└─' : '├─' }}</span>
<span class="tree-path mono">{{ d.path }}</span>
<span class="tree-desc">{{ d.desc }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.source {
/* 全站浅色带统一形态:两端回到 --bg,中段 18% 处柔和起伏,保证相邻 section 无接缝 */
background: linear-gradient(180deg, var(--bg), var(--bg-soft) 18%, var(--bg));
}
.sec-head {
margin-bottom: clamp(36px, 5vw, 56px);
}
.stat-strip {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 1px;
background: var(--line-soft);
border: 1px solid var(--line-soft);
border-radius: var(--radius);
overflow: hidden;
}
.stat {
display: flex;
flex-direction: column;
gap: 8px;
padding: 22px 18px;
background: var(--bg);
min-width: 0; /* 允许网格列被压缩,不被内容顶宽 */
}
.stat-value {
/* 强制单行:数值一律不换行,否则各列标签基线会错位;
字号随视口收缩,保证最长的 "Windows x64" 也能在最窄的六分列内放下 */
white-space: nowrap;
font-size: clamp(1rem, 1.5vw, 1.4rem);
font-weight: 700;
color: var(--accent-strong);
letter-spacing: -0.02em;
}
.stat-label {
color: var(--text-faint);
font-size: 0.8rem;
line-height: 1.5;
}
.tree {
margin-top: clamp(40px, 6vw, 64px);
}
.tree-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 16px;
}
.tree-list {
border-left: 1px dashed var(--line-soft);
padding-left: 18px;
}
.tree-row {
display: grid;
grid-template-columns: 20px minmax(170px, auto) minmax(0, 1fr);
gap: 12px;
align-items: baseline;
padding: 9px 0;
}
.tree-mark {
color: var(--text-faint);
font-size: 0.8rem;
}
.tree-path {
color: var(--text);
font-size: 0.84rem;
font-weight: 600;
white-space: nowrap;
}
.tree-desc {
color: var(--text-dim);
font-size: 0.9rem;
}
@media (max-width: 1100px) {
.stat-strip {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 700px) {
.stat-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tree-row {
grid-template-columns: 20px minmax(0, 1fr);
gap: 4px 10px;
}
.tree-desc {
grid-column: 2;
}
}
</style>
+238
View File
@@ -0,0 +1,238 @@
<script setup lang="ts">
import { t } from '@/i18n'
const year = new Date().getFullYear()
const navAnchors: { id: string; key: keyof typeof t.nav }[] = [
{ id: 'features', key: 'features' },
{ id: 'compiler', key: 'compiler' },
{ id: 'oop', key: 'oop' },
{ id: 'meta', key: 'meta' },
{ id: 'repl', key: 'repl' },
{ id: 'source', key: 'source' },
{ id: 'start', key: 'quickstart' },
{ id: 'ecosystem', key: 'ecosystem' },
{ id: 'roadmap', key: 'roadmap' },
]
function scrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>
<template>
<footer class="footer">
<!-- 终局 CTA -->
<div class="cta-band">
<div class="container cta-inner" data-reveal>
<h2 class="cta-title">{{ t.cta.title }}</h2>
<p class="cta-sub">{{ t.cta.sub }}</p>
<div class="cta-actions">
<a class="btn btn-primary" href="#start">
{{ t.cta.start }}
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6" /></svg>
</a>
<a
class="btn btn-ghost"
href="https://git.gvsds.com/GVSDS/TransPyC"
target="_blank"
rel="noreferrer"
>
{{ t.cta.source }}
</a>
</div>
</div>
</div>
<!-- 底部 -->
<div class="footer-base">
<div class="container footer-grid">
<div class="f-brand">
<div class="f-brand-line">
<svg viewBox="0 0 64 64" aria-hidden="true">
<path d="M18 46 V18 h7 l9 15 9-15 h7 V46 H42 V31 L32 46 22 31 v15 Z" />
</svg>
<span class="f-name">Viper</span>
</div>
<p class="f-tagline">{{ t.footer.tagline }}</p>
<p class="f-meta mono">{{ t.footer.builtBy }} · © {{ year }}</p>
</div>
<nav class="f-col" aria-label="site">
<span class="f-col-title mono">{{ t.footer.linksTitle }}</span>
<a v-for="n in navAnchors" :key="n.id" class="f-link" :href="`#${n.id}`">
{{ t.nav[n.key] }}
</a>
<a
class="f-link f-link-ext"
href="https://venom.gvsds.com"
target="_blank"
rel="noreferrer"
>
Venom ↗
</a>
</nav>
<button class="back-top mono" @click="scrollTop">
↑ {{ t.footer.backTop }}
</button>
</div>
</div>
</footer>
</template>
<style scoped>
/* CTA 带:与上方 section 无缝衔接(不加分隔线),仅以柔光收束页面 */
.cta-band {
position: relative;
overflow: hidden;
background: radial-gradient(ellipse 70% 120% at 50% 0%, rgba(var(--accent-rgb), 0.07), transparent 70%);
}
.cta-inner {
padding: clamp(5rem, 10vw, 8rem) 0;
text-align: center;
}
.cta-title {
font-size: clamp(1.9rem, 4.4vw, 3.2rem);
font-weight: 700;
letter-spacing: -0.025em;
line-height: 1.12;
}
.cta-sub {
margin: 18px auto 0;
color: var(--text-dim);
max-width: 46ch;
font-size: 1.02rem;
}
.cta-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 34px;
}
.cta-actions .btn svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.25s var(--ease-out);
}
.btn-primary:hover svg {
transform: translateX(3px);
}
/* 底栏(深绿反色,收住浅色页) */
.footer-base {
--text: #e7f2ec;
--text-dim: #9db9ab;
--text-faint: #6f8d7e;
--line-soft: rgba(255, 255, 255, 0.09);
--accent: #4ddb8a;
border-top: 1px solid rgba(255, 255, 255, 0.08);
background: var(--bg-deep);
}
.footer-grid {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: start;
gap: 48px;
padding: 52px 0 40px;
}
.f-brand-line {
display: flex;
align-items: center;
gap: 10px;
}
.f-brand-line svg {
width: 24px;
height: 24px;
fill: var(--accent);
}
.f-name {
font-size: 1.2rem;
font-weight: 750;
}
.f-tagline {
margin-top: 12px;
color: var(--text-dim);
font-size: 0.98rem;
}
.f-meta {
margin-top: 8px;
color: var(--text-faint);
font-size: 0.76rem;
letter-spacing: 0.05em;
}
.f-col {
display: flex;
flex-direction: column;
gap: 10px;
}
.f-col-title {
color: var(--text-faint);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 6px;
}
.f-link {
color: var(--text-dim);
font-size: 0.95rem;
transition: color 0.2s var(--ease-out);
}
.f-link:hover {
color: var(--accent);
}
.f-link-ext {
color: var(--accent);
opacity: 0.85;
}
.back-top {
justify-self: end;
color: var(--text-faint);
font-size: 0.8rem;
letter-spacing: 0.06em;
border: 1px solid var(--line-soft);
border-radius: 999px;
padding: 9px 18px;
transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.back-top:hover {
color: var(--accent);
border-color: var(--line);
background: rgba(var(--accent-rgb), 0.14);
}
@media (max-width: 860px) {
.footer-grid {
grid-template-columns: 1fr;
gap: 36px;
}
.back-top {
justify-self: start;
}
}
</style>
+351
View File
@@ -0,0 +1,351 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { t, currentLang, setLang, type Lang } from '@/i18n'
const scrolled = ref(false)
const menuOpen = ref(false)
/** 主题:属性驱动的暗黑模式(具体色值覆盖见 src/assets/base.css) */
type Theme = 'light' | 'dark'
const THEME_KEY = 'viper-theme'
const theme = ref<Theme>('light')
const systemDark = window.matchMedia('(prefers-color-scheme: dark)')
/** 写入主题:属性驱动 base.css 的变量覆盖,同时同步移动端浏览器 UI 底色 */
function applyTheme(next: Theme) {
theme.value = next
document.documentElement.setAttribute('data-theme', next)
document
.querySelector('meta[name="theme-color"]')
?.setAttribute('content', next === 'dark' ? '#0c1512' : '#ffffff')
}
function toggleTheme() {
const next: Theme = theme.value === 'dark' ? 'light' : 'dark'
applyTheme(next)
try {
// 记住手动选择;写入失败(隐私模式等)只影响下次访问,不影响本次
localStorage.setItem(THEME_KEY, next)
} catch {
/* 忽略写入失败 */
}
}
/** 用户没手动选择过时跟随系统切换 */
function onSystemThemeChange(e: MediaQueryListEvent) {
try {
if (localStorage.getItem(THEME_KEY)) return
} catch {
/* 读取失败按"未选择"处理 */
}
applyTheme(e.matches ? 'dark' : 'light')
}
function onScroll() {
scrolled.value = window.scrollY > 10
}
function toggleLang() {
setLang(currentLang.value === 'zh' ? 'en' : 'zh')
}
function toggleMenu() {
menuOpen.value = !menuOpen.value
}
function closeMenu() {
menuOpen.value = false
}
const links: { id: string; key: keyof typeof t.nav }[] = [
{ id: 'features', key: 'features' },
{ id: 'compiler', key: 'compiler' },
{ id: 'oop', key: 'oop' },
{ id: 'meta', key: 'meta' },
{ id: 'repl', key: 'repl' },
{ id: 'source', key: 'source' },
{ id: 'start', key: 'quickstart' },
]
onMounted(() => {
window.addEventListener('scroll', onScroll, { passive: true })
onScroll()
// index.html 的内联脚本已在首屏前写好 data-theme,这里只把组件状态同步回来
theme.value = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'
systemDark.addEventListener('change', onSystemThemeChange)
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', onScroll)
systemDark.removeEventListener('change', onSystemThemeChange)
})
</script>
<template>
<header class="site-header" :class="{ scrolled }">
<div class="container header-inner">
<a class="brand" href="#top" @click="closeMenu">
<svg viewBox="0 0 64 64" aria-hidden="true">
<path d="M18 46 V18 h7 l9 15 9-15 h7 V46 H42 V31 L32 46 22 31 v15 Z" />
</svg>
<span class="brand-name">Viper</span>
<span class="brand-sub mono">lang</span>
</a>
<nav class="nav-links" :class="{ open: menuOpen }">
<a
v-for="link in links"
:key="link.id"
:href="`#${link.id}`"
class="nav-link"
@click="closeMenu"
>
{{ t.nav[link.key] }}
</a>
<a
class="nav-link nav-source"
href="https://git.gvsds.com/GVSDS/TransPyC"
target="_blank"
rel="noreferrer"
>
{{ t.nav.repo }}
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8" /></svg>
</a>
</nav>
<div class="header-actions">
<button
class="theme-btn"
type="button"
:title="theme === 'dark' ? '切换到浅色模式' : '切换到暗黑模式'"
:aria-label="theme === 'dark' ? '切换到浅色模式' : '切换到暗黑模式'"
@click="toggleTheme"
>
<!-- 图标示意"将切换到的目标主题":当前是暗色 → 显示太阳 -->
<svg v-if="theme === 'dark'" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="4.3" />
<path
d="M12 2.2v2.3M12 19.5v2.3M2.2 12h2.3M19.5 12h2.3M5 5l1.6 1.6M17.4 17.4 19 19M19 5l-1.6 1.6M6.6 17.4 5 19"
/>
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.7 13.6A8.4 8.4 0 1 1 10.4 3.3a6.7 6.7 0 0 0 10.3 10.3z" />
</svg>
</button>
<button class="lang-btn mono" @click="toggleLang" aria-label="Switch language">
{{ currentLang === 'zh' ? 'EN' : '中' }}
</button>
<button
class="burger"
:class="{ open: menuOpen }"
@click="toggleMenu"
aria-label="Menu"
>
<span></span><span></span>
</button>
</div>
</div>
</header>
</template>
<style scoped>
.site-header {
position: fixed;
inset: 0 0 auto;
z-index: 60;
height: var(--nav-h);
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
transition: background 0.35s var(--ease-out), border-color 0.35s var(--ease-out),
backdrop-filter 0.35s var(--ease-out);
}
.site-header.scrolled {
background: var(--glass);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom-color: var(--line-soft);
}
.header-inner {
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.brand svg {
width: 26px;
height: 26px;
fill: var(--accent);
}
.brand-name {
font-weight: 750;
font-size: 1.22rem;
letter-spacing: 0.02em;
}
.brand-sub {
color: var(--accent);
font-size: 0.68rem;
letter-spacing: 0.2em;
transform: translateY(1px);
}
.nav-links {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.nav-link {
position: relative;
padding: 8px 13px;
font-size: 0.94rem;
color: var(--text-dim);
border-radius: 6px;
transition: color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.nav-link:hover {
color: var(--text);
background: var(--hover);
}
.nav-source {
display: inline-flex;
align-items: center;
gap: 5px;
}
.nav-source svg {
width: 13px;
height: 13px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.lang-btn,
.theme-btn {
border: 1px solid var(--line-soft);
color: var(--text-dim);
height: 34px;
border-radius: 8px;
transition: border-color 0.2s, color 0.2s, background 0.2s;
}
.lang-btn {
width: 42px;
font-size: 0.82rem;
letter-spacing: 0.08em;
}
.theme-btn {
width: 38px;
display: inline-grid;
place-items: center;
}
.theme-btn svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
}
.lang-btn:hover,
.theme-btn:hover {
border-color: var(--line);
color: var(--accent-strong);
background: rgba(var(--accent-rgb), 0.06);
}
.burger {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 38px;
height: 34px;
border: 1px solid var(--line-soft);
border-radius: 8px;
padding: 0 9px;
}
.burger span {
height: 2px;
background: var(--text);
border-radius: 2px;
transition: transform 0.25s var(--ease-out), opacity 0.2s;
}
.burger.open span:nth-child(1) {
transform: translateY(3.5px) rotate(45deg);
}
.burger.open span:nth-child(2) {
transform: translateY(-3.5px) rotate(-45deg);
}
@media (max-width: 1180px) {
.nav-link {
padding: 8px 9px;
font-size: 0.9rem;
}
}
@media (max-width: 860px) {
.burger {
display: flex;
}
.nav-links {
position: fixed;
top: var(--nav-h);
left: 0;
right: 0;
margin: 0;
flex-direction: column;
align-items: stretch;
gap: 2px;
background: var(--glass-strong);
backdrop-filter: blur(18px);
border-bottom: 1px solid var(--line-soft);
padding: 10px 20px 18px;
transform: translateY(-14px);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s var(--ease-out), transform 0.25s var(--ease-out);
}
.nav-links.open {
opacity: 1;
transform: none;
pointer-events: auto;
}
.nav-link {
padding: 13px 6px;
font-size: 1.05rem;
}
}
</style>
+339
View File
@@ -0,0 +1,339 @@
// Viper official website · English dictionary
// Structure must mirror zh.ts (validated as typeof zh)
// Wording rule: all code samples use current syntax only (define / novirtual / const int* …), no legacy t.* forms.
import type zh from './zh'
const en: typeof zh = {
nav: {
features: 'Features',
compiler: 'Compiler',
oop: 'Object Model',
meta: 'Compile-time',
repl: 'REPL',
source: 'Source Tour',
quickstart: 'Get Started',
ecosystem: 'Ecosystem',
roadmap: 'Roadmap',
repo: 'Repository',
},
hero: {
kicker: 'A SYSTEM-LEVEL LANGUAGE',
statement: 'Low-level, yet simpler?',
lead: 'A system-level language written in the most readable syntax humans agree on — Python — compiling through LLVM IR down to native machine code. No interpreter, no GC, no runtime. The compiler itself is written in Viper.',
ctaStart: 'Get Started',
ctaSource: 'Source Code',
dataRows: ['Self-hosted', 'No runtime · no GC', 'Annotations are directives', 'Native machine code'],
},
features: {
eyebrow: 'CORE DESIGN',
title: 'Why Viper',
sub: 'Not another Python implementation, not C sugar.',
cards: [
{
term: 'Fully self-hosted',
code: 'App/**.vp → TransPyV.exe',
desc: 'The compiler is written in Viper: 76 files, 64,656 lines, compiling itself into a native executable.',
},
{
term: 'Python syntax, LLVM semantics',
code: 'x: int = 42',
desc: 'Source stays valid Python, while annotations drive code generation — int is i32.',
},
{
term: 'Annotations are directives',
code: 'ptr: const int*',
desc: 'const and * encode const-ness and pointer levels in the type; the rest is inferred.',
},
{
term: 'Content-addressed modules',
code: '@"<sha1>.func"',
desc: 'A module is identified by the SHA1 of its own source, so symbols carry a prefix and cross-file collisions cannot happen.',
},
{
term: 'Incremental builds',
code: '# hit → skip',
desc: 'Four cache checks — signature, declaration, implementation, standard library — so only affected modules are retranslated.',
},
{
term: 'Zero runtime',
code: '# malloc · no GC',
desc: 'The output is native machine code: no GC, no bytecode interpreter, memory and lifetimes are yours.',
},
{
term: 'Async and coroutines',
code: 's = s + await leaf(i)',
desc: 'async/await compile to a resumable state machine with suspension, exception propagation and destruction.',
},
{
term: 'Matching and closures',
code: 'match ty:',
desc: 'match supports nesting and class-variant dispatch; closures capture on demand, nonlocal and lambda alike.',
},
],
},
compiler: {
eyebrow: 'HOW IT COMPILES',
title: 'Two-phase compilation',
sub: 'Declarations first, translation after; modules are named by content hash and unchanged work is skipped.',
steps: [
{ tag: 'src/*.vp', desc: 'Imports discover reachable modules recursively — only referenced files are translated.' },
{ tag: 'Content SHA1', desc: 'The first 8 bytes of the content hash identify the module and its cache shard.' },
{ tag: 'Phase 1 · Declare', desc: 'Emits vpi signatures, stub declarations, text implementations and dependency lists.' },
{ tag: 'Skip check', desc: 'Any of four cache hits skips the work; only affected modules are retranslated.' },
{ tag: 'Phase 2 · Merge', desc: 'Merges declarations and implementations in topological order, deduping symbols and hoisting types.' },
{ tag: 'llc · -O0', desc: 'The complete IR is compiled to object files, in parallel.' },
{ tag: 'clang++ · lld', desc: 'Links a native executable.' },
],
cache: {
title: 'Build switches',
items: [
{ flag: '--phase 1|2|all', desc: 'Declare only / translate only / whole pipeline' },
{ flag: '--clean', desc: 'Drop caches and artifacts' },
{ flag: '--clear-cache', desc: 'Module caches only' },
{ flag: '--rebuild-includes', desc: 'Rebuild standard-library artifacts' },
{ flag: '--repl', desc: 'Enter the REPL' },
{ flag: '--asan', desc: 'ASan-instrumented build' },
{ flag: '--phase2command', desc: 'Export the phase-two command' },
],
},
},
oop: {
eyebrow: 'OBJECT MODEL',
title: 'Structs, with polymorphism on demand',
sub: 'No implicit boxing, no hidden allocation — a class is a struct layout, and the vtable is a pointer you can replace.',
codeTitle: 'Inheritance and dispatch',
code: [
{ t: 'virtual class Shape:' },
{ t: ' sides: int' },
{ t: ' def area(self) -> int:' },
{ t: ' return 0', c: 'default implementation' },
{ t: '' },
{ t: 'novirtual class Square(Shape):' },
{ t: ' size: int' },
{ t: ' def area(self) -> int:' },
{ t: ' return self.size * self.size', c: 'statically bound override, no vtable cost' },
],
notesTitle: 'Key points',
notes: [
{ key: 'vtable offset', value: 'First field of the object: copy 8 bytes to swap the dispatch table' },
{ key: 'Slot order', value: 'Parents before children, up to 32 slots, stable across modules' },
{ key: 'novirtual', value: 'Static binding plus this-adjustment — zero vtable cost, mixable with virtual' },
{ key: 'Multiple parents', value: 'Fields flattened, MRO flattened up to 64 layers' },
{ key: 'Layout control', value: 'union shares memory, packed drops padding, aligned(N) forces alignment' },
{ key: 'Operator overload', value: 'The full dunder set: __add__ / __eq__ / __lshift__ …' },
{ key: 'Construction', value: '__init__ / __new__ / __before_init__ / __sizeof__' },
],
},
meta: {
eyebrow: 'COMPILE-TIME',
title: 'Compile-time facilities',
sub: 'Macros, conditional compilation, inline IR and assembly, function attributes — language syntax, not a preprocessor.',
codeTitle: 'Declarations and modifiers',
code: [
{ t: 'define MAX_SIZE = 4096', c: 'inlined at use sites, no global symbol' },
{ t: '' },
{ t: 'export def Init() -> int:', c: 'frees the symbol from the hash prefix for other modules or C' },
{ t: ' return 0' },
{ t: '' },
{ t: 'inline def Fast(x: int) -> int:', c: 'inlining hint' },
{ t: ' return x * 2' },
{ t: '' },
{ t: 'extern def LibCall(x: int) -> int:', c: 'declaration only' },
{ t: ' pass' },
],
chipsTitle: 'Go deeper',
chips: [
{ name: 'Inline LLVM IR', note: 'c.LLVMIR("add i32 …", int)' },
{ name: 'Inline assembly', note: 'c.Asm("nop") · register and memory constraints' },
{ name: 'Function attributes', note: 'always_inline · nounwind · packed' },
{ name: 'Structured conditionals', note: 'syntax instead of #ifdef' },
],
},
repl: {
eyebrow: 'REPL',
title: 'Interactive REPL',
sub: 'Type line by line: each fragment compiles to a shared library and runs at once — variables, functions and types accumulate.',
points: [
{
title: 'A fragment is a module',
desc: 'Each input is wrapped into a fragment function, translated to module IR, compiled, loaded, called and echoed back.',
},
{
title: 'Variables across fragments',
desc: 'After x: int = 5 you can use x + 1 directly; session variables live in slots.',
},
{
title: 'Functions across fragments',
desc: 'A top-level def in a block becomes an exported function (limits: single-line signature, no async def yet).',
},
{
title: 'Types across fragments',
desc: 'A class defined in a block enters the session and can be constructed later.',
},
{
title: 'Blocks and error recovery',
desc: 'A trailing colon continues the line, an empty line runs the block; errors recover without killing the session.',
},
],
sampleTitle: 'Session sample',
lines: [
'>>> x: int = 5',
'>>> def twice(n: int) -> int:',
'... return n * 2',
'>>> twice(x) + 1',
'11',
],
},
source: {
eyebrow: 'SOURCE TOUR',
title: 'Source tour',
sub: 'The compiler lives in the repository — you can read it module by module.',
stats: [
{ value: '76', label: 'compiler sources (.vp)' },
{ value: '64,656', label: 'lines in the compiler' },
{ value: '52', label: 'translator core files · 49,978 lines' },
{ value: '39', label: 'self-tests' },
{ value: '22.1.8', label: 'LLVM version' },
{ value: 'Windows x64', label: 'current target' },
],
dirsTitle: 'Directory map',
dirs: [
{ path: 'App/main.vp', desc: 'CLI entry: arguments, phase scheduling, build orchestration' },
{ path: 'App/lib/Phase/', desc: 'Both phases and the build pipeline: scanning, topology, artifacts, parallel compile' },
{ path: 'App/lib/Stub/', desc: 'Declaration generation and merging: signatures, topological merge, dedup, type hoisting' },
{ path: 'App/lib/core/Gen/', desc: 'Translator and code generation core: types, expressions, statements, classes, calls' },
{ path: 'App/lib/Projectrans/', desc: 'Project config, path resolution, hash sharding and cache policy' },
{ path: 'Test/', desc: 'Self-test suite: syntax, pointers, OOP, async, negative cases, hash namespaces' },
],
},
quickstart: {
eyebrow: 'GET STARTED',
title: 'Hello Viper in three minutes',
tagline: 'Hi, Viper — see you in three minutes.',
sub: 'The compiler is fully self-hosted — TransPyV is a native executable. Grab a Windows (mingw) + LLVM toolchain and you are ready.',
steps: [
{
num: '01',
title: 'Clone compiler & template',
desc: 'TransPyC is the compiler repository — its self-hosted TransPyV already builds projects; ViperTemplateProject is a ready-to-run Windows template.',
},
{
num: '02',
title: 'Build the project',
desc: 'Drive the two-phase build with the native TransPyV; the executable lands in the template output/.',
},
{
num: '03',
title: 'Run the native binary',
desc: 'You get a real native app.exe — it prints "Hello, world".',
},
],
prereqs: ['TransPyV (native)', 'LLVM 22.1.8 · llc', 'mingw · clang++ / lld', 'git'],
cmdNote: '# Windows PowerShell · mingw + LLVM on PATH',
output: '你好,世界',
cliTitle: 'Common flags',
cli: [
{ flag: '--project <file.vpj>', desc: 'Build from a project file' },
{ flag: '--phase 1|2|all', desc: 'Declare only / translate only / whole pipeline' },
{ flag: '--repl', desc: 'Enter the interactive REPL' },
{ flag: '--clean', desc: 'Drop caches and artifacts — a clean rebuild' },
{ flag: '--asan', desc: 'ASan-instrumented build to hunt memory bugs' },
{ flag: '--phase2command', desc: 'Export the phase-two build command' },
],
},
ecosystem: {
eyebrow: 'ECOSYSTEM',
title: 'From compiler to operating system',
sub: 'Viper is no island: a complete self-built toolchain and systems practice surrounds it.',
items: [
{
name: 'TransPyV',
role: 'Compiler',
desc: 'The self-hosted compiler: two-phase declaration extraction and LLVM IR translation, written in Viper.',
href: 'https://git.gvsds.com/GVSDS/TransPyC',
badge: 'Self-hosted',
},
{
name: 'includes/',
role: 'Standard library',
desc: 'Pure Viper: self-built parser, IR builder, coroutine runtime, buddy allocator, threads and Win32 bindings.',
href: '',
badge: '',
},
{
name: 'testcheck',
role: 'Test framework',
desc: 'Compile-time assertions and grouping — the entry point of all 39 self-tests.',
href: '',
badge: '',
},
{
name: 'ViperTemplateProject',
role: 'Template project',
desc: 'A ready-to-run Windows project skeleton: project file, build script and sample sources.',
href: '',
badge: '',
},
{
name: 'Venom',
role: 'Package manager',
desc: 'The official package manager: distribution, installation and dependency resolution.',
href: 'https://venom.gvsds.com',
badge: 'WIP',
},
{
name: 'ViperOS',
role: 'Operating system',
desc: 'An x86_64 kernel practice written in Viper, pushing the language to the bare metal.',
href: '',
badge: '',
},
],
},
roadmap: {
eyebrow: 'ROADMAP',
title: 'Bootstrapped — ecosystem ahead',
sub: 'The compiler is fully self-hosted and free from the Python runtime. Next: make library sharing first-class.',
items: [
{
phase: 'done',
title: 'Full bootstrap',
desc: 'The compiler is rewritten in Viper: 76 sources, 64,656 lines, compiling itself into a native TransPyV.exe; a self-built parser and IR library replaced third-party dependencies.',
},
{
phase: 'done',
title: 'Interactive REPL',
desc: 'Session variables, cross-fragment functions and types: every fragment compiles to a shared library and runs at once, with recoverable errors.',
},
{
phase: 'active',
title: 'Generic specialisation',
desc: 'Template registration and on-demand specialisation are in place; the self-test suite still masks it to isolate a known crash — generic containers come once fixed.',
},
{
phase: 'next',
title: 'Venom packages',
desc: 'The official package manager ships — publishing, installing and resolving Viper libraries become first-class.',
},
{
phase: 'next',
title: 'Cross-platform targets',
desc: 'Windows x64 only today (sources bind Win32 directly); Unix-like targets and toolchain adaptation come next.',
},
],
},
cta: {
title: 'Ready to write your first native line?',
sub: 'Start from the template project and see your first Viper program run within minutes.',
start: 'Get Started',
source: 'Source Code',
},
footer: {
tagline: 'Low-level, yet simpler?',
builtBy: 'Viper Website · GVSDS',
linksTitle: 'Navigation',
backTop: 'Back to top',
},
}
export default en
+44
View File
@@ -0,0 +1,44 @@
// 轻量 i18n:zh/en 双字典 + 响应式替换(无外部依赖)
import { reactive, computed } from 'vue'
import zh from './zh'
import en from './en'
export type Lang = 'zh' | 'en'
function deepCopyInto(target: Record<string, unknown>, source: Record<string, unknown>): void {
for (const key of Object.keys(source)) {
const srcVal = source[key]
const dstVal = target[key]
if (
srcVal !== null &&
typeof srcVal === 'object' &&
!Array.isArray(srcVal) &&
dstVal !== null &&
typeof dstVal === 'object'
) {
deepCopyInto(dstVal as Record<string, unknown>, srcVal as Record<string, unknown>)
} else {
target[key] = srcVal
}
}
}
const state = reactive({
lang: (localStorage.getItem('viper-lang') as Lang) || 'zh',
})
// 响应式文案对象:结构 = typeof zh(en.ts 已按同结构校验)
export const t = reactive<typeof zh>(deepClone(zh))
function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T
}
export const currentLang = computed(() => state.lang)
export function setLang(lang: Lang): void {
if (state.lang === lang) return
state.lang = lang
localStorage.setItem('viper-lang', lang)
deepCopyInto(t as unknown as Record<string, unknown>, lang === 'zh' ? zh : en)
}
+335
View File
@@ -0,0 +1,335 @@
// Viper 官网 · 中文文案字典
// 注意:结构与 en.ts 完全一致(en.ts 以 typeof zh 校验)
// 文案口径:全站代码示例只使用现行语法(define / novirtual / const int* 等),不展示历史 t.* 写法
export default {
nav: {
features: '特性',
compiler: '编译模型',
oop: '对象模型',
meta: '编译期',
repl: 'REPL',
source: '源码导览',
quickstart: '快速上手',
ecosystem: '生态',
roadmap: '路线图',
repo: '仓库',
},
hero: {
kicker: 'A SYSTEM-LEVEL LANGUAGE',
statement: '底层,再简单一点吧?',
lead: '一门系统级编程语言:以 Python 语法书写,编译为 LLVM IR,直达原生机器码。没有解释器,没有 GC,没有运行时——编译器本身也由 Viper 写成。',
ctaStart: '快速上手',
ctaSource: '查看源码',
dataRows: ['完全自举', '零运行时 · 无 GC', '注解即编译指令', '原生机器码'],
},
features: {
eyebrow: 'CORE DESIGN',
title: '为什么是 Viper',
sub: '不是 Python 的又一个实现,也不是 C 的语法糖。',
cards: [
{
term: '完全自举',
code: 'App/**.vp → TransPyV.exe',
desc: '编译器由 Viper 写成,76 个文件、64,656 行,能编译自身并产出原生可执行文件。',
},
{
term: 'Python 语法,LLVM 语义',
code: 'x: int = 42',
desc: '源码是合法的 Python 语法,而注解直接决定代码生成——int 就是 i32。',
},
{
term: '注解即编译指令',
code: 'ptr: const int*',
desc: 'const 与 * 把常量性、指针层级编码进类型,无注解处由编译器推导。',
},
{
term: '内容寻址的模块',
code: '@"<sha1>.func"',
desc: '模块标识取自源文件内容的 SHA1,符号自动带前缀,跨文件重名不可能冲突。',
},
{
term: '增量编译',
code: '# 命中即跳过',
desc: '签名、声明、实现与标准库产物四处缓存判定,改一个文件只重译受影响模块。',
},
{
term: '零运行时',
code: '# malloc · no GC',
desc: '产物是原生机器码,没有 GC 与字节码解释器,内存与生命周期自负。',
},
{
term: '异步与协程',
code: 's = s + await leaf(i)',
desc: 'async/await 编译为可恢复状态机,含挂起、异常传播与销毁。',
},
{
term: '匹配与闭包',
code: 'match ty:',
desc: 'match 支持嵌套与类变体分派;闭包按需捕获,nonlocal 与 lambda 同等对待。',
},
],
},
compiler: {
eyebrow: 'HOW IT COMPILES',
title: '两阶段编译模型',
sub: '声明先行,翻译在后;模块以内容哈希命名,未变动的部分直接跳过。',
steps: [
{ tag: 'src/*.vp', desc: '按 import 递归发现可达模块,只翻译真正被引用的文件。' },
{ tag: '内容 SHA1', desc: '内容哈希前 8 字节即模块标识,决定缓存目录。' },
{ tag: '阶段一 · 声明', desc: '产出 vpi 签名、stub 声明、text 实现与依赖清单。' },
{ tag: '增量判定', desc: '四处缓存命中即跳过,只重译受影响模块。' },
{ tag: '阶段二 · 合并', desc: '按拓扑序合并声明与实现,去重符号、前移类型。' },
{ tag: 'llc · -O0', desc: '完整 IR 编译为目标文件,多模块并行。' },
{ tag: 'clang++ · lld', desc: '链接为原生可执行文件。' },
],
cache: {
title: '构建开关',
items: [
{ flag: '--phase 1|2|all', desc: '单跑声明 / 单跑翻译 / 全流程' },
{ flag: '--clean', desc: '清理缓存与产物' },
{ flag: '--clear-cache', desc: '只清模块缓存' },
{ flag: '--rebuild-includes', desc: '只重建标准库产物' },
{ flag: '--repl', desc: '进入交互式 REPL' },
{ flag: '--asan', desc: 'ASan 插桩构建' },
{ flag: '--phase2command', desc: '导出第二阶段命令' },
],
},
},
oop: {
eyebrow: 'OBJECT MODEL',
title: '结构体之上,多态可选',
sub: '没有隐式装箱,没有隐藏分配——class 就是结构体布局,虚表是一根可以替换的指针。',
codeTitle: '继承与分派',
code: [
{ t: 'virtual class Shape:' },
{ t: ' sides: int' },
{ t: ' def area(self) -> int:' },
{ t: ' return 0', c: '基类默认实现' },
{ t: '' },
{ t: 'novirtual class Square(Shape):' },
{ t: ' size: int' },
{ t: ' def area(self) -> int:' },
{ t: ' return self.size * self.size', c: '静态绑定覆盖,无虚表开销' },
],
notesTitle: '要点',
notes: [
{ key: 'vtable 位置', value: '对象首字段:偏移 0,复制 8 字节即可替换派发表' },
{ key: '槽位顺序', value: '父类在前、子类在后,上限 32,跨模块不错位' },
{ key: 'novirtual', value: '静态绑定 + this 调整,零虚表开销,可与 virtual 混用' },
{ key: '多继承', value: '字段展平,MRO 展平上限 64 层' },
{ key: '布局控制', value: 'union 共享内存、packed 去填充、aligned(N) 强对齐' },
{ key: '运算符重载', value: '__add__ / __eq__ / __lshift__ 等全套 dunder' },
{ key: '构造', value: '__init__ / __new__ / __before_init__ / __sizeof__' },
],
},
meta: {
eyebrow: 'COMPILE-TIME',
title: '编译期设施',
sub: '宏、条件编译、内联 IR 与汇编、函数属性——都是语言内的语法,而不是预处理器。',
codeTitle: '声明与修饰',
code: [
{ t: 'define MAX_SIZE = 4096', c: '引用处内联,不生成全局符号' },
{ t: '' },
{ t: 'export def Init() -> int:', c: '豁免哈希前缀,供跨模块或 C 链接' },
{ t: ' return 0' },
{ t: '' },
{ t: 'inline def Fast(x: int) -> int:', c: '提示内联' },
{ t: ' return x * 2' },
{ t: '' },
{ t: 'extern def LibCall(x: int) -> int:', c: '只声明、不定义' },
{ t: ' pass' },
],
chipsTitle: '深入一层',
chips: [
{ name: '内联 LLVM IR', note: 'c.LLVMIR("add i32 …", int)' },
{ name: '内联汇编', note: 'c.Asm("nop") · 寄存器与内存约束' },
{ name: '函数属性', note: 'always_inline · nounwind · packed' },
{ name: '结构化条件编译', note: '语法结构取代 #ifdef' },
],
},
repl: {
eyebrow: 'REPL',
title: '交互式 REPL',
sub: '一行行写,逐句编译到动态库并立即执行——变量、函数与类型都能跨片段累积。',
points: [
{
title: '片段即模块',
desc: '每行输入被包成片段函数,翻译为模块 IR、编译为动态库、装载调用后读回结果。',
},
{
title: '跨片段变量',
desc: 'x: int = 5 之后可直接用 x + 1,会话变量按槽位寻址、持续有效。',
},
{
title: '跨片段函数',
desc: '块内顶格的 def 提升为导出函数(限制:签名单行、暂不支持 async def)。',
},
{
title: '跨片段类型',
desc: '块里的 class 进入会话,后续片段可直接构造并调用其方法。',
},
{
title: '多行块与错误恢复',
desc: '行尾冒号续行、空行执行;出错经统一出口恢复,不中断会话。',
},
],
sampleTitle: '会话示例',
lines: [
'>>> x: int = 5',
'>>> def twice(n: int) -> int:',
'... return n * 2',
'>>> twice(x) + 1',
'11',
],
},
source: {
eyebrow: 'SOURCE TOUR',
title: '源码导览',
sub: '编译器就在仓库里,规模与分工一览——可以逐模块读完。',
stats: [
{ value: '76', label: '编译器源文件(.vp)' },
{ value: '64,656', label: '本体源码行数' },
{ value: '52', label: '翻译器核心文件 · 49,978 行' },
{ value: '39', label: '自测用例' },
{ value: '22.1.8', label: 'LLVM 版本' },
{ value: 'Windows x64', label: '当前目标平台' },
],
dirsTitle: '目录职责',
dirs: [
{ path: 'App/main.vp', desc: '命令行入口:参数解析、阶段调度与构建编排' },
{ path: 'App/lib/Phase/', desc: '两阶段实现与构建管线:扫描、拓扑、产物落盘、并行编译' },
{ path: 'App/lib/Stub/', desc: '声明生成与合并:签名读取、拓扑合并、符号去重、类型前移' },
{ path: 'App/lib/core/Gen/', desc: '翻译器与代码生成核心:类型、表达式、语句、类与调用' },
{ path: 'App/lib/Projectrans/', desc: '工程配置、路径解析、哈希切片与缓存策略' },
{ path: 'Test/', desc: '自测套件:语法、指针、OOP、异步、负向与哈希命名空间' },
],
},
quickstart: {
eyebrow: 'GET STARTED',
title: '三分钟跑通 Hello Viper',
tagline: '你好,Viper,三分钟后见。',
sub: '编译器已完全自举——TransPyV 是原生可执行文件。准备 Windows(mingw)+ LLVM 工具链即可开跑。',
steps: [
{
num: '01',
title: '克隆编译器与模板',
desc: 'TransPyC 仓库即编译器本体,其中自举实现 TransPyV 已能直接编译工程;ViperTemplateProject 是开箱即用的 Windows 模板。',
},
{
num: '02',
title: '编译项目',
desc: '以原生 TransPyV 驱动两阶段编译,可执行文件产出到模板工程的 output/。',
},
{
num: '03',
title: '运行原生程序',
desc: '得到一个真正的原生 app.exe——打印出「你好,世界」。',
},
],
prereqs: ['TransPyV · 原生编译器', 'LLVM 22.1.8 · llc', 'mingw · clang++ / lld', 'git'],
cmdNote: '# Windows PowerShell · mingw + LLVM 已加入 PATH',
output: '你好,世界',
cliTitle: '常用命令行参数',
cli: [
{ flag: '--project <file.vpj>', desc: '指定工程文件,按工程配置驱动编译' },
{ flag: '--phase 1|2|all', desc: '只跑声明阶段 / 只跑翻译阶段 / 全流程' },
{ flag: '--repl', desc: '进入交互式 REPL' },
{ flag: '--clean', desc: '清理缓存与产物,等价于干净重建' },
{ flag: '--asan', desc: '以 ASan 插桩构建,定位内存问题' },
{ flag: '--phase2command', desc: '导出第二阶段构建命令' },
],
},
ecosystem: {
eyebrow: 'ECOSYSTEM',
title: '从编译器到操作系统',
sub: 'Viper 不是孤岛:围绕它的是完整的自研工具链与系统级实践。',
items: [
{
name: 'TransPyV',
role: '编译器',
desc: '自举的编译器本体:两阶段声明提取与 LLVM IR 翻译,本身即以 Viper 写成。',
href: 'https://git.gvsds.com/GVSDS/TransPyC',
badge: '自举',
},
{
name: 'includes/',
role: '标准库',
desc: '纯 Viper 的标准库:自研解析器、IR 构建、协程运行时、伙伴分配器、线程与 Win32 绑定。',
href: '',
badge: '',
},
{
name: 'testcheck',
role: '测试框架',
desc: '编译期断言与分组框架,39 个自测用例的统一入口。',
href: '',
badge: '',
},
{
name: 'ViperTemplateProject',
role: '模板工程',
desc: '开箱即用的 Windows 工程骨架:工程文件、构建脚本与示例源码。',
href: '',
badge: '',
},
{
name: 'Venom',
role: '包管理',
desc: '官方包管理器:包的分发、安装与依赖解析。',
href: 'https://venom.gvsds.com',
badge: '开发中',
},
{
name: 'ViperOS',
role: '操作系统',
desc: '以 Viper 构建的 x86_64 内核实践,检验语言在裸机上的极限。',
href: '',
badge: '',
},
],
},
roadmap: {
eyebrow: 'ROADMAP',
title: '自举完成,生态启程',
sub: '编译器已完全自举、摆脱 Python 运行时——下一步是把库生态做成 Viper 的一等公民。',
items: [
{
phase: 'done',
title: '完全自举',
desc: '编译器由 Viper 重写完成:76 个源文件、64,656 行,能编译自身并产出原生 TransPyV.exe;自研解析器与 IR 库取代了第三方依赖。',
},
{
phase: 'done',
title: '交互式 REPL',
desc: '会话变量、跨片段函数与类型:每段输入编译为动态库后即时执行,错误可恢复。',
},
{
phase: 'active',
title: '泛型特化',
desc: '类模板注册与按需特化已落地,自测套件暂将其屏蔽以隔离已知崩溃——修复后放出泛型容器。',
},
{
phase: 'next',
title: 'Venom 包管理',
desc: '官方包管理器上线,Viper 库的发布、安装与依赖解析成为一等公民。',
},
{
phase: 'next',
title: '跨平台目标',
desc: '当前仅支持 Windows x64(源码直接绑定 Win32),后续补齐类 Unix 目标与工具链适配。',
},
],
},
cta: {
title: '准备好写第一行原生代码了吗?',
sub: '从模板工程出发,五分钟内看到你的第一个 Viper 程序运行。',
start: '快速上手',
source: '查看源码',
},
footer: {
tagline: '底层,再简单一点吧?',
builtBy: 'Viper 官网 · GVSDS',
linksTitle: '导航',
backTop: '回到顶部',
},
}
+9
View File
@@ -0,0 +1,9 @@
import { createApp } from 'vue'
import App from './App.vue'
// 品牌字专用字体:自托管衬线体 Playfair Display Black(latin 900 子集,内嵌 woff2,无外网依赖)
// 备选:若要切回无衬线,改成 '@fontsource/inter/latin-900.css'(其依赖仍保留在 package.json)
import '@fontsource/playfair-display/latin-900.css'
import 'lenis/dist/lenis.css'
import '@/assets/base.css'
createApp(App).mount('#app')
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"types": ["node"]
}
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
host: '0.0.0.0',
port: 5175,
},
build: {
outDir: 'dist',
sourcemap: false,
chunkSizeWarningLimit: 1500,
cssCodeSplit: false,
},
})