chore: 初始化 Venom 官网独立仓库
VenomWebSite 原被 SMANAGE 主仓 .gitignore 排除、无版本控制,现拆为独立仓库并接入 git.gvsds.com/GVSDS/VenomWebSite。 包含:Vue 3 + TS + Vite 官网源码、Lenis 平滑惯性滚动、deploy.ps1 部署脚本、README 与 .gitignore / .gitattributes。
This commit is contained in:
commit
5021207790
24 files changed
+2756
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# 默认:入库统一 LF,检出时不因平台改变(避免 CRLF 噪音 diff)
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows 脚本保持 CRLF
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# 二进制:不做换行符转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.webp binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# 依赖与构建产物
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# 日志与临时文件
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,66 @@
|
||||
# VenomWebSite
|
||||
|
||||
Venom 官网([venom.gvsds.com](https://venom.gvsds.com))源码。
|
||||
|
||||
Venom 是 [Viper](https://viper.gvsds.com) 语言的包管理器:覆盖包从发布到安装、依赖解析,并接入 TransPyC 的两阶段编译链路。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3(`<script setup>` + TypeScript)
|
||||
- Vite 8(rolldown)
|
||||
- 零 UI 框架:手写 CSS 设计系统([src/assets/base.css](src/assets/base.css))
|
||||
- [Lenis](https://github.com/darkroomengineering/lenis) 平滑惯性滚动
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├─ App.vue 页面装配 + Lenis 实例 + 滚动揭示观察器
|
||||
├─ main.ts 入口
|
||||
├─ assets/base.css 设计系统:色板 / 字体栈 / 通用工具类
|
||||
├─ i18n/ 中英双语文案(index.ts 保存语言状态,zh.ts / en.ts 为词条)
|
||||
└─ components/
|
||||
├─ TheHeader.vue 顶栏:导航、语言切换
|
||||
├─ HeroSection.vue 首屏
|
||||
├─ CapabilitiesSection.vue 能力卡片(发布 / 解析 / 集成)
|
||||
├─ TerminalSection.vue 终端演示
|
||||
└─ TheFooter.vue 页脚
|
||||
deploy.ps1 构建 + 部署脚本(见下)
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run dev # 开发服务器:http://localhost:5176(监听 0.0.0.0,便于局域网/手机真机预览)
|
||||
```
|
||||
|
||||
## 构建与部署
|
||||
|
||||
```powershell
|
||||
npm run build # vue-tsc -b 类型检查 + vite build → dist/
|
||||
.\deploy.ps1 # 清理旧产物 → 全量构建 → 同步 dist 到 TempLate/Venom → 校验一致性
|
||||
```
|
||||
|
||||
`deploy.ps1` 做四件事:
|
||||
|
||||
1. 删除 `dist/` 与生产目录 `../TempLate/Venom/` 下的旧 `assets/`、`index.html`(避免残留旧 hash 文件)
|
||||
2. 执行 `npm run build`,类型检查失败即中断
|
||||
3. 把 `dist/*` 同步到 `../TempLate/Venom/`
|
||||
4. 校验生产目录的文件名集合与 `dist/` 完全一致,并确认 `index.html` 引用的入口 JS 真实存在
|
||||
|
||||
生产目录 `TempLate/Venom` 由 Django 站点以 `venom.gvsds.com` 子域名托管,产物入口使用根绝对路径 `/assets/...`。
|
||||
**本地直接打开构建产物会白屏**,需以 `TempLate/Venom` 为站点根访问。
|
||||
|
||||
> `deploy.ps1` 必须保持纯 ASCII:Windows PowerShell 5.1 会用 ANSI 代码页解析无 BOM 的 UTF-8 脚本,非 ASCII 字符会乱码。
|
||||
|
||||
## 相关仓库
|
||||
|
||||
- [ViperWebSite](https://git.gvsds.com/GVSDS/ViperWebSite) — Viper 语言官网
|
||||
- [GVSite](https://git.gvsds.com/GVSDS/GVSite) — GVSDS 工作室站
|
||||
|
||||
## 浏览器兼容
|
||||
|
||||
- `overflow: clip` 前置 `overflow-x: hidden` 回退(老 iOS Safari < 16 / Chrome < 90 会丢弃 `clip`)
|
||||
- 横向裁剪放在根元素 `html` 上:放 `body` 会把 `body` 的 `overflow-y` 算成 `auto`,被 Lenis 的 `allowNestedScroll` 误判为嵌套滚动源
|
||||
- 尊重 `prefers-reduced-motion`:系统开启「减弱动态效果」时自动退回原生滚动
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# VenomWebSite deploy script: build and sync dist to production dir (TempLate/Venom)
|
||||
#
|
||||
# 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/Venom) so no stale hashes remain
|
||||
# 2. Full rebuild (vue-tsc type check + vite build)
|
||||
# 3. Sync dist to TempLate/Venom (index.html + assets)
|
||||
# 4. Verify TempLate/Venom 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\Venom'
|
||||
$ProdAssetsDir = Join-Path $ProdRoot 'assets'
|
||||
$IndexHtml = Join-Path $ProdRoot 'index.html'
|
||||
|
||||
Write-Host "`n===== VENOM 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 "===== VENOM 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
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!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="Venom — Viper 官方包管理器:包的分发、安装与依赖解析。Venom — the official package manager for the Viper language.">
|
||||
<title>Venom · Viper 官方包管理器</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1225
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "venom-website",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Venom official website - the package manager for the Viper language (venom.gvsds.com)",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"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="#0e8a5d"/>
|
||||
<path d="M14 47 V17 h8 l10 19 10-19 h8 V47 H40 V28 L32 47 24 28 v19 Z"
|
||||
fill="#ffffff" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
+67
@@ -0,0 +1,67 @@
|
||||
<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 CapabilitiesSection from '@/components/CapabilitiesSection.vue'
|
||||
import TerminalSection from '@/components/TerminalSection.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 />
|
||||
<CapabilitiesSection />
|
||||
<TerminalSection />
|
||||
</main>
|
||||
<TheFooter />
|
||||
</template>
|
||||
@@ -0,0 +1,349 @@
|
||||
/* ============================================================
|
||||
Venom 官网 · 设计系统
|
||||
一张暗色画布 + 单一强调色(翡翠绿),双字体族,克制运动
|
||||
与 Viper 官网共享同一套设计 token(同属 GVSDS 语言生态)
|
||||
============================================================ */
|
||||
|
||||
: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(14, 138, 93, 0.3);
|
||||
--line-soft: #dfe8e3;
|
||||
--text: #10201a;
|
||||
--text-dim: #5b6e64;
|
||||
--text-faint: #8da097;
|
||||
--accent: #0e8a5d;
|
||||
--accent-rgb: 14, 138, 93; /* 主题色分量,供 rgba(var(--accent-rgb), α) 使用 */
|
||||
--accent-strong: #0b7650;
|
||||
--accent-ink: #ffffff;
|
||||
--glow: rgba(14, 138, 93, 0.16);
|
||||
--shadow-code: 0 26px 60px -30px rgba(15, 42, 32, 0.4);
|
||||
|
||||
/* 字体(本地栈,无外网依赖) */
|
||||
--font-sans: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Cascadia Code', Consolas, 'Courier New', monospace;
|
||||
|
||||
/* 代码高亮(深色代码块内使用) */
|
||||
--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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ------- 版式工具 ------- */
|
||||
.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 10px 26px -14px rgba(14, 138, 93, 0.7);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-strong);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 0 1px var(--accent-strong), 0 16px 34px -14px rgba(14, 138, 93, 0.8);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
border: 1px solid var(--line-soft);
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: var(--line);
|
||||
background: rgba(14, 138, 93, 0.06);
|
||||
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,147 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
/** 鼠标跟随光晕:在网格上做事件委托,把指针在卡片内的坐标写进该卡片的 CSS 变量 --mx / --my */
|
||||
function onCardPointerMove(e: PointerEvent) {
|
||||
const card = (e.target as HTMLElement | null)?.closest<HTMLElement>('.cap-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="capabilities" class="section capabilities">
|
||||
<div class="container">
|
||||
<div class="cap-head" data-reveal>
|
||||
<p class="eyebrow mono">{{ t.capabilities.eyebrow }}</p>
|
||||
<h2 class="section-title">{{ t.capabilities.title }}</h2>
|
||||
<p class="section-sub">{{ t.capabilities.sub }}</p>
|
||||
</div>
|
||||
|
||||
<div class="cap-grid" @pointermove="onCardPointerMove">
|
||||
<article
|
||||
v-for="(item, i) in t.capabilities.items"
|
||||
:key="item.num"
|
||||
class="cap-card"
|
||||
data-reveal
|
||||
:style="{ '--reveal-delay': `${i * 110}ms` }"
|
||||
>
|
||||
<p class="cap-num mono">{{ item.num }}</p>
|
||||
<p class="cap-name mono">{{ item.name }}</p>
|
||||
<h3 class="cap-title">{{ item.title }}</h3>
|
||||
<p class="cap-desc">{{ item.desc }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.capabilities {
|
||||
/* 顶部留透明区:让首屏辉光透上来继续可见,实现跨 section 的光晕衔接;
|
||||
其后接浅色带(两端回到 --bg,与相邻 section 无接缝) */
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
var(--bg) 20%,
|
||||
var(--bg-soft) 42%,
|
||||
var(--bg) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.cap-head {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 18px;
|
||||
margin-top: clamp(2.4rem, 5vw, 3.6rem);
|
||||
}
|
||||
|
||||
.cap-card {
|
||||
position: relative;
|
||||
overflow: hidden; /* 裁掉光晕的直角,贴合圆角卡片 */
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 26px 26px 24px;
|
||||
transition: border-color 0.25s var(--ease-out), transform 0.25s var(--ease-out),
|
||||
box-shadow 0.3s var(--ease-out);
|
||||
}
|
||||
|
||||
/* 鼠标跟随光晕:位置由 --mx / --my 决定,未移动时落在卡片中心 */
|
||||
.cap-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.14),
|
||||
transparent 68%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.28s var(--ease-out);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 仅在支持悬停的指针设备上启用,触屏不残留光斑 */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.cap-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cap-card:hover {
|
||||
border-color: var(--line);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 22px 46px -30px rgba(14, 138, 93, 0.5);
|
||||
}
|
||||
|
||||
/* 卡片内容压在光晕之上 */
|
||||
.cap-num,
|
||||
.cap-name,
|
||||
.cap-title,
|
||||
.cap-desc {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cap-num {
|
||||
color: var(--accent-strong);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.cap-name {
|
||||
margin-top: 2px;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
|
||||
.cap-title {
|
||||
margin-top: 14px;
|
||||
font-size: 1.16rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.cap-desc {
|
||||
margin-top: 8px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.cap-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="top" class="hero">
|
||||
<div class="hero-bg" aria-hidden="true">
|
||||
<div class="hero-glow"></div>
|
||||
</div>
|
||||
|
||||
<div class="container hero-inner">
|
||||
<p class="pill mono">
|
||||
<span class="dot"></span>
|
||||
<span>{{ t.hero.badge }}</span>
|
||||
</p>
|
||||
<h1 class="hero-brand">
|
||||
Venom<span class="brand-dot">.</span>
|
||||
</h1>
|
||||
<p class="hero-stamp mono">{{ t.hero.stamp }}</p>
|
||||
<p class="hero-lead">{{ t.hero.lead }}</p>
|
||||
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-primary" href="https://viper.gvsds.com" target="_blank" rel="noreferrer">
|
||||
{{ t.hero.ctaBack }}
|
||||
<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 mono"
|
||||
href="https://viper.gvsds.com/#ecosystem"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ t.hero.ctaSite }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="hero-scroll mono" href="#capabilities" aria-hidden="true">
|
||||
<span>↓</span>
|
||||
</a>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero {
|
||||
position: relative;
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 辉光要越过底边渗入下一 section,所以不能整体裁剪。
|
||||
这里只做横向裁剪:辉光是 1100px 固定宽度,不横向裁会把页面撑得比视口宽
|
||||
(手机端表现为可以左右拖动、右侧一片空白)。
|
||||
前置的 overflow: hidden 是不支持 overflow: clip 的浏览器的回退:辉光不外溢,但布局一定安全。 */
|
||||
overflow: hidden;
|
||||
padding: calc(var(--nav-h) + 24px) 0 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@supports (overflow-x: clip) {
|
||||
.hero {
|
||||
overflow-x: clip;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* 背景:白底 + 极淡翡翠辉光(下缘越过 hero 底边,继续可见于下一 section) */
|
||||
.hero-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
top: -24%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1100px;
|
||||
height: 155%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(ellipse at 50% 41%, rgba(14, 138, 93, 0.1), transparent 68%);
|
||||
filter: blur(24px);
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 880px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(29, 111, 184, 0.35);
|
||||
color: #1d6fb8;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
border-radius: 999px;
|
||||
padding: 5px 13px;
|
||||
opacity: 0;
|
||||
animation: riseIn 0.7s var(--ease-out) 0.1s forwards;
|
||||
}
|
||||
|
||||
.pill .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1d6fb8;
|
||||
animation: caretBlink 1.4s steps(1) infinite;
|
||||
}
|
||||
|
||||
.hero-brand {
|
||||
margin: 20px 0 0;
|
||||
font-size: clamp(3.6rem, 10vw, 6.4rem);
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.035em;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
opacity: 0;
|
||||
animation: riseIn 0.75s var(--ease-out) 0.2s forwards;
|
||||
}
|
||||
|
||||
.brand-dot {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hero-stamp {
|
||||
margin-top: 18px;
|
||||
color: var(--accent-strong);
|
||||
font-size: clamp(0.9rem, 1.8vw, 1.15rem);
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 650;
|
||||
opacity: 0;
|
||||
animation: riseIn 0.7s var(--ease-out) 0.32s forwards;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin: 26px auto 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 1.04rem;
|
||||
max-width: 60ch;
|
||||
opacity: 0;
|
||||
animation: riseIn 0.7s var(--ease-out) 0.42s forwards;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin-top: 40px;
|
||||
opacity: 0;
|
||||
animation: riseIn 0.7s var(--ease-out) 0.52s 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-scroll {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 20px;
|
||||
z-index: 1;
|
||||
transform: translateX(-50%);
|
||||
color: var(--text-faint);
|
||||
font-size: 1rem;
|
||||
opacity: 0.6;
|
||||
animation: fadeIn 0.8s ease 1.4s backwards, floatY 2.4s ease-in-out 1.4s infinite;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.hero-scroll:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes floatY {
|
||||
0%, 100% { transform: translate(-50%, 0); }
|
||||
50% { transform: translate(-50%, 7px); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero {
|
||||
align-items: flex-start;
|
||||
padding-top: calc(var(--nav-h) + 56px);
|
||||
}
|
||||
.hero-scroll {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="terminal" class="section terminal-section">
|
||||
<div class="container term-wrap">
|
||||
<div class="code-win term" 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.terminal.file }}</span>
|
||||
</div>
|
||||
<pre><code><span class="tok-cmt">{{ t.terminal.cmdComment }}</span>
|
||||
<span class="p"><span class="tok-fn">venom</span> <span class="tok-type">init</span> <span class="tok-str">--template viper-app</span></span>
|
||||
<span class="p"><span class="tok-fn">venom</span> <span class="tok-type">add</span> <span class="tok-str">viper-json</span></span>
|
||||
<span class="p"><span class="tok-fn">venom</span> <span class="tok-type">build</span></span>
|
||||
<span class="tok-plain">→</span> <span class="out">./output/app.exe</span> <span class="tok-plain">ready in</span> <span class="tok-num">86ms</span><span class="caret"></span></code></pre>
|
||||
</div>
|
||||
<p class="soon mono" data-reveal>{{ t.terminal.soon }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.terminal-section {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.term-wrap {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.term .out {
|
||||
color: #5ad7a6;
|
||||
}
|
||||
|
||||
.term .p::before {
|
||||
content: '$ ';
|
||||
color: #5ad7a6;
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.soon {
|
||||
margin-top: 14px;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="container footer-inner">
|
||||
<div class="f-brand">
|
||||
<div class="f-brand-line">
|
||||
<svg viewBox="0 0 64 64" aria-hidden="true">
|
||||
<rect width="64" height="64" rx="14" fill="none" />
|
||||
<path d="M14 47 V17 h8 l10 19 10-19 h8 V47 H40 V28 L32 47 24 28 v19 Z" />
|
||||
</svg>
|
||||
<span class="f-name">Venom</span>
|
||||
</div>
|
||||
<p class="f-tagline">{{ t.footer.tagline }}</p>
|
||||
</div>
|
||||
|
||||
<div class="foot-links mono">
|
||||
<a href="https://viper.gvsds.com" target="_blank" rel="noreferrer">Viper</a>
|
||||
<a href="https://venom.gvsds.com" target="_blank" rel="noreferrer">Venom</a>
|
||||
<a href="https://git.gvsds.com/GVSDS/TransPyC" target="_blank" rel="noreferrer">TransPyC</a>
|
||||
</div>
|
||||
|
||||
<p class="f-copy mono">{{ t.footer.copyright.replace('©', '© ' + year) }}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
--text: #e7f2ec;
|
||||
--text-dim: #9db9ab;
|
||||
--text-faint: #6f8d7e;
|
||||
--line-soft: rgba(255, 255, 255, 0.09);
|
||||
--accent: #5ad7a6;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: var(--bg-deep);
|
||||
padding: 34px 0 30px;
|
||||
color: #6f8d7e;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.f-brand-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.f-brand-line svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.f-name {
|
||||
color: #e7f2ec;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.f-tagline {
|
||||
margin-top: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.foot-links {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.foot-links a {
|
||||
color: #9db9ab;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.foot-links a:hover {
|
||||
color: #5ad7a6;
|
||||
}
|
||||
|
||||
.f-copy {
|
||||
color: #6f8d7e;
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.footer-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { t, currentLang, setLang } from '@/i18n'
|
||||
|
||||
const scrolled = ref(false)
|
||||
|
||||
function onScroll() {
|
||||
scrolled.value = window.scrollY > 10
|
||||
}
|
||||
|
||||
function toggleLang() {
|
||||
setLang(currentLang.value === 'zh' ? 'en' : 'zh')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="site-header" :class="{ scrolled }">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="#top">
|
||||
<svg viewBox="0 0 64 64" aria-hidden="true">
|
||||
<rect width="64" height="64" rx="14" fill="none" />
|
||||
<path d="M14 47 V17 h8 l10 19 10-19 h8 V47 H40 V28 L32 47 24 28 v19 Z" />
|
||||
</svg>
|
||||
<span class="brand-name">Venom</span>
|
||||
<span class="brand-sub mono">PKG MANAGER</span>
|
||||
</a>
|
||||
|
||||
<div class="header-actions">
|
||||
<button class="lang-btn mono" @click="toggleLang" aria-label="Switch language">
|
||||
{{ currentLang === 'zh' ? 'EN' : '中' }}
|
||||
</button>
|
||||
<a
|
||||
class="nav-pill mono"
|
||||
href="https://viper.gvsds.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ t.header.backToViper }}
|
||||
</a>
|
||||
</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: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-bottom-color: var(--line-soft);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-weight: 750;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
color: var(--accent);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.22em;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
border: 1px solid var(--line-soft);
|
||||
color: var(--text-dim);
|
||||
width: 42px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.08em;
|
||||
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
border-color: var(--line);
|
||||
color: var(--accent-strong);
|
||||
background: rgba(14, 138, 93, 0.06);
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.86rem;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 999px;
|
||||
padding: 7px 15px;
|
||||
transition: color 0.22s var(--ease-out), border-color 0.22s var(--ease-out),
|
||||
background 0.22s var(--ease-out);
|
||||
}
|
||||
|
||||
.nav-pill:hover {
|
||||
color: var(--accent-strong);
|
||||
border-color: var(--line);
|
||||
background: rgba(14, 138, 93, 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav-pill {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
// Venom official website · English dictionary
|
||||
// Structure must mirror zh.ts (validated as typeof zh)
|
||||
import type zh from './zh'
|
||||
|
||||
const en: typeof zh = {
|
||||
header: {
|
||||
backToViper: '← viper.gvsds.com',
|
||||
},
|
||||
hero: {
|
||||
badge: 'UNDER DEVELOPMENT',
|
||||
stamp: 'The package manager for the Viper language',
|
||||
lead: 'Venom is the official package manager built for Viper — unifying how Viper libraries are published, installed and resolved, so sharing feels as native as the language itself.',
|
||||
ctaBack: 'Back to the Viper website',
|
||||
ctaSite: 'viper.gvsds.com ↗',
|
||||
},
|
||||
capabilities: {
|
||||
eyebrow: 'WHAT VENOM DOES',
|
||||
title: 'One flow for the whole package life',
|
||||
sub: 'From publish to install, then dependency resolution — finally plugged straight into the TransPyC two-phase toolchain.',
|
||||
items: [
|
||||
{
|
||||
num: '01',
|
||||
name: 'PUBLISH',
|
||||
title: 'Publish',
|
||||
desc: 'Ship Viper libraries in versions anyone can fetch.',
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
name: 'RESOLVE',
|
||||
title: 'Resolve',
|
||||
desc: 'Deterministic dependency resolution atop Viper SHA1 namespaces.',
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
name: 'INTEGRATE',
|
||||
title: 'Integrate',
|
||||
desc: 'Plugs straight into the TransPyC two-phase toolchain.',
|
||||
},
|
||||
],
|
||||
},
|
||||
terminal: {
|
||||
file: 'venom — sample',
|
||||
cmdComment: '# sample commands; the real CLI lands with the release',
|
||||
soon: '// a brand placeholder — ecosystem & interfaces to follow',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'The Viper package manager · work in progress',
|
||||
copyright: '© GVSDS · Venom, the Viper package manager',
|
||||
},
|
||||
}
|
||||
|
||||
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('venom-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('venom-lang', lang)
|
||||
deepCopyInto(t as unknown as Record<string, unknown>, lang === 'zh' ? zh : en)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Venom 官网 · 中文文案字典
|
||||
// 注意:结构与 en.ts 完全一致(en.ts 以 typeof zh 校验)
|
||||
export default {
|
||||
header: {
|
||||
backToViper: '← viper.gvsds.com',
|
||||
},
|
||||
hero: {
|
||||
badge: 'UNDER DEVELOPMENT · 开发中',
|
||||
stamp: 'Viper 语言官方包管理器',
|
||||
lead: 'Venom 是为 Viper 语言而生的官方包管理器:统一 Viper 库的分发、安装与依赖解析,让复用生态像使用语言本身一样自然。',
|
||||
ctaBack: '回到 Viper 官网',
|
||||
ctaSite: 'viper.gvsds.com ↗',
|
||||
},
|
||||
capabilities: {
|
||||
eyebrow: 'WHAT VENOM DOES',
|
||||
title: '包的一生,一次走完',
|
||||
sub: '从发布到安装,再到依赖解析,最后无缝接入 TransPyC 的两阶段编译链路。',
|
||||
items: [
|
||||
{
|
||||
num: '01',
|
||||
name: 'PUBLISH',
|
||||
title: '发布',
|
||||
desc: '让 Viper 库能够按版本发布并被任何人获取。',
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
name: 'RESOLVE',
|
||||
title: '解析',
|
||||
desc: '在 Viper 的 SHA1 命名空间之上,做确定性的依赖解析。',
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
name: 'INTEGRATE',
|
||||
title: '集成',
|
||||
desc: '与 TransPyC 两阶段编译链路无缝衔接。',
|
||||
},
|
||||
],
|
||||
},
|
||||
terminal: {
|
||||
file: 'venom — 示意 · sample',
|
||||
cmdComment: '# 命令示意,正式接口将于发布时确定',
|
||||
soon: '// 当前为品牌占位页 · 生态与接口随后公布 —— placeholder page, details will follow',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'Viper 官方包管理器 · 开发中',
|
||||
copyright: '© GVSDS · Venom, the Viper package manager',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
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: 5176,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
chunkSizeWarningLimit: 1500,
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user