feat: 官网配品牌蓝——顶栏 0073B7、页脚 0070B2、详情页头部块 006DAD

顶栏改常驻蓝底白字(滚动仅加阴影,不再白底透明);页脚底色改 0070B2 配白字;详情页按 PyPI banner--project 版式把包名+摘要+安装命令做成 006DAD 蓝底头部块,COPY 按钮白底蓝字。
This commit is contained in:
TermiNexus committed 2026-10-07 20:27:11 +08:00
1 parent 34fa817a31
commit 63ad1eb965
3 files changed
+186 -150

No files matched your search

+11 -13
View File
@@ -31,15 +31,12 @@ const year = new Date().getFullYear()
<style scoped>
.footer {
--text: #e8f1fa;
--text-dim: #9db8cf;
--text-faint: #6f88a0;
--line-soft: rgba(255, 255, 255, 0.09);
--accent: #7cc4ff;
border-top: 1px solid rgba(255, 255, 255, 0.08);
background: var(--bg-deep);
--foot-fg: #ffffff;
--foot-fg-dim: rgba(255, 255, 255, 0.82);
--foot-fg-faint: rgba(255, 255, 255, 0.68);
background: #0070b2;
padding: 34px 0 30px;
color: #6f88a0;
color: var(--foot-fg-dim);
}
.footer-inner {
@@ -60,11 +57,11 @@ const year = new Date().getFullYear()
.f-brand-line svg {
width: 22px;
height: 22px;
fill: var(--accent);
fill: var(--foot-fg);
}
.f-name {
color: #e8f1fa;
color: var(--foot-fg);
font-size: 1.05rem;
font-weight: 750;
}
@@ -80,16 +77,17 @@ const year = new Date().getFullYear()
}
.foot-links a {
color: #9db8cf;
color: var(--foot-fg-dim);
transition: color 0.2s;
}
.foot-links a:hover {
color: #7cc4ff;
color: var(--foot-fg);
text-decoration: underline;
}
.f-copy {
color: #6f88a0;
color: var(--foot-fg-faint);
font-size: 0.76rem;
letter-spacing: 0.04em;
}
+18 -19
View File
@@ -53,23 +53,23 @@ onBeforeUnmount(() => {
</template>
<style scoped>
/* 常驻品牌蓝顶栏(#0073B7 底 + 白字),滚动时只加一层阴影 */
.site-header {
--hdr-fg: #ffffff;
--hdr-fg-dim: rgba(255, 255, 255, 0.82);
--hdr-line: rgba(255, 255, 255, 0.45);
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);
background: #0073b7;
transition: box-shadow 0.3s 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);
box-shadow: 0 2px 12px -6px rgba(10, 26, 47, 0.45);
}
.header-inner {
@@ -88,17 +88,18 @@ onBeforeUnmount(() => {
.brand svg {
width: 24px;
height: 24px;
fill: var(--accent);
fill: var(--hdr-fg);
}
.brand-name {
font-weight: 750;
font-size: 1.2rem;
letter-spacing: 0.02em;
color: var(--hdr-fg);
}
.brand-sub {
color: var(--accent);
color: var(--hdr-fg-dim);
font-size: 0.62rem;
letter-spacing: 0.22em;
transform: translateY(1px);
@@ -111,8 +112,8 @@ onBeforeUnmount(() => {
}
.lang-btn {
border: 1px solid var(--line-soft);
color: var(--text-dim);
border: 1px solid var(--hdr-line);
color: var(--hdr-fg);
width: 42px;
height: 34px;
border-radius: 8px;
@@ -122,15 +123,14 @@ onBeforeUnmount(() => {
}
.lang-btn:hover {
border-color: var(--line);
color: var(--accent-strong);
background: rgba(var(--accent-rgb), 0.06);
border-color: #fff;
background: rgba(255, 255, 255, 0.16);
}
.nav-pill {
color: var(--text-dim);
color: var(--hdr-fg);
font-size: 0.86rem;
border: 1px solid var(--line-soft);
border: 1px solid var(--hdr-line);
border-radius: 999px;
padding: 7px 15px;
transition: color 0.22s var(--ease-out), border-color 0.22s var(--ease-out),
@@ -138,9 +138,8 @@ onBeforeUnmount(() => {
}
.nav-pill:hover {
color: var(--accent-strong);
border-color: var(--line);
background: rgba(var(--accent-rgb), 0.05);
border-color: #fff;
background: rgba(255, 255, 255, 0.16);
}
@media (max-width: 640px) {
+157 -118
View File
@@ -3,8 +3,9 @@
* 包详情页 `/project/<包名>`(PyPI 风格版式)
*
* 数据来自 `/API/Venom/Project/<包名>`(主服务按域名反查产品并回源子服务器)。
* 左侧为 README(Markdown → 消毒 HTML),右侧为元数据 / 依赖 / 版本历史卡片。
* 数据到达前展示骨架 loading 层,避免首屏空白。
* 顶部为 PyPI `banner--project` 式蓝色头部块(包名 + 摘要 + 安装命令),
* 下方左栏为 README(Markdown → 消毒 HTML),右栏为元数据 / 依赖 / 版本历史卡片。
* 数据到达前头部与正文都展示骨架 loading,避免首屏空白。
*/
import { computed, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
@@ -78,19 +79,41 @@ watch(() => route.params.name, load, { immediate: true })
<template>
<section class="project">
<div class="container">
<RouterLink class="back" to="/">{{ t.project.back }}</RouterLink>
<!-- ==================== 顶部头部块(banner) ==================== -->
<div v-if="!notFound && !error" class="banner">
<div class="container banner-inner">
<RouterLink class="back" to="/">{{ t.project.back }}</RouterLink>
<!-- ==================== loading 层(骨架) ==================== -->
<template v-if="project">
<h1 class="banner-name">
{{ project.PackageName }}
<span v-if="project.LatestVersion" class="banner-ver">{{ project.LatestVersion }}</span>
</h1>
<p class="banner-summary">{{ project.PackageDesc || t.project.noDesc }}</p>
<div class="install">
<code class="install-cmd">{{ installCommand }}</code>
<button class="copy" @click="copyInstall">{{ copied ? 'OK' : 'COPY' }}</button>
</div>
<p class="banner-hint">{{ t.project.installHint }}</p>
</template>
<template v-else>
<div class="sk sk-name on-blue"></div>
<div class="sk sk-line on-blue"></div>
<div class="sk sk-bar on-blue"></div>
<p class="banner-hint">{{ t.project.loading }}</p>
</template>
</div>
</div>
<!-- ==================== 正文 ==================== -->
<div class="container body">
<!-- loading 层(骨架) -->
<div v-if="loading" class="layout" aria-busy="true" aria-live="polite">
<div class="main">
<div class="sk sk-title"></div>
<div class="sk sk-line"></div>
<div class="sk sk-line short"></div>
<div class="sk sk-bar"></div>
<div class="sk sk-block"></div>
<div class="sk sk-block"></div>
<p class="sk-hint">{{ t.project.loading }}</p>
<div class="sk sk-block short"></div>
</div>
<aside class="side">
<div class="sk sk-card"></div>
@@ -98,7 +121,7 @@ watch(() => route.params.name, load, { immediate: true })
</aside>
</div>
<!-- ==================== 不存在 / 失败 ==================== -->
<!-- 不存在 / 失败 -->
<div v-else-if="notFound" class="state">
<p class="state-title">{{ t.project.notFound }}</p>
<p class="state-sub">{{ route.params.name }}</p>
@@ -111,20 +134,9 @@ watch(() => route.params.name, load, { immediate: true })
<button class="btn btn-ghost" @click="load">{{ t.project.retry }}</button>
</div>
<!-- ==================== 包详情 ==================== -->
<!-- 包详情 -->
<div v-else-if="project" class="layout">
<div class="main">
<h1 class="pkg-name">{{ project.PackageName }}</h1>
<p class="pkg-desc">{{ project.PackageDesc || t.project.noDesc }}</p>
<div class="install">
<code class="install-cmd">{{ installCommand }}</code>
<button class="copy" @click="copyInstall">{{ copied ? 'OK' : 'COPY' }}</button>
</div>
<p class="hint">{{ t.project.installHint }}</p>
<hr class="rule" />
<article class="readme">
<div v-if="readmeHtml" class="md" v-html="readmeHtml"></div>
</article>
@@ -195,21 +207,12 @@ watch(() => route.params.name, load, { immediate: true })
<style scoped>
/* ==================== 页面骨架 ==================== */
.project {
padding-top: calc(var(--nav-h) + 28px);
padding-bottom: clamp(3rem, 6vw, 5rem);
min-height: 70vh;
}
.back {
display: inline-block;
font-size: 0.85rem;
color: var(--accent);
margin-bottom: 20px;
}
.back:hover {
color: var(--accent-strong);
text-decoration: underline;
.body {
padding-top: 30px;
}
.layout {
@@ -230,6 +233,98 @@ watch(() => route.params.name, load, { immediate: true })
min-width: 0;
}
/* ==================== 顶部头部块(PyPI banner--project) ==================== */
.banner {
background: #006dad;
color: #fff;
padding: calc(var(--nav-h) + 22px) 0 30px;
}
.banner-inner {
display: block;
}
.banner .back {
display: inline-block;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 16px;
}
.banner .back:hover {
color: #fff;
text-decoration: underline;
}
.banner-name {
font-size: clamp(1.7rem, 3.2vw, 2.2rem);
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
color: #fff;
word-break: break-all;
}
.banner-ver {
margin-left: 10px;
font-size: 0.95rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.72);
vertical-align: middle;
}
.banner-summary {
margin-top: 10px;
font-size: 1rem;
color: rgba(255, 255, 255, 0.92);
max-width: 76ch;
}
.install {
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
max-width: 620px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--radius);
padding: 9px 11px;
}
.install-cmd {
flex: 1;
min-width: 0;
font-family: var(--font-code);
font-size: 0.86rem;
color: #fff;
overflow-x: auto;
white-space: nowrap;
}
.copy {
flex-shrink: 0;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
color: #006dad;
background: #fff;
border: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 5px 11px;
transition: background 0.15s, color 0.15s;
}
.copy:hover {
background: rgba(255, 255, 255, 0.85);
}
.banner-hint {
margin-top: 9px;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.72);
}
/* ==================== loading 骨架 ==================== */
.sk {
background: linear-gradient(90deg, #eef0f2 25%, #f6f7f8 37%, #eef0f2 63%);
@@ -238,6 +333,17 @@ watch(() => route.params.name, load, { immediate: true })
animation: skShimmer 1.3s ease-in-out infinite;
}
/* 蓝色头部块上的骨架:半透明白 */
.sk.on-blue {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.2) 25%,
rgba(255, 255, 255, 0.34) 37%,
rgba(255, 255, 255, 0.2) 63%
);
background-size: 400% 100%;
}
@keyframes skShimmer {
0% {
background-position: 100% 50%;
@@ -253,42 +359,43 @@ watch(() => route.params.name, load, { immediate: true })
}
}
.sk-title {
width: 42%;
height: 34px;
.sk-name {
width: 38%;
height: 32px;
}
.sk-line {
margin-top: 14px;
width: 78%;
width: 62%;
height: 14px;
}
.sk-line.short {
width: 52%;
}
.sk-bar {
margin-top: 22px;
margin-top: 20px;
width: 100%;
max-width: 620px;
height: 40px;
}
.sk-block {
margin-top: 26px;
width: 100%;
height: 120px;
height: 130px;
}
.sk-block + .sk-block {
margin-top: 22px;
}
.sk-block.short {
height: 70px;
}
.sk-card {
height: 150px;
margin-bottom: 18px;
}
.sk-hint {
.sk-card + .sk-card {
margin-top: 18px;
font-size: 0.82rem;
color: var(--text-faint);
}
/* ==================== 状态(不存在 / 失败) ==================== */
@@ -297,7 +404,7 @@ watch(() => route.params.name, load, { immediate: true })
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 40px 0;
padding: 32px 0;
}
.state-title {
@@ -312,74 +419,6 @@ watch(() => route.params.name, load, { immediate: true })
word-break: break-all;
}
/* ==================== 包信息 ==================== */
.pkg-name {
font-size: clamp(1.75rem, 3.2vw, 2.25rem);
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--text);
word-break: break-all;
}
.pkg-desc {
margin-top: 10px;
font-size: 1rem;
color: var(--text-dim);
max-width: 72ch;
}
.install {
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-soft);
border: 1px solid var(--line-soft);
border-radius: var(--radius);
padding: 10px 12px;
}
.install-cmd {
flex: 1;
min-width: 0;
font-family: var(--font-code);
font-size: 0.86rem;
color: var(--text);
overflow-x: auto;
white-space: nowrap;
}
.copy {
flex-shrink: 0;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
color: var(--accent);
border: 1px solid var(--line-soft);
border-radius: var(--radius-sm);
padding: 5px 10px;
background: #fff;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.copy:hover {
color: var(--accent-strong);
border-color: var(--accent);
}
.hint {
margin-top: 8px;
font-size: 0.8rem;
color: var(--text-faint);
}
.rule {
margin: 28px 0 24px;
border: none;
border-top: 1px solid var(--line-soft);
}
/* ==================== README ==================== */
.md {
color: var(--text);