fix(mobile): 统一移动端汉堡菜单与PC端用户菜单选项
CI / Backend tests (push) Canceled after 0s
CI / Frontend build (push) Canceled after 0s

- SettingDrawer 新增「重新扫描」「包清单定时配置」,与 PC 端 AppTopbar pick() 保持一致
- 引入 useStore 读取 loading 状态,扫描中禁用重新扫描项
- 引入 PkgListConfigDialog,点击「包清单定时配置」弹出与 PC 端一致
- App.vue SettingDrawer 新增 @rescan=doRescan 事件监听
- 补充 .disabled 样式,扫描时置灰不可点
This commit is contained in:
yinjun.chen1
2026-09-21 23:03:39 +08:00
parent 765d9f5170
commit 72699ae422
3 changed files with 396 additions and 371 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ const fmtScanBytes = computed(() => {
</router-view>
<MobileTabbar v-if="!isLogin() && isMobile()" :kb="kbOpen" />
<SettingDrawer v-model="settingsOpen" />
<SettingDrawer v-model="settingsOpen" @rescan="doRescan" />
</template>
<style scoped>
+26 -1
View File
@@ -1,13 +1,18 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useTheme } from '../composables/useTheme'
import { useStore } from '../store'
import api from '../api'
import { storage } from '../utils/storage'
import PkgListConfigDialog from './PkgListConfigDialog.vue'
const props = defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'rescan'])
const router = useRouter()
const { theme, toggle } = useTheme()
const { state } = useStore()
const pkgConfigOpen = ref(false)
const user = storage.getJSON('vcm-user', { name: '管理员', email: 'admin@verdaccio.local' })
@@ -15,11 +20,22 @@ function close() {
emit('update:modelValue', false)
}
function goRescan() {
if (state.loading) return
close()
emit('rescan')
}
function goThresholds() {
close()
router.push('/thresholds')
}
function goPkgConfig() {
close()
pkgConfigOpen.value = true
}
async function logout() {
await api.logout() // 先销毁后端会话并清本地 token,再跳登录页
close()
@@ -46,6 +62,9 @@ async function logout() {
<span>{{ user.email }}</span>
</div>
</div>
<div class="m-drawer-item" :class="{ disabled: state.loading }" @click="goRescan">
<span style="font-family: var(--sans)">{{ state.loading ? '⏳ 扫描中…' : '🔄 重新扫描' }}</span>
</div>
<div class="m-drawer-item" @click="toggle">
<span style="font-family: var(--sans)">🌙 主题切换</span>
<span>{{ theme === 'dark' ? '暗色 ▸' : '亮色 ▸' }}</span>
@@ -54,6 +73,10 @@ async function logout() {
<span style="font-family: var(--sans)">⚙️ 预警设置</span>
<span>阈值 ▸</span>
</div>
<div class="m-drawer-item" @click="goPkgConfig">
<span style="font-family: var(--sans)">⏱️ 包清单定时配置</span>
<span>定时 ▸</span>
</div>
<div class="m-drawer-item">
<span style="font-family: var(--sans)">📦 版本信息</span>
<span>v1.0.0</span>
@@ -65,6 +88,7 @@ async function logout() {
</aside>
</Transition>
</Teleport>
<PkgListConfigDialog v-model:visible="pkgConfigOpen" />
</template>
<style scoped>
@@ -149,6 +173,7 @@ async function logout() {
transition: .15s;
}
.m-drawer-item:hover { background: var(--orange-soft); color: var(--orange); }
.m-drawer-item.disabled { opacity: .5; pointer-events: none; }
.m-drawer-item span { font-family: var(--num); font-size: 11px; color: var(--ink-faint); font-weight: 600; white-space: nowrap; }
.m-drawer-item span:first-child { font-family: var(--sans); font-size: 13px; color: var(--ink); font-weight: 600; }
.m-drawer-item:hover span:first-child { color: var(--orange); }
+369 -369
View File
@@ -1,6 +1,6 @@
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PackageDrawer from '../components/PackageDrawer.vue'
import CleanDialog from '../components/CleanDialog.vue'
import PkgListDrawer from '../components/PkgListDrawer.vue'
@@ -9,36 +9,36 @@ import PkgLogo from '../components/PkgLogo.vue'
import { useStore, fmtSize } from '../store'
import { storage } from '../utils/storage'
const { state, stats, refreshPackages, ignorePkg, unignorePkg, cleanPkg, deleteVersion, syncLatest,
const { state, refreshPackages, ignorePkg, unignorePkg, deleteVersion, syncLatest,
refreshPkgList, refreshPkgListConfig, triggerPkgPull } = useStore()
const route = useRoute()
const router = useRouter()
const keyword = ref('')
const statusFilter = ref('all') // all | warn | ok | ignored | broken
const sortMode = ref('default') // default | versions-desc | size-desc | date-desc | date-asc
const isMobile = ref(false)
let debounceTimer = null
// —— 今日更新筛选(从概览「今日更新包数」卡片跳转进入) ——
// 与后端 lastUpdated 同口径:北京时间 YYYY-MM-DD
const todayStr = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date())
const todayFilter = ref(false)
function clearTodayFilter() {
todayFilter.value = false
router.replace({ path: '/search' })
}
// —— 响应式 placeholder:移动端精简,桌面端完整 ——
function checkMobile() {
isMobile.value = window.matchMedia('(max-width: 767px)').matches
}
const placeholderText = computed(() =>
isMobile.value ? '搜索包名…(回车搜索 · 实时过滤)' : '搜索包名…(回车搜索 · 实时过滤 · 防抖 300ms)',
)
const route = useRoute()
const router = useRouter()
const keyword = ref('')
const statusFilter = ref('all') // all | warn | ok | ignored | broken
const sortMode = ref('default') // default | versions-desc | size-desc | date-desc | date-asc
const isMobile = ref(false)
let debounceTimer = null
// —— 今日更新筛选(从概览「今日更新包数」卡片跳转进入) ——
// 与后端 lastUpdated 同口径:北京时间 YYYY-MM-DD
const todayStr = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date())
const todayFilter = ref(false)
function clearTodayFilter() {
todayFilter.value = false
router.replace({ path: '/search' })
}
// —— 响应式 placeholder:移动端精简,桌面端完整 ——
function checkMobile() {
isMobile.value = window.matchMedia('(max-width: 767px)').matches
}
const placeholderText = computed(() =>
isMobile.value ? '搜索包名…(回车搜索 · 实时过滤)' : '搜索包名…(回车搜索 · 实时过滤 · 防抖 300ms)',
)
// —— 搜索历史(localStorage 最近 5 条,提交时记录) ——
const HISTORY_KEY = 'vcm-search-history'
const searchHistory = ref(storage.getJSON(HISTORY_KEY, []))
@@ -60,31 +60,31 @@ function clearHistory() {
searchHistory.value = []
storage.remove(HISTORY_KEY)
}
// —— 表格右上角:筛选/排序下拉 ——
const filterOpen = ref(false)
const sortOpen = ref(false)
function toggleFilter() {
filterOpen.value = !filterOpen.value
sortOpen.value = false
}
function toggleSort() {
sortOpen.value = !sortOpen.value
filterOpen.value = false
}
function pickFilter(key) {
statusFilter.value = key
filterOpen.value = false
}
function pickSort(mode) {
sortMode.value = mode
sortOpen.value = false
}
function closeMenus() {
filterOpen.value = false
sortOpen.value = false
}
// —— 表格右上角:筛选/排序下拉 ——
const filterOpen = ref(false)
const sortOpen = ref(false)
function toggleFilter() {
filterOpen.value = !filterOpen.value
sortOpen.value = false
}
function toggleSort() {
sortOpen.value = !sortOpen.value
filterOpen.value = false
}
function pickFilter(key) {
statusFilter.value = key
filterOpen.value = false
}
function pickSort(mode) {
sortMode.value = mode
sortOpen.value = false
}
function closeMenus() {
filterOpen.value = false
sortOpen.value = false
}
onMounted(() => {
document.addEventListener('click', closeMenus)
checkMobile()
@@ -107,12 +107,12 @@ onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
window.removeEventListener('resize', fitTableHeight)
})
watch(keyword, () => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => refreshPackages(keyword.value), 300)
})
watch(keyword, () => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => refreshPackages(keyword.value), 300)
})
const filtered = computed(() => {
let list = state.packages
if (todayFilter.value) list = list.filter(p => p.lastUpdated === todayStr)
@@ -188,37 +188,37 @@ watch(
// 否则 .vtable 无固定高度 → clientHeight=内容全高 → vEnd 扩大至全部 → 虚拟滚动退化为全量渲染
watch(vTotal, () => nextTick(fitTableHeight))
watch(searchHistory, () => nextTick(fitTableHeight))
const pills = [
{ key: 'all', label: '全部' },
{ key: 'warn', label: '🔥 预警' },
{ key: 'ok', label: '✅ 正常' },
{ key: 'ignored', label: '💤 已忽略' },
{ key: 'broken', label: '⚠️ 异常' },
]
const drawerPkg = ref(null)
const drawerVisible = ref(false)
const cleanVisible = ref(false)
const cleanPkgData = ref(null)
function openDrawer(pkg) {
// 版本明细由 PackageDrawer 打开后按需加载
drawerPkg.value = pkg
drawerVisible.value = true
}
function openClean(pkg) {
cleanPkgData.value = pkg
cleanVisible.value = true
}
async function doIgnore(pkg) {
await ignorePkg(pkg.name)
ElMessage.success(`已忽略 ${pkg.name}`)
}
async function doUnignore(pkg) {
await unignorePkg(pkg.name)
ElMessage.success(`已移出忽略 ${pkg.name}`)
}
const pills = [
{ key: 'all', label: '全部' },
{ key: 'warn', label: '🔥 预警' },
{ key: 'ok', label: '✅ 正常' },
{ key: 'ignored', label: '💤 已忽略' },
{ key: 'broken', label: '⚠️ 异常' },
]
const drawerPkg = ref(null)
const drawerVisible = ref(false)
const cleanVisible = ref(false)
const cleanPkgData = ref(null)
function openDrawer(pkg) {
// 版本明细由 PackageDrawer 打开后按需加载
drawerPkg.value = pkg
drawerVisible.value = true
}
function openClean(pkg) {
cleanPkgData.value = pkg
cleanVisible.value = true
}
async function doIgnore(pkg) {
await ignorePkg(pkg.name)
ElMessage.success(`已忽略 ${pkg.name}`)
}
async function doUnignore(pkg) {
await unignorePkg(pkg.name)
ElMessage.success(`已移出忽略 ${pkg.name}`)
}
async function doDeleteVersion(pkg, version) {
try {
await ElMessageBox.confirm(
@@ -362,38 +362,38 @@ function showFailedList() {
// 清单待拉取数量(显示在「包清单」入口角标上)
const pendingCount = computed(() => state.pkgList.items.filter(i => i.status === 'pending').length)
</script>
<template>
<div class="page-inner">
<div class="search-hero">
<div class="search-box">
<span class="s-ico">🔍</span>
<input
v-model="keyword"
type="text"
:placeholder="placeholderText"
@keyup.esc="keyword = ''"
@keyup.enter="submitSearch()"
/>
<span v-if="keyword" class="s-clear" @click="keyword = ''">✕</span>
<button class="s-go" @click="submitSearch()">搜索</button>
</div>
<!-- 最近搜索(最近 5 条,localStorage 持久化) -->
<div v-if="searchHistory.length" class="history-row">
<span class="h-label">🕘 最近搜索</span>
<span v-for="h in searchHistory" :key="h" class="h-chip" @click="useHistory(h)">{{ h }}</span>
<span class="h-clear" @click="clearHistory">清除</span>
</div>
</div>
<!-- 表格工具栏:左侧结果数 + 右侧筛选/排序下拉 -->
<div class="toolbar">
<div class="tb-count">
共找到 <b>{{ filtered.length }}</b> 个包
<template v-if="keyword">(关键词「{{ keyword }}」)</template>
<template v-if="todayFilter">(<span class="today-chip">仅今日更新 {{ todayStr }} <i @click="clearTodayFilter" title="清除筛选">✕</i></span>)</template>
</div>
<template>
<div class="page-inner">
<div class="search-hero">
<div class="search-box">
<span class="s-ico">🔍</span>
<input
v-model="keyword"
type="text"
:placeholder="placeholderText"
@keyup.esc="keyword = ''"
@keyup.enter="submitSearch()"
/>
<span v-if="keyword" class="s-clear" @click="keyword = ''">✕</span>
<button class="s-go" @click="submitSearch()">搜索</button>
</div>
<!-- 最近搜索(最近 5 条,localStorage 持久化) -->
<div v-if="searchHistory.length" class="history-row">
<span class="h-label">🕘 最近搜索</span>
<span v-for="h in searchHistory" :key="h" class="h-chip" @click="useHistory(h)">{{ h }}</span>
<span class="h-clear" @click="clearHistory">清除</span>
</div>
</div>
<!-- 表格工具栏:左侧结果数 + 右侧筛选/排序下拉 -->
<div class="toolbar">
<div class="tb-count">
共找到 <b>{{ filtered.length }}</b> 个包
<template v-if="keyword">(关键词「{{ keyword }}」)</template>
<template v-if="todayFilter">(<span class="today-chip">仅今日更新 {{ todayStr }} <i @click="clearTodayFilter" title="清除筛选">✕</i></span>)</template>
</div>
<div class="tb-actions">
<!-- npm 包清单:查看/编辑/删除入口 -->
<button
@@ -426,36 +426,36 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
@click.stop="doSyncLatest"
>{{ state.syncProgress.running ? '⏳' : '📥' }}</button>
<div class="dd">
<button class="dd-btn" :class="{ on: filterOpen }" title="快捷筛选" @click.stop="toggleFilter">🎚️</button>
<Transition name="dd">
<div v-if="filterOpen" class="dd-menu">
<div class="dd-title">快捷筛选</div>
<div
v-for="p in pills"
:key="p.key"
class="dd-item"
:class="{ on: statusFilter === p.key }"
@click="pickFilter(p.key)"
>{{ p.label }}<span v-if="statusFilter === p.key" class="dd-check">✓</span></div>
</div>
</Transition>
</div>
<div class="dd">
<button class="dd-btn" :class="{ on: sortOpen }" title="排序" @click.stop="toggleSort">↕️</button>
<Transition name="dd">
<div v-if="sortOpen" class="dd-menu">
<div class="dd-title">排序方式</div>
<div class="dd-item" :class="{ on: sortMode === 'default' }" @click="pickSort('default')">默认<span v-if="sortMode === 'default'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'versions-desc' }" @click="pickSort('versions-desc')">版本数 ↓<span v-if="sortMode === 'versions-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'size-desc' }" @click="pickSort('size-desc')">占用大小 ↓<span v-if="sortMode === 'size-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'date-desc' }" @click="pickSort('date-desc')">最近更新 ↓<span v-if="sortMode === 'date-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'date-asc' }" @click="pickSort('date-asc')">最近更新 ↑<span v-if="sortMode === 'date-asc'" class="dd-check">✓</span></div>
</div>
</Transition>
</div>
</div>
</div>
<button class="dd-btn" :class="{ on: filterOpen }" title="快捷筛选" @click.stop="toggleFilter">🎚️</button>
<Transition name="dd">
<div v-if="filterOpen" class="dd-menu">
<div class="dd-title">快捷筛选</div>
<div
v-for="p in pills"
:key="p.key"
class="dd-item"
:class="{ on: statusFilter === p.key }"
@click="pickFilter(p.key)"
>{{ p.label }}<span v-if="statusFilter === p.key" class="dd-check">✓</span></div>
</div>
</Transition>
</div>
<div class="dd">
<button class="dd-btn" :class="{ on: sortOpen }" title="排序" @click.stop="toggleSort">↕️</button>
<Transition name="dd">
<div v-if="sortOpen" class="dd-menu">
<div class="dd-title">排序方式</div>
<div class="dd-item" :class="{ on: sortMode === 'default' }" @click="pickSort('default')">默认<span v-if="sortMode === 'default'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'versions-desc' }" @click="pickSort('versions-desc')">版本数 ↓<span v-if="sortMode === 'versions-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'size-desc' }" @click="pickSort('size-desc')">占用大小 ↓<span v-if="sortMode === 'size-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'date-desc' }" @click="pickSort('date-desc')">最近更新 ↓<span v-if="sortMode === 'date-desc'" class="dd-check">✓</span></div>
<div class="dd-item" :class="{ on: sortMode === 'date-asc' }" @click="pickSort('date-asc')">最近更新 ↑<span v-if="sortMode === 'date-asc'" class="dd-check">✓</span></div>
</div>
</Transition>
</div>
</div>
</div>
<div v-if="state.loading" class="loading-tip">⏳ 加载中…</div>
<div v-else ref="vWrapEl" class="vtable" @scroll.passive="onVScroll">
<table>
@@ -506,17 +506,17 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
</table>
<div v-if="!filtered.length" class="empty-tip">没有匹配的包</div>
</div>
</div>
<PackageDrawer
v-model:visible="drawerVisible"
:pkg="drawerPkg"
:threshold="state.threshold.versionThreshold"
@clean="openClean"
@ignore="doIgnore"
@unignore="doUnignore"
@delete-version="doDeleteVersion"
/>
</div>
<PackageDrawer
v-model:visible="drawerVisible"
:pkg="drawerPkg"
:threshold="state.threshold.versionThreshold"
@clean="openClean"
@ignore="doIgnore"
@unignore="doUnignore"
@delete-version="doDeleteVersion"
/>
<CleanDialog
:visible="cleanVisible"
:pkg="cleanPkgData"
@@ -526,146 +526,146 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
/>
<PkgListDrawer v-model:visible="pkgListOpen" />
</template>
<style scoped>
.search-hero { padding: 18px 0 12px; display: flex; flex-direction: column; align-items: center; }
.search-box {
display: flex;
align-items: center;
gap: 12px;
width: min(680px, 100%);
height: 52px;
background: var(--card);
border: 2px solid var(--line);
border-radius: 99px;
padding: 0 10px 0 22px;
box-shadow: 0 14px 34px -14px rgba(180, 120, 60, .25);
transition: .2s;
}
.search-box:focus-within {
border-color: var(--orange);
box-shadow: 0 0 0 4px rgba(255, 140, 0, .14), 0 14px 34px -14px rgba(255, 140, 0, .3);
}
.s-ico { font-size: 18px; color: var(--ink-faint); }
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 14.5px;
font-family: var(--sans);
color: var(--ink);
background: transparent;
min-width: 0;
}
.search-box input::placeholder { color: var(--ink-faint); }
.s-clear {
width: 26px;
height: 26px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--num-bg);
color: var(--ink-dim);
font-size: 11px;
cursor: pointer;
flex-shrink: 0;
}
.s-clear:hover { background: var(--red-soft); color: var(--red); }
.s-go {
display: inline-flex;
align-items: center;
gap: 7px;
height: 38px;
padding: 0 22px;
border: none;
border-radius: 99px;
cursor: pointer;
background: var(--grad-main);
color: #fff;
font-size: 13.5px;
font-weight: 600;
box-shadow: 0 8px 18px -6px rgba(255, 140, 0, .6);
transition: .18s;
flex-shrink: 0;
}
.s-go:hover { transform: translateY(-1px); }
/* 最近搜索(搜索框下方,最近 5 条) */
.history-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 14px;
width: min(680px, 100%);
}
.h-label { font-size: 12px; color: var(--ink-faint); font-weight: 600; }
.h-chip {
font-size: 12px;
color: var(--ink-dim);
background: var(--card);
border: 1.5px solid var(--line);
padding: 5px 13px;
border-radius: 99px;
cursor: pointer;
transition: .15s;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.h-chip:hover { border-color: color-mix(in srgb, var(--orange) 60%, transparent); color: var(--orange); }
.h-clear { font-size: 11.5px; color: var(--ink-faint); cursor: pointer; margin-left: 2px; }
.h-clear:hover { color: var(--red); }
/* 表格工具栏:左结果数 + 右筛选/排序下拉 */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 20px 0 12px;
}
.tb-count {
font-family: var(--num);
font-size: 12.5px;
color: var(--ink-faint);
font-weight: 600;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tb-count b { color: var(--orange); }
.today-chip { color: var(--orange); font-weight: 600; }
.today-chip i {
display: inline-grid;
place-items: center;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--orange-soft);
color: var(--orange);
font-style: normal;
font-size: 9px;
cursor: pointer;
margin-left: 4px;
vertical-align: 1px;
}
.today-chip i:hover { background: var(--orange); color: #fff; }
.tb-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.dd { position: relative; }
.dd-btn {
width: 36px;
height: 36px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 16px;
cursor: pointer;
background: var(--card);
border: 1.5px solid var(--line);
transition: .15s;
}
<style scoped>
.search-hero { padding: 18px 0 12px; display: flex; flex-direction: column; align-items: center; }
.search-box {
display: flex;
align-items: center;
gap: 12px;
width: min(680px, 100%);
height: 52px;
background: var(--card);
border: 2px solid var(--line);
border-radius: 99px;
padding: 0 10px 0 22px;
box-shadow: 0 14px 34px -14px rgba(180, 120, 60, .25);
transition: .2s;
}
.search-box:focus-within {
border-color: var(--orange);
box-shadow: 0 0 0 4px rgba(255, 140, 0, .14), 0 14px 34px -14px rgba(255, 140, 0, .3);
}
.s-ico { font-size: 18px; color: var(--ink-faint); }
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 14.5px;
font-family: var(--sans);
color: var(--ink);
background: transparent;
min-width: 0;
}
.search-box input::placeholder { color: var(--ink-faint); }
.s-clear {
width: 26px;
height: 26px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--num-bg);
color: var(--ink-dim);
font-size: 11px;
cursor: pointer;
flex-shrink: 0;
}
.s-clear:hover { background: var(--red-soft); color: var(--red); }
.s-go {
display: inline-flex;
align-items: center;
gap: 7px;
height: 38px;
padding: 0 22px;
border: none;
border-radius: 99px;
cursor: pointer;
background: var(--grad-main);
color: #fff;
font-size: 13.5px;
font-weight: 600;
box-shadow: 0 8px 18px -6px rgba(255, 140, 0, .6);
transition: .18s;
flex-shrink: 0;
}
.s-go:hover { transform: translateY(-1px); }
/* 最近搜索(搜索框下方,最近 5 条) */
.history-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 14px;
width: min(680px, 100%);
}
.h-label { font-size: 12px; color: var(--ink-faint); font-weight: 600; }
.h-chip {
font-size: 12px;
color: var(--ink-dim);
background: var(--card);
border: 1.5px solid var(--line);
padding: 5px 13px;
border-radius: 99px;
cursor: pointer;
transition: .15s;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.h-chip:hover { border-color: color-mix(in srgb, var(--orange) 60%, transparent); color: var(--orange); }
.h-clear { font-size: 11.5px; color: var(--ink-faint); cursor: pointer; margin-left: 2px; }
.h-clear:hover { color: var(--red); }
/* 表格工具栏:左结果数 + 右筛选/排序下拉 */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 20px 0 12px;
}
.tb-count {
font-family: var(--num);
font-size: 12.5px;
color: var(--ink-faint);
font-weight: 600;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tb-count b { color: var(--orange); }
.today-chip { color: var(--orange); font-weight: 600; }
.today-chip i {
display: inline-grid;
place-items: center;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--orange-soft);
color: var(--orange);
font-style: normal;
font-size: 9px;
cursor: pointer;
margin-left: 4px;
vertical-align: 1px;
}
.today-chip i:hover { background: var(--orange); color: #fff; }
.tb-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.dd { position: relative; }
.dd-btn {
width: 36px;
height: 36px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 16px;
cursor: pointer;
background: var(--card);
border: 1.5px solid var(--line);
transition: .15s;
}
.dd-btn:hover { border-color: color-mix(in srgb, var(--orange) 60%, transparent); }
.dd-btn.on { background: var(--green); border-color: var(--green); box-shadow: 0 6px 14px -4px rgba(50, 205, 50, .5); }
.sync-btn { font-size: 17px; }
@@ -714,38 +714,38 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
z-index: 2;
}
.fail-badge:hover { transform: scale(1.12); }
.dd-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 168px;
background: var(--card);
border: 1.5px solid var(--line);
border-radius: 14px;
box-shadow: 0 18px 44px -14px rgba(0, 0, 0, .5);
padding: 7px;
z-index: 60;
}
.dd-title { font-size: 11px; color: var(--ink-faint); font-weight: 700; padding: 6px 10px 4px; letter-spacing: .3px; }
.dd-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-dim);
padding: 8px 10px;
border-radius: 9px;
cursor: pointer;
transition: .13s;
}
.dd-item:hover { background: var(--num-bg); color: var(--ink); }
.dd-item.on { color: var(--green-deep); background: var(--green-soft); }
.dd-check { font-weight: 800; }
.dd-enter-active { transition: transform .16s cubic-bezier(.2, .8, .25, 1), opacity .16s; }
.dd-leave-active { transition: transform .12s ease-in, opacity .12s; }
.dd-enter-from, .dd-leave-to { transform: translateY(-6px) scale(.97); opacity: 0; transform-origin: top right; }
.dd-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 168px;
background: var(--card);
border: 1.5px solid var(--line);
border-radius: 14px;
box-shadow: 0 18px 44px -14px rgba(0, 0, 0, .5);
padding: 7px;
z-index: 60;
}
.dd-title { font-size: 11px; color: var(--ink-faint); font-weight: 700; padding: 6px 10px 4px; letter-spacing: .3px; }
.dd-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-dim);
padding: 8px 10px;
border-radius: 9px;
cursor: pointer;
transition: .13s;
}
.dd-item:hover { background: var(--num-bg); color: var(--ink); }
.dd-item.on { color: var(--green-deep); background: var(--green-soft); }
.dd-check { font-weight: 800; }
.dd-enter-active { transition: transform .16s cubic-bezier(.2, .8, .25, 1), opacity .16s; }
.dd-leave-active { transition: transform .12s ease-in, opacity .12s; }
.dd-enter-from, .dd-leave-to { transform: translateY(-6px) scale(.97); opacity: 0; transform-origin: top right; }
.pkg-name { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.pkg-name .pkg-cell { min-width: 0; }
.pkg-cell { font-weight: 600; word-break: break-all; }
@@ -763,33 +763,33 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
.vtr-ph td { padding: 0; border-bottom: none; }
.vtr-ph { cursor: default; }
.vtr-ph:hover { background: transparent; }
.pkg-desc {
display: block;
max-width: 230px;
font-size: 12px;
color: var(--ink-dim);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.num-cell.over { color: var(--red); }
/* 操作列:按钮横排一行,不换行 */
.vtable td:last-child { white-space: nowrap; }
.vtable td:last-child .btn { padding: 6px 13px; font-size: 12px; }
.vtable td:last-child .btn + .btn { margin-left: 7px; }
.empty-tip { padding: 40px 0; text-align: center; color: var(--ink-faint); font-size: 12.5px; }
.loading-tip { text-align: center; padding: 40px 0; color: var(--ink-faint); font-size: 13px; }
@media (max-width: 767px) {
.search-box { height: 48px; }
.search-box .s-go { display: none; }
.history-row { width: 100%; }
.toolbar { margin: 16px 0 10px; }
.vtable { overflow: auto; -webkit-overflow-scrolling: touch; }
.vtable table { min-width: 880px; width: auto; }
.vtable th:first-child,
.vtable td:first-child { min-width: 220px; }
.pkg-cell { word-break: break-all; }
}
</style>
.pkg-desc {
display: block;
max-width: 230px;
font-size: 12px;
color: var(--ink-dim);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.num-cell.over { color: var(--red); }
/* 操作列:按钮横排一行,不换行 */
.vtable td:last-child { white-space: nowrap; }
.vtable td:last-child .btn { padding: 6px 13px; font-size: 12px; }
.vtable td:last-child .btn + .btn { margin-left: 7px; }
.empty-tip { padding: 40px 0; text-align: center; color: var(--ink-faint); font-size: 12.5px; }
.loading-tip { text-align: center; padding: 40px 0; color: var(--ink-faint); font-size: 13px; }
@media (max-width: 767px) {
.search-box { height: 48px; }
.search-box .s-go { display: none; }
.history-row { width: 100%; }
.toolbar { margin: 16px 0 10px; }
.vtable { overflow: auto; -webkit-overflow-scrolling: touch; }
.vtable table { min-width: 880px; width: auto; }
.vtable th:first-child,
.vtable td:first-child { min-width: 220px; }
.pkg-cell { word-break: break-all; }
}
</style>