Files
verdaccio-cache-manager/frontend/src/App.vue
T
陈银军 8a54fb2d5c feat: 包信息展示与扫描数据修正
- scanner: 版本数只统计实际存在的 .tgz(修复 playwright-core 5113 版本虚高),解析 description/homepage/repository/author/license 元数据字段
- 前端: 新增 PkgLogo 首字母徽标组件,详情抽屉展示包 logo(主页 favicon 加载失败自动回退)、介绍与主页/仓库/许可胶囊,优化抽屉头部布局
- 全量 UI 改进: 毛玻璃扫描遮罩与进度、头像菜单重新扫描、主题切换、移动端 Tabbar/StatMini/Top30Panel 响应式、列表瘦身与详情按需加载
- compose: TZ=Asia/Shanghai + 命名卷 vcm-data 解决 SQLite 权限
- 更新 amd64 镜像 tar
2026-09-19 21:07:27 +08:00

222 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import AppTopbar from './components/AppTopbar.vue'
import MobileTabbar from './components/MobileTabbar.vue'
import SettingDrawer from './components/SettingDrawer.vue'
import { useStore } from './store'
const route = useRoute()
const { state, refreshAll, scanNow } = useStore()
const settingsOpen = ref(false)
const isLogin = () => route.path === '/login'
const isMobile = () => window.matchMedia('(max-width: 767px)').matches
// 数据基于何时扫描(后端扫描时间,缓存复用)
const fmtScanTime = computed(() => {
const t = state.scannedAt
if (!t) return ''
const d = new Date(t)
const p = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
})
async function doRescan() {
await scanNow()
}
// 扫描进度字节数 → 可读文本
const fmtScanBytes = computed(() => {
const b = state.scanProgress.bytes || 0
const mb = b / 1024 / 1024
if (mb >= 1024) return `${(mb / 1024).toFixed(2)} GB`
return `${mb.toFixed(1)} MB`
})
onMounted(() => {
if (!isLogin()) refreshAll()
})
</script>
<template>
<div class="ambient"></div>
<div v-if="!isLogin()">
<!-- 桌面/平板顶栏 -->
<AppTopbar class="desktop-topbar" @open-settings="settingsOpen = true" @rescan="doRescan" />
<!-- 移动端顶栏:☰ + 品牌 -->
<header class="m-topbar">
<div class="m-brand">
<div class="m-logo">📦</div>
<div><b>Verdaccio</b>&nbsp;<span>Manager</span></div>
</div>
<div class="m-menu" @click="settingsOpen = true">☰</div>
</header>
<!-- 导航栏下方:扫描信息条 -->
<div class="scan-banner" v-if="state.loading || state.scannedAt">
<span v-if="state.loading" class="scan-banner-loading">⏳ 正在扫描缓存目录,请稍候…</span>
<span v-else>🕐 数据基于 {{ fmtScanTime }} 扫描</span>
</div>
<!-- 扫描遮罩:毛玻璃 + loading + 实时进度 -->
<div v-if="state.loading" class="scan-mask">
<div class="scan-card">
<div class="scan-spinner"></div>
<div class="scan-title">正在扫描缓存目录…</div>
<div class="scan-info">
已扫描 <b>{{ state.scanProgress.packages.toLocaleString() }}</b> 个包
<span class="dot">·</span> <b>{{ fmtScanBytes }}</b>
</div>
<div class="scan-bar"><i></i></div>
</div>
</div>
</div>
<router-view v-slot="{ Component }">
<transition name="fade-slide" appear>
<component :is="Component" :key="route.path" />
</transition>
</router-view>
<MobileTabbar v-if="!isLogin() && isMobile()" />
<SettingDrawer v-model="settingsOpen" />
</template>
<style scoped>
.m-topbar { display: none; }
/* —— 扫描信息条(顶栏下方) —— */
.scan-banner {
display: flex;
align-items: center;
justify-content: flex-start; /* 靠左对齐 */
gap: 8px;
padding: 6px 20px;
font-size: 12px;
color: var(--ink-faint);
background: var(--panel-soft);
border-bottom: 1px solid var(--line-soft);
font-family: var(--num);
}
.scan-banner-loading {
color: var(--orange);
font-weight: 600;
animation: blink 1.2s ease-in-out infinite;
}
/* —— 扫描遮罩(玻璃拟态:半透明背景 + 卡片磨砂模糊 + 细边框/高光)
遮罩层只做半透明(避免全屏 blur 的性能开销与双倍模糊),
真正的毛玻璃效果做在卡片上:半透明背景 + backdrop-filter 模糊 + 细边框 + 高光 —— */
.scan-mask {
position: fixed;
inset: 0;
z-index: 500;
display: grid;
place-items: center;
background: rgba(18, 16, 14, .38);
}
html[data-theme="bright"] .scan-mask { background: rgba(244, 242, 239, .45); }
.scan-card {
position: relative;
background: rgba(28, 26, 24, .55);
backdrop-filter: blur(18px) saturate(1.35);
-webkit-backdrop-filter: blur(18px) saturate(1.35);
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 20px;
box-shadow:
0 24px 60px -16px rgba(0, 0, 0, .35),
inset 0 1px 0 rgba(255, 255, 255, .18);
padding: 32px 44px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
min-width: 300px;
max-width: 92vw;
}
html[data-theme="bright"] .scan-card {
background: rgba(255, 255, 255, .55);
border-color: rgba(255, 255, 255, .55);
box-shadow:
0 24px 60px -16px rgba(0, 0, 0, .22),
inset 0 1px 0 rgba(255, 255, 255, .85);
}
.scan-spinner {
width: 40px;
height: 40px;
border-radius: 50%;
border: 4px solid var(--line);
border-top-color: var(--orange);
animation: spin360 .8s linear infinite;
}
.scan-title { font-size: 15px; font-weight: 700; }
.scan-info { font-family: var(--num); font-size: 12.5px; color: var(--ink-dim); }
.scan-info b { color: var(--orange); font-weight: 700; }
.scan-info .dot { margin: 0 4px; color: var(--ink-faint); }
.scan-bar {
width: 100%;
height: 6px;
border-radius: 99px;
background: var(--num-bg);
overflow: hidden;
margin-top: 6px;
}
.scan-bar i {
display: block;
height: 100%;
width: 42%;
border-radius: 99px;
background: linear-gradient(90deg, transparent, var(--orange), transparent);
animation: scan-slide 1.1s ease-in-out infinite;
}
@keyframes scan-slide {
from { transform: translateX(-110%); }
to { transform: translateX(340%); }
}
@media (max-width: 767px) {
.desktop-topbar { display: none; }
.m-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px 10px;
background: var(--card);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 100;
}
.scan-banner {
padding: 5px 16px;
font-size: 11px;
}
.m-brand { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; }
.m-logo {
width: 30px;
height: 30px;
border-radius: 10px;
display: grid;
place-items: center;
font-size: 15px;
background: var(--grad-main);
}
.m-brand b { color: var(--orange); }
.m-brand span { color: var(--ink-faint); font-weight: 600; font-size: 12px; }
.m-menu {
width: 34px;
height: 34px;
border-radius: 11px;
display: grid;
place-items: center;
background: var(--card-2);
border: 1px solid var(--line);
font-size: 16px;
color: var(--ink-dim);
cursor: pointer;
}
}
</style>