Files
wordpress-theme-aksrt/assets/admin/src/pages/tools/ToolsBackup.vue
T
kate522 3b951a5a36 refactor(admin): retire settings migration and compat menu demotion
Stop auto-running AKSRT_Settings_Migration, drop multi-option settings
merge and Zibll favicon aliases, remove the migration tools entry from
menus/schema, flatten admin sections so pages are not demoted into a
compat bag, and retire the migration-status payload for leftover SPA
routes after a full admin rebuild.
2026-07-11 15:54:35 +08:00

279 lines
7.5 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>
<div class="aksrt-page-container">
<PageHeader
title="数据备份"
desc="管理设置快照、创建手动备份,并执行恢复或删除。"
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/tools/import-export')">导入导出</n-button>
</n-space>
</template>
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>备份策略</h3>
</div>
<n-space vertical size="small" style="width: 100%;">
<n-space>
<n-tag round :type="settings.backup_auto ? 'success' : 'default'">
自动备份:{{ settings.backup_auto ? '已开启' : '未开启' }}
</n-tag>
<n-tag round type="info">频率:{{ frequencyLabel }}</n-tag>
<n-tag round type="info">保留:{{ settings.backup_retain }} 份</n-tag>
<n-tag round :type="backups.length ? 'success' : 'default'">现有快照:{{ backups.length }} 份</n-tag>
</n-space>
<n-space align="center" wrap>
<span class="aksrt-backup__label">自动备份</span>
<n-switch v-model:value="settings.backup_auto" />
<span class="aksrt-backup__label">频率</span>
<n-select v-model:value="settings.backup_frequency" :options="frequencyOptions" style="width: 140px;" />
<span class="aksrt-backup__label">保留</span>
<n-input-number v-model:value="settings.backup_retain" :min="3" :max="20" style="width: 140px;" />
<n-button type="primary" :loading="savingPolicy" @click="handleSavePolicy">保存策略</n-button>
</n-space>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>手动创建快照</h3>
</div>
<n-space vertical size="small" style="width: 100%;">
<n-input v-model:value="backupLabel" placeholder="可选:输入本次快照备注,如“升级前备份”" />
<n-space>
<n-button type="primary" :loading="creating" @click="handleCreateBackup">立即创建快照</n-button>
<n-button :loading="loading" @click="load">刷新列表</n-button>
</n-space>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>快照列表</h3>
</div>
<n-empty v-if="!backups.length" description="暂无设置快照" />
<n-data-table
v-else
:columns="columns"
:data="backups"
:bordered="true"
size="small"
:single-line="false"
:scroll-x="900"
/>
<p class="aksrt-backup__hint">
恢复非当前网站 / 非当前版本的快照可能导致配置异常,恢复前请确认站点环境一致。
</p>
</n-card>
</n-spin>
</div>
</template>
<script setup>
import { computed, h, onMounted, ref } from 'vue'
import { NButton, NSpace, NTag } from 'naive-ui'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const creating = ref(false)
const savingPolicy = ref(false)
const backupLabel = ref('')
const backups = ref([])
const settings = ref({
backup_auto: false,
backup_frequency: 'weekly',
backup_retain: 7,
})
const frequencyLabelMap = {
daily: '每天',
weekly: '每周',
monthly: '每月',
}
const frequencyOptions = [
{ label: '每天', value: 'daily' },
{ label: '每周', value: 'weekly' },
{ label: '每月', value: 'monthly' },
]
const frequencyLabel = computed(() => frequencyLabelMap[settings.value.backup_frequency] || '每周')
const columns = [
{
title: 'ID',
key: 'id',
width: 100,
},
{
title: '类型',
key: 'type',
width: 120,
render: (row) => h(NTag, { size: 'small', round: true, type: tagType(row.type) }, () => typeLabel(row.type)),
},
{
title: '名称',
key: 'label',
minWidth: 220,
},
{
title: '创建时间',
key: 'created_at',
width: 180,
},
{
title: '操作',
key: 'actions',
width: 180,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NButton, {
size: 'tiny',
type: 'primary',
quaternary: true,
onClick: () => handleRestore(row),
}, () => '恢复'),
h(NButton, {
size: 'tiny',
type: 'error',
quaternary: true,
onClick: () => handleDelete(row),
}, () => '删除'),
]),
},
]
onMounted(load)
function go(path) {
router.push(path)
}
function typeLabel(type) {
return {
manual: '手动备份',
scheduled: '定期备份',
reset_all: '重置全部',
reset_domain: '重置分区',
update_theme: '主题更新',
}[type] || type
}
function tagType(type) {
return {
manual: 'info',
scheduled: 'success',
reset_all: 'warning',
reset_domain: 'warning',
update_theme: 'error',
}[type] || 'default'
}
async function load() {
loading.value = true
try {
const [schemaRes, backupRes] = await Promise.all([
api.getSchemaPage('tools', 'backup'),
api.listSettingBackups(),
])
const schemaData = schemaRes?.data || {}
settings.value = {
backup_auto: !!schemaData.backup_auto,
backup_frequency: schemaData.backup_frequency || 'weekly',
backup_retain: Number(schemaData.backup_retain || 7),
}
backups.value = backupRes?.data?.items || []
} catch (error) {
message.error(error?.message || '加载备份列表失败')
} finally {
loading.value = false
}
}
async function handleCreateBackup() {
creating.value = true
try {
await api.createSettingBackup({
type: 'manual',
label: backupLabel.value,
})
backupLabel.value = ''
message.success('设置快照已创建')
await load()
} catch (error) {
message.error(error?.message || '创建快照失败')
} finally {
creating.value = false
}
}
async function handleSavePolicy() {
savingPolicy.value = true
try {
await api.saveSchemaPage('tools', 'backup', {
backup_auto: !!settings.value.backup_auto,
backup_frequency: settings.value.backup_frequency || 'weekly',
backup_retain: Math.min(20, Math.max(3, Number(settings.value.backup_retain || 7))),
})
message.success('备份策略已保存')
await load()
} catch (error) {
message.error(error?.message || '保存备份策略失败')
} finally {
savingPolicy.value = false
}
}
async function handleRestore(row) {
if (!window.confirm(`确认恢复快照「${row.label}」吗?此操作会覆盖当前设置。`)) {
return
}
try {
await api.restoreSettingBackup(row.id)
message.success('设置已恢复')
await load()
} catch (error) {
message.error(error?.message || '恢复快照失败')
}
}
async function handleDelete(row) {
if (!window.confirm(`确认删除快照「${row.label}」吗?`)) {
return
}
try {
await api.deleteSettingBackup(row.id)
message.success('快照已删除')
await load()
} catch (error) {
message.error(error?.message || '删除快照失败')
}
}
</script>
<style scoped>
.aksrt-backup__hint {
margin: 12px 0 0;
color: #667085;
font-size: 12px;
line-height: 1.7;
}
.aksrt-backup__label {
color: #344054;
font-size: 12px;
}
</style>