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:
TermiNexus committed 2026-09-28 17:31:21 +08:00
commit 5021207790
24 files changed
+2756

No files matched your search

+18
View File
@@ -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
View File
@@ -0,0 +1,11 @@
# 依赖与构建产物
node_modules/
dist/
# 日志与临时文件
*.log
.DS_Store
# 编辑器
.vscode/
.idea/
+66
View File
@@ -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
View File
@@ -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
Vendored
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
export default component
}
+14
View File
@@ -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>
+1225
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#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
View File
@@ -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>
+349
View File
@@ -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;
}
}
+147
View File
@@ -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>
+207
View File
@@ -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>
+53
View File
@@ -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>
+104
View File
@@ -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>
+150
View File
@@ -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&nbsp;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>
+52
View File
@@ -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
+44
View File
@@ -0,0 +1,44 @@
// 轻量 i18n:zh/en 双字典 + 响应式替换(无外部依赖)
import { reactive, computed } from 'vue'
import zh from './zh'
import en from './en'
export type Lang = 'zh' | 'en'
function deepCopyInto(target: Record<string, unknown>, source: Record<string, unknown>): void {
for (const key of Object.keys(source)) {
const srcVal = source[key]
const dstVal = target[key]
if (
srcVal !== null &&
typeof srcVal === 'object' &&
!Array.isArray(srcVal) &&
dstVal !== null &&
typeof dstVal === 'object'
) {
deepCopyInto(dstVal as Record<string, unknown>, srcVal as Record<string, unknown>)
} else {
target[key] = srcVal
}
}
}
const state = reactive({
lang: (localStorage.getItem('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)
}
+48
View File
@@ -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',
},
}
+6
View File
@@ -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')
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"types": ["node"]
}
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
host: '0.0.0.0',
port: 5176,
},
build: {
outDir: 'dist',
sourcemap: false,
chunkSizeWarningLimit: 1500,
cssCodeSplit: false,
},
})