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>
|
||||
|
||||
<MobileTabbar v-if="!isLogin() && isMobile()" :kb="kbOpen" />
|
||||
<SettingDrawer v-model="settingsOpen" />
|
||||
<SettingDrawer v-model="settingsOpen" @rescan="doRescan" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user