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:
commit
01a51477ed
29 files changed
+5307
No files matched your search
+11
@@ -0,0 +1,11 @@
|
||||
# 依赖与构建产物
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# 日志与临时文件
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
+106
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
Generated
+1245
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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: '回到顶部',
|
||||
},
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user