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