。。。。。。
This commit is contained in:
@@ -97,6 +97,10 @@ onMounted(() => {
|
|||||||
if (route.query.updatedToday === '1') todayFilter.value = true
|
if (route.query.updatedToday === '1') todayFilter.value = true
|
||||||
refreshPkgList()
|
refreshPkgList()
|
||||||
refreshPkgListConfig()
|
refreshPkgListConfig()
|
||||||
|
// 挂载兜底:首次进入(含已有缓存数据)确保限高生效;容器未就绪时推迟到下一帧重试
|
||||||
|
nextTick(() => {
|
||||||
|
if (!fitTableHeight()) requestAnimationFrame(fitTableHeight)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', closeMenus)
|
document.removeEventListener('click', closeMenus)
|
||||||
@@ -137,8 +141,13 @@ const ROW_H = 58 // 单行高度(px):td padding 14×2 + 内容 30(logo
|
|||||||
const OVERSCAN = 6 // 可视区外多渲染的缓冲行数
|
const OVERSCAN = 6 // 可视区外多渲染的缓冲行数
|
||||||
const vWrapEl = ref(null)
|
const vWrapEl = ref(null)
|
||||||
const vScrollTop = ref(0)
|
const vScrollTop = ref(0)
|
||||||
|
// 容器可视区高度(响应式):限高/缩放后必须同步刷新,vEnd 依赖它重算渲染窗口
|
||||||
|
const vViewH = ref(600)
|
||||||
function onVScroll() {
|
function onVScroll() {
|
||||||
vScrollTop.value = vWrapEl.value ? vWrapEl.value.scrollTop : 0
|
if (vWrapEl.value) {
|
||||||
|
vScrollTop.value = vWrapEl.value.scrollTop
|
||||||
|
vViewH.value = vWrapEl.value.clientHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const vTotal = computed(() => filtered.value.length)
|
const vTotal = computed(() => filtered.value.length)
|
||||||
const vStart = computed(() => {
|
const vStart = computed(() => {
|
||||||
@@ -146,8 +155,7 @@ const vStart = computed(() => {
|
|||||||
return Math.max(0, s)
|
return Math.max(0, s)
|
||||||
})
|
})
|
||||||
const vEnd = computed(() => {
|
const vEnd = computed(() => {
|
||||||
const viewH = vWrapEl.value ? vWrapEl.value.clientHeight : 600
|
const e = Math.ceil((vScrollTop.value + vViewH.value) / ROW_H) + OVERSCAN
|
||||||
const e = Math.ceil((vScrollTop.value + viewH) / ROW_H) + OVERSCAN
|
|
||||||
return Math.min(vTotal.value, e)
|
return Math.min(vTotal.value, e)
|
||||||
})
|
})
|
||||||
const visibleRows = computed(() => filtered.value.slice(vStart.value, vEnd.value))
|
const visibleRows = computed(() => filtered.value.slice(vStart.value, vEnd.value))
|
||||||
@@ -164,9 +172,11 @@ watch(
|
|||||||
// —— 表格容器限高:虚拟滚动依赖容器自身滚动,按视口剩余空间动态撑满 ——
|
// —— 表格容器限高:虚拟滚动依赖容器自身滚动,按视口剩余空间动态撑满 ——
|
||||||
function fitTableHeight() {
|
function fitTableHeight() {
|
||||||
const el = vWrapEl.value
|
const el = vWrapEl.value
|
||||||
if (!el) return
|
if (!el) return false
|
||||||
const top = el.getBoundingClientRect().top
|
const top = el.getBoundingClientRect().top
|
||||||
el.style.height = `${Math.max(180, Math.floor(window.innerHeight - top - 14))}px`
|
el.style.height = `${Math.max(180, Math.floor(window.innerHeight - top - 14))}px`
|
||||||
|
vViewH.value = el.clientHeight
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
() => state.loading,
|
() => state.loading,
|
||||||
@@ -174,6 +184,9 @@ watch(
|
|||||||
if (!v) nextTick(fitTableHeight)
|
if (!v) nextTick(fitTableHeight)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// 列表数据量变化后重新适配容器高度:数据已在 App 级缓存、loading 不翻转的场景也必须生效,
|
||||||
|
// 否则 .vtable 无固定高度 → clientHeight=内容全高 → vEnd 扩大至全部 → 虚拟滚动退化为全量渲染
|
||||||
|
watch(vTotal, () => nextTick(fitTableHeight))
|
||||||
watch(searchHistory, () => nextTick(fitTableHeight))
|
watch(searchHistory, () => nextTick(fitTableHeight))
|
||||||
|
|
||||||
const pills = [
|
const pills = [
|
||||||
@@ -228,20 +241,18 @@ async function onCleanConfirmed(res) {
|
|||||||
// —— 一键拉取存量包最新版本 ——
|
// —— 一键拉取存量包最新版本 ——
|
||||||
const summaryShown = ref(false)
|
const summaryShown = ref(false)
|
||||||
|
|
||||||
const syncConfirmOpen = ref(false)
|
async function doSyncLatest() {
|
||||||
const syncResultOpen = ref(false)
|
const sp = state.syncProgress
|
||||||
const syncResultDetail = ref('')
|
if (sp.running) return
|
||||||
const syncResultFailed = ref(false)
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
function openSyncConfirm() {
|
'将查询上游 npm registry,为存量包拉取每个包的最新版本(已缓存最新版会自动跳过)。',
|
||||||
if (state.syncProgress.running) return
|
'拉取最新版本',
|
||||||
syncConfirmOpen.value = true
|
{ type: 'info', confirmButtonText: '开始拉取', cancelButtonText: '取消', closeOnClickModal: false },
|
||||||
}
|
)
|
||||||
function closeSyncConfirm() {
|
} catch {
|
||||||
syncConfirmOpen.value = false
|
return // 用户取消
|
||||||
}
|
}
|
||||||
async function confirmSyncLatest() {
|
|
||||||
closeSyncConfirm()
|
|
||||||
summaryShown.value = false
|
summaryShown.value = false
|
||||||
const res = await syncLatest()
|
const res = await syncLatest()
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
@@ -252,10 +263,6 @@ async function confirmSyncLatest() {
|
|||||||
// 任务异步执行,完成后由下方 watch 弹出汇总
|
// 任务异步执行,完成后由下方 watch 弹出汇总
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSyncResult() {
|
|
||||||
syncResultOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.syncProgress.running,
|
() => state.syncProgress.running,
|
||||||
async (running, prev) => {
|
async (running, prev) => {
|
||||||
@@ -267,17 +274,18 @@ watch(
|
|||||||
detail.push(`❌ 拉取失败 ${sp.failed} 个`)
|
detail.push(`❌ 拉取失败 ${sp.failed} 个`)
|
||||||
if (sp.failedList.length) detail.push(`失败明细:\n${sp.failedList.map(f => `· ${f}`).join('\n')}`)
|
if (sp.failedList.length) detail.push(`失败明细:\n${sp.failedList.map(f => `· ${f}`).join('\n')}`)
|
||||||
}
|
}
|
||||||
syncResultDetail.value = detail.join('\n')
|
try {
|
||||||
syncResultFailed.value = !!sp.failed
|
await ElMessageBox.alert(detail.join('\n'), sp.failed ? '拉取完成(部分失败)' : '拉取完成', {
|
||||||
syncResultOpen.value = true
|
confirmButtonText: '知道了',
|
||||||
|
type: sp.failed ? 'warning' : 'success',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
/* 用户关闭弹窗无需处理 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async function doSyncLatest() {
|
|
||||||
openSyncConfirm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// —— npm 包清单:查看/编辑入口 + 按清单手动拉取(后台执行,失败自动重试 3 次) ——
|
// —— npm 包清单:查看/编辑入口 + 按清单手动拉取(后台执行,失败自动重试 3 次) ——
|
||||||
const pkgListOpen = ref(false)
|
const pkgListOpen = ref(false)
|
||||||
const pullLoading = ref(null)
|
const pullLoading = ref(null)
|
||||||
@@ -416,9 +424,7 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
|
|||||||
:class="{ on: state.syncProgress.running }"
|
:class="{ on: state.syncProgress.running }"
|
||||||
:title="state.syncProgress.running ? `正在拉取… ${state.syncProgress.processed}/${state.syncProgress.total}` : '一键拉取存量包最新版本'"
|
:title="state.syncProgress.running ? `正在拉取… ${state.syncProgress.processed}/${state.syncProgress.total}` : '一键拉取存量包最新版本'"
|
||||||
@click.stop="doSyncLatest"
|
@click.stop="doSyncLatest"
|
||||||
>
|
>{{ state.syncProgress.running ? '⏳' : '📥' }}</button>
|
||||||
<span :class="state.syncProgress.running ? 'sync-spin' : ''">{{ state.syncProgress.running ? '⏳' : '📥' }}</span>
|
|
||||||
</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">
|
||||||
@@ -519,33 +525,6 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
|
|||||||
@confirmed="onCleanConfirmed"
|
@confirmed="onCleanConfirmed"
|
||||||
/>
|
/>
|
||||||
<PkgListDrawer v-model:visible="pkgListOpen" />
|
<PkgListDrawer v-model:visible="pkgListOpen" />
|
||||||
|
|
||||||
<!-- 一键拉取确认弹窗(统一风格) -->
|
|
||||||
<Transition name="dd">
|
|
||||||
<div v-if="syncConfirmOpen" class="sync-mask" @click.self="closeSyncConfirm">
|
|
||||||
<div class="sync-confirm">
|
|
||||||
<div class="sync-confirm-title">拉取最新版本</div>
|
|
||||||
<div class="sync-confirm-body">将查询上游 npm registry,为存量包拉取每个包的最新版本(已缓存最新版会自动跳过)。</div>
|
|
||||||
<div class="sync-confirm-ops">
|
|
||||||
<button class="pld-btn" @click="closeSyncConfirm">取消</button>
|
|
||||||
<button class="pld-btn primary" @click="confirmSyncLatest">开始拉取</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- 拉取结果弹窗 -->
|
|
||||||
<Transition name="dd">
|
|
||||||
<div v-if="syncResultOpen" class="sync-mask" @click.self="closeSyncResult">
|
|
||||||
<div class="sync-result" :class="{ warn: syncResultFailed }">
|
|
||||||
<div class="sync-result-title">{{ syncResultFailed ? '拉取完成(部分失败)' : '拉取完成' }}</div>
|
|
||||||
<pre class="sync-result-body">{{ syncResultDetail }}</pre>
|
|
||||||
<div class="sync-result-ops">
|
|
||||||
<button class="pld-btn primary" @click="closeSyncResult">知道了</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -688,10 +667,9 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
|
|||||||
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:not(.sync-btn):not(.pull-btn) { 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; }
|
||||||
.sync-btn.on { background: transparent; border-color: var(--line); box-shadow: none; }
|
.sync-btn.on { animation: sync-spin 1.1s linear infinite; }
|
||||||
.sync-btn.on .sync-spin { animation: sync-spin 1.1s linear infinite; }
|
|
||||||
@keyframes sync-spin {
|
@keyframes sync-spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
@@ -803,54 +781,6 @@ const pendingCount = computed(() => state.pkgList.items.filter(i => i.status ===
|
|||||||
.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; }
|
||||||
|
|
||||||
/* —— 一键拉取弹窗(统一风格) —— */
|
|
||||||
.sync-mask {
|
|
||||||
position: fixed; inset: 0; z-index: 500;
|
|
||||||
display: grid; place-items: center;
|
|
||||||
background: rgba(18, 16, 14, .38);
|
|
||||||
backdrop-filter: blur(14px) saturate(1.3);
|
|
||||||
}
|
|
||||||
html[data-theme="bright"] .sync-mask {
|
|
||||||
background: rgba(244, 242, 239, .45);
|
|
||||||
backdrop-filter: blur(14px) saturate(1.3);
|
|
||||||
}
|
|
||||||
.sync-confirm, .sync-result {
|
|
||||||
width: min(420px, 92vw);
|
|
||||||
background: var(--card);
|
|
||||||
border: 1.5px solid var(--line);
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 30px 70px -20px rgba(0, 0, 0, .5);
|
|
||||||
padding: 22px 24px;
|
|
||||||
display: flex; flex-direction: column; gap: 14px;
|
|
||||||
}
|
|
||||||
html[data-theme="bright"] .sync-confirm,
|
|
||||||
html[data-theme="bright"] .sync-result {
|
|
||||||
box-shadow: 0 30px 70px -20px rgba(0, 0, 0, .22);
|
|
||||||
}
|
|
||||||
.sync-confirm-title, .sync-result-title { font-size: 15px; font-weight: 800; }
|
|
||||||
.sync-confirm-body { font-size: 13px; color: var(--ink-dim); line-height: 1.6; }
|
|
||||||
.sync-result-body {
|
|
||||||
font-size: 12.5px; color: var(--ink-dim); line-height: 1.7;
|
|
||||||
white-space: pre-wrap; max-height: 50vh; overflow: auto;
|
|
||||||
padding: 10px 12px; border-radius: 10px; background: var(--panel-soft);
|
|
||||||
}
|
|
||||||
.sync-confirm-ops, .sync-result-ops {
|
|
||||||
display: flex; justify-content: flex-end; gap: 10px;
|
|
||||||
}
|
|
||||||
.sync-result.warn .sync-result-title { color: var(--red); }
|
|
||||||
|
|
||||||
/* —— 弹窗按钮(与 PkgListDrawer 统一风格) —— */
|
|
||||||
.pld-btn {
|
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
|
||||||
height: 36px; padding: 0 16px; border-radius: 12px;
|
|
||||||
border: 1.5px solid var(--line); background: var(--card);
|
|
||||||
color: var(--ink-dim); font-size: 12.5px; font-weight: 600;
|
|
||||||
cursor: pointer; transition: .15s;
|
|
||||||
}
|
|
||||||
.pld-btn:hover { border-color: color-mix(in srgb, var(--orange) 60%, transparent); color: var(--orange); }
|
|
||||||
.pld-btn.primary { background: var(--grad-main); border-color: transparent; color: #fff; box-shadow: 0 8px 18px -6px rgba(255, 140, 0, .55); }
|
|
||||||
.pld-btn.primary:hover { transform: translateY(-1px); }
|
|
||||||
|
|
||||||
@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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user