topfans/frontend/pages/profile/cache-cleanup-detail.vue
2026-07-29 16:03:33 +08:00

431 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="cache-cleanup-detail">
<!-- 顶部类目信息 -->
<view class="header">
<text class="header-label">{{ category.label }}</text>
<text v-if="category.description" class="header-desc">{{ category.description }}</text>
</view>
<!-- 加载占位 -->
<view v-if="loading && !category.label" class="loading">
<text>加载中...</text>
</view>
<!-- 简单模板preload / progress / sandbox-tmp / others -->
<view v-else-if="breakdown === null" class="simple-body">
<view class="big-stat">
<text class="big-value">{{ formatSize(category.sizeBytes) }}</text>
<text class="big-count">共 {{ category.keyCount }} 项缓存</text>
</view>
<view class="action-wrap">
<button
class="clean-btn"
:disabled="category.sizeBytes <= 0"
@click="confirmSimple"
>
清 理 缓 存
</button>
<text v-if="category.sizeBytes <= 0" class="action-hint">当前无缓存可清理</text>
</view>
</view>
<!-- 分组模板draft / guide -->
<view v-else class="grouped-body">
<view v-if="breakdown.length === 0" class="empty">
<text>✓ 当前无缓存可清理</text>
</view>
<view
v-for="(g, idx) in breakdown"
:key="String(g.uid) + '-' + idx"
class="group-card"
:class="{ 'group-card-global': g.global }"
>
<view class="group-main">
<view class="group-title">
<text class="group-icon">{{ g.isCurrent ? '👤' : (g.global ? '🌐' : '👥') }}</text>
<text class="group-name">{{ g.displayUid }}</text>
</view>
<text class="group-size">{{ formatSize(g.sizeBytes) }} · {{ g.keyCount }} 项</text>
<text v-if="g.warning" class="group-warn"></text>
</view>
<button
class="clean-btn small"
:class="{ 'strong': !g.isCurrent && !g.global }"
:disabled="!g.canClean"
@click="confirmGroup(g)"
>
{{ g.canClean ? '清 理' : disabledLabel(g.disabledReason) }}
</button>
</view>
</view>
<!-- 通用确认弹窗 -->
<ConfirmModal
:visible="confirmModal.visible"
:title="confirmModal.title"
:content="confirmModal.content"
:confirmText="confirmModal.confirmText"
@confirm="onConfirmModal"
@cancel="onCancelModal"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import {
getCacheInfo,
getCategoryBreakdown,
cleanCategory,
cleanCategoryGroup,
formatSize,
} from '@/utils/cacheManager'
import ConfirmModal from '@/components/ConfirmModal.vue'
const id = ref(null)
// 当前类目元信息label / description / sizeBytes / keyCount / warning
const category = ref({ label: '', description: '', sizeBytes: 0, keyCount: 0, warning: false })
// null = 简单模板;[] / Array = 分组模板
const breakdown = ref(null)
const loading = ref(false)
// 确认弹窗状态
const confirmModal = ref({
visible: false,
title: '',
content: '',
confirmText: '确认',
pendingType: null, // 'simple' | 'group'
pendingGroup: null,
})
onLoad((options) => {
id.value = options?.id || null
load()
})
async function load() {
if (!id.value) {
uni.showToast({ title: '参数错误', icon: 'none' })
return
}
loading.value = true
try {
const [info, br] = await Promise.all([
Promise.race([
getCacheInfo(),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000)),
]),
Promise.race([
getCategoryBreakdown(id.value),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000)),
]),
])
const meta = (info.categories || []).find((c) => c.id === id.value)
if (meta) {
category.value = {
label: meta.label,
description: meta.description || '',
sizeBytes: meta.sizeBytes,
keyCount: meta.keyCount,
warning: !!meta.warning,
}
}
breakdown.value = br // null = simple; array = grouped
} catch (e) {
console.warn('[cache-cleanup-detail] load failed:', e.message)
uni.showToast({ title: '加载失败,请下拉重试', icon: 'none' })
} finally {
loading.value = false
}
}
onPullDownRefresh(async () => {
await load()
uni.stopPullDownRefresh()
})
function confirmSimple() {
if (category.value.sizeBytes <= 0) {
uni.showToast({ title: '该分类暂无缓存', icon: 'none' })
return
}
confirmModal.value = {
visible: true,
title: '清理缓存',
content: category.value.warning
? '将清空该分类缓存,是否继续?'
: '确认清理该分类缓存?',
confirmText: '确认清理',
pendingType: 'simple',
pendingGroup: null,
}
}
function confirmGroup(g) {
if (!g.canClean) {
uni.showToast({ title: disabledLabel(g.disabledReason), icon: 'none' })
return
}
let content
if (g.isCurrent) {
// 普通警告:自己的数据
content = '将清空你未提交的数据,是否继续?'
} else if (g.uid === '__legacy__') {
// 老版本数据:强警告
content = '将清空老版本数据,是否继续?'
} else if (g.global) {
// 全局标记:理论上 canClean=false 不会到这里,兜底
return
} else {
// 其他用户:强警告
content = `将清空 ${g.displayUid} 的数据,对方下次登录不会看到。是否继续?`
}
confirmModal.value = {
visible: true,
title: '清理缓存',
content,
confirmText: '确认清理',
pendingType: 'group',
pendingGroup: g,
}
}
function onConfirmModal() {
const type = confirmModal.value.pendingType
const grp = confirmModal.value.pendingGroup
confirmModal.value.visible = false
if (type === 'simple') {
doCleanSimple()
} else if (type === 'group') {
doCleanGroup(grp)
}
}
function onCancelModal() {
confirmModal.value.visible = false
}
async function doCleanSimple() {
if (!id.value) return
uni.showLoading({ title: '清理中...' })
try {
const r = await cleanCategory(id.value)
uni.hideLoading()
const freed = r?.freedBytes || 0
const cnt = r?.keyCount || 0
uni.showToast({ title: `已清理 ${formatSize(freed)} (${cnt} 项)`, icon: 'none' })
setTimeout(() => uni.navigateBack(), 600)
} catch (e) {
uni.hideLoading()
console.warn('[cache-cleanup-detail] clean failed:', e.message)
uni.showToast({ title: '清理失败', icon: 'none' })
}
}
async function doCleanGroup(g) {
if (!id.value || !g) return
// 映射 breakdown.uid → cleanCategoryGroup opts.uid
// self → 'self'
// __legacy__ → '__legacy__'
// 其他 → 直接传具体 uid
let uidParam
if (g.isCurrent) {
uidParam = 'self'
} else if (g.uid === '__global__') {
// 全局数据不可清理(兜底,正常情况下 canClean=false 不会到这里)
uni.showToast({ title: '全局数据不可清理', icon: 'none' })
return
} else {
uidParam = g.uid
}
uni.showLoading({ title: '清理中...' })
try {
const r = await cleanCategoryGroup(id.value, { uid: uidParam })
uni.hideLoading()
const freed = r?.freedBytes || 0
const cnt = r?.keyCount || 0
uni.showToast({ title: `已清理 ${formatSize(freed)} (${cnt} 项)`, icon: 'none' })
setTimeout(() => uni.navigateBack(), 600)
} catch (e) {
uni.hideLoading()
console.warn('[cache-cleanup-detail] cleanGroup failed:', e.message)
uni.showToast({ title: '清理失败', icon: 'none' })
}
}
function disabledLabel(reason) {
switch (reason) {
case 'logged-out': return '请先登录'
case 'empty': return '暂无数据'
case 'guide-current': return '保留中'
case 'guide-global': return '全局保留'
default: return '不可清理'
}
}
</script>
<style scoped>
.cache-cleanup-detail {
min-height: 100vh;
background: #f8f8f8;
padding-bottom: 80rpx;
}
/* 顶部类目信息 */
.header {
background: #fff;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.header-label {
font-size: 36rpx;
font-weight: 600;
color: #333;
display: block;
}
.header-desc {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
/* 加载占位 */
.loading {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 28rpx;
}
/* 简单模板 */
.simple-body {
background: #fff;
margin-top: 20rpx;
padding: 48rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.big-stat {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 48rpx;
}
.big-value {
font-size: 72rpx;
font-weight: 600;
color: #1890ff;
line-height: 1.2;
}
.big-count {
font-size: 26rpx;
color: #999;
margin-top: 12rpx;
}
.action-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
width: 100%;
}
.clean-btn {
width: 480rpx;
height: 88rpx;
line-height: 88rpx;
background: #1890ff;
color: #fff;
font-size: 30rpx;
border-radius: 44rpx;
border: 0;
}
.clean-btn::after { border: 0; }
.clean-btn[disabled] {
background: #d9d9d9;
color: #fff;
}
.action-hint {
font-size: 22rpx;
color: #999;
}
/* 分组模板 */
.grouped-body {
background: #fff;
margin-top: 20rpx;
padding: 16rpx 24rpx;
}
.empty {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
.group-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.group-card:last-child {
border-bottom: 0;
margin-bottom: 0;
}
.group-card-global {
background: #fafafa;
}
.group-main {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
min-width: 0;
}
.group-title {
display: flex;
align-items: center;
gap: 8rpx;
}
.group-icon {
font-size: 28rpx;
}
.group-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.group-size {
font-size: 24rpx;
color: #999;
}
.group-warn {
font-size: 22rpx;
color: #faad14;
}
.clean-btn.small {
width: 160rpx;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
background: #1890ff;
color: #fff;
border-radius: 32rpx;
margin-left: 16rpx;
}
.clean-btn.small.strong {
background: #ff7a45;
}
.clean-btn.small[disabled] {
background: #d9d9d9;
color: #fff;
}
</style>