Files
RawMark/web/src/pages/settings/components/StoragePanel.vue
T
eezd 6b537c03e0 refactor(web): align resource APIs and forms with the starter conventions
Centralize typed resource functions under common/apis and return complete JSON envelopes while preserving raw responses, bearer session isolation, and single-owner errors.

Keep settings state and queues inside the setup store. Use Element Plus forms and dialogs for login, settings, imports, moves, and images; preserve editor interactions and themes. Update callers, tests, generated declarations, and request import constraints together.
2026-10-26 11:53:37 +08:00

178 lines
5.9 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.
<script setup lang="ts">
import type { StorageUsage } from '@@/apis/storage/type'
import { getStorageApi } from '@@/apis/storage'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import MaintenancePanel from './MaintenancePanel.vue'
import SettingRow from './SettingRow.vue'
const props = defineProps<{ prepareMaintenance: () => Promise<void> }>()
const emit = defineEmits<{ importNotes: [] }>()
const usage = ref<StorageUsage | null>(null)
const busy = ref(false)
const error = ref('')
let mounted = true
onBeforeUnmount(() => {
mounted = false
})
const names: Record<StorageUsage['categories'][number]['id'], string> = { vault: '笔记库', databases: '数据库', fonts: '字体', themes: '主题', plugins: '插件', other: '其他' }
const colors = ['var(--rm-accent)', '#56a89c', '#bd9364', '#a88ac2', '#d57e8d', '#8d99a8']
const segments = computed(() => {
let offset = 0
return (usage.value?.categories ?? []).map((category, index) => {
const percent = usage.value!.totalSize ? category.size / usage.value!.totalSize * 100 : 0
const segment = { ...category, percent, offset, color: colors[index] }
offset += percent
return segment
})
})
function size(value: number): string {
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
const unit = value > 0 ? Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1) : 0
return `${(value / 1024 ** unit).toLocaleString('zh-CN', { maximumFractionDigits: unit ? 2 : 0 })} ${units[unit]}`
}
const updated = computed(() => usage.value ? new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'medium' }).format(usage.value.calculatedAt) : '')
async function scan() {
if (busy.value) return
busy.value = true
error.value = ''
try {
const next = (await getStorageApi()).data
if (mounted) usage.value = next
} catch (cause) {
if (mounted) error.value = `统计失败:${String(cause)}`
} finally {
if (mounted) busy.value = false
}
}
onMounted(scan)
</script>
<template>
<div>
<h3 class="rm-settings-group-title">
空间占用
</h3>
<SettingRow id="storage">
<el-button class="rm-btn" :disabled="busy" @click="scan">
{{ busy ? '统计中…' : '刷新' }}
</el-button>
</SettingRow>
<p class="rm-share-note">
统计应用数据与内置可选字体,不包含默认 Maple 和程序本体;附件与回收站已包含在笔记库内。不跟随内部符号链接,不包含数据目录外的运维备份。
</p>
<p v-if="error" class="rm-auth-error" role="alert">
{{ error }}
</p>
<div v-if="usage" class="rm-storage" :aria-busy="busy">
<div class="rm-storage-summary">
<svg viewBox="0 0 120 120" role="img" aria-label="各类存储空间占比">
<circle cx="60" cy="60" r="46" fill="none" stroke="var(--rm-divider)" stroke-width="14" />
<circle v-for="segment in segments" :key="segment.id" cx="60" cy="60" r="46" pathLength="100" fill="none" :stroke="segment.color" stroke-width="14" :stroke-dasharray="`${segment.percent} ${100 - segment.percent}`" :stroke-dashoffset="-segment.offset" transform="rotate(-90 60 60)" />
</svg>
<div><span>总占用</span><strong :title="`${usage.totalSize} 字节`">{{ size(usage.totalSize) }}</strong><small>统计时间:{{ updated }}</small></div>
</div>
<dl class="rm-storage-list">
<template v-for="segment in segments" :key="segment.id">
<div>
<dt><i :style="{ background: segment.color }" />{{ names[segment.id] }}</dt><dd :title="`${segment.size} 字节`">
{{ size(segment.size) }} <small>{{ segment.percent.toFixed(1) }}%</small>
</dd>
</div>
<template v-if="segment.id === 'fonts'">
<div class="rm-storage-sub">
<dt>内置可选字体</dt><dd>{{ size(usage.builtinFontsSize) }}</dd>
</div>
<div class="rm-storage-sub">
<dt>导入字体</dt><dd>{{ size(usage.customFontsSize) }}</dd>
</div>
</template>
<template v-if="segment.id === 'vault'">
<div class="rm-storage-sub">
<dt>附件</dt><dd :title="`${usage.assetsSize} 字节`">
{{ size(usage.assetsSize) }} <small>{{ (usage.totalSize ? usage.assetsSize / usage.totalSize * 100 : 0).toFixed(1) }}%</small>
</dd>
</div>
<div class="rm-storage-sub">
<dt>回收站</dt><dd :title="`${usage.trashSize} 字节`">
{{ size(usage.trashSize) }} <small>{{ (usage.totalSize ? usage.trashSize / usage.totalSize * 100 : 0).toFixed(1) }}%</small>
</dd>
</div>
</template>
</template>
</dl>
</div>
<MaintenancePanel :prepare-maintenance="props.prepareMaintenance" @changed="scan" @import-notes="emit('importNotes')" />
</div>
</template>
<style scoped>
.rm-storage-summary {
display: flex;
align-items: center;
gap: 20px;
margin: 20px 0;
}
.rm-storage-summary svg {
width: 128px;
height: 128px;
flex-shrink: 0;
}
.rm-storage-summary strong {
display: block;
font-size: var(--rm-ui-font-heading);
margin: 6px 0;
}
.rm-storage-summary small {
display: block;
color: var(--rm-text-muted);
}
.rm-storage-list {
margin: 0;
}
.rm-storage-list > div {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 8px 0;
}
.rm-storage-list dt {
display: flex;
align-items: center;
gap: 8px;
}
.rm-storage-list i {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.rm-storage-list dd {
margin: 0;
text-align: right;
}
.rm-storage-list small {
display: inline-block;
min-width: 52px;
color: var(--rm-text-muted);
}
.rm-storage-list .rm-storage-sub {
padding-left: 18px;
color: var(--rm-text-muted);
}
@media (max-width: 480px) {
.rm-storage-summary {
gap: 12px;
flex-wrap: wrap;
}
.rm-storage-summary svg {
width: 100px;
height: 100px;
}
}
</style>