2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-07 05:47:21 +08:00

feat: 数据备份前端

This commit is contained in:
耗子
2024-10-13 22:44:42 +08:00
parent e1bcabec5d
commit f235492f8b
30 changed files with 559 additions and 73 deletions

View File

@@ -7,7 +7,7 @@ export default {
path: '/app',
component: Layout,
meta: {
order: 8
order: 90
},
children: [
{
@@ -16,7 +16,7 @@ export default {
component: () => import('./IndexView.vue'),
meta: {
title: 'appIndex.title',
icon: 'mdi:puzzle-outline',
icon: 'mdi:apps',
role: ['admin'],
requireAuth: true
}

View File

@@ -300,8 +300,8 @@ onMounted(() => {
</n-tab-pane>
</n-tabs>
</common-page>
<n-modal v-model:show="addUserModal" title="建用户">
<n-card closable @close="() => (addUserModal = false)" title="建用户" style="width: 60vw">
<n-modal v-model:show="addUserModal" title="建用户">
<n-card closable @close="() => (addUserModal = false)" title="建用户" style="width: 60vw">
<n-form :model="addUserModel">
<n-form-item path="username" label="用户名">
<n-input

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import backup from '@/api/panel/backup'
import ListView from '@/views/backup/ListView.vue'
import { NButton, NInput } from 'naive-ui'
const currentTab = ref('website')
const createModal = ref(false)
const createModel = ref({
target: '',
path: ''
})
const oldTab = ref('')
const handleCreate = () => {
backup.create(currentTab.value, createModel.value.target, createModel.value.path).then(() => {
createModal.value = false
window.$message.success('创建成功')
// 有点low但是没找到更好的办法
oldTab.value = currentTab.value
currentTab.value = ''
setTimeout(() => {
currentTab.value = oldTab.value
}, 0)
})
}
</script>
<template>
<common-page show-footer>
<template #action>
<div flex items-center>
<n-button class="ml-16" type="primary" @click="createModal = true">
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
创建备份
</n-button>
</div>
</template>
<n-flex vertical>
<n-alert type="info">此处仅显示面板默认备份目录的文件</n-alert>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="website" tab="网站">
<list-view v-model:type="currentTab" />
</n-tab-pane>
<n-tab-pane name="mysql" tab="MySQL">
<list-view v-model:type="currentTab" />
</n-tab-pane>
<n-tab-pane name="postgres" tab="PostgreSQL">
<list-view v-model:type="currentTab" />
</n-tab-pane>
</n-tabs>
</n-flex>
</common-page>
<n-modal
v-model:show="createModal"
preset="card"
title="创建备份"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="createModal = false"
>
<n-form :model="createModel">
<n-form-item path="name" label="名称">
<n-input
v-model:value="createModel.target"
type="text"
@keydown.enter.prevent
placeholder="输入网站/数据库名称"
/>
</n-form-item>
<n-form-item path="path" label="目录">
<n-input
v-model:value="createModel.path"
type="text"
@keydown.enter.prevent
placeholder="留空使用默认路径"
/>
</n-form-item>
</n-form>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<n-button type="info" block @click="handleCreate">提交</n-button>
</n-col>
</n-row>
</n-modal>
</template>

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import backup from '@/api/panel/backup'
import { renderIcon } from '@/utils'
import type { MessageReactive } from 'naive-ui'
import { NButton, NInput, NPopconfirm } from 'naive-ui'
import type { Backup } from './types'
const type = defineModel<string>('type', { type: String, required: true })
let messageReactive: MessageReactive | null = null
const restoreModal = ref(false)
const restoreModel = ref({
file: '',
target: ''
})
const columns: any = [
{ title: '文件名', key: 'name', fixed: 'left', resizable: true, ellipsis: { tooltip: true } },
{ title: '大小', key: 'size', width: 200, ellipsis: { tooltip: true } },
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
fixed: 'right',
hideInExcel: true,
render(row: any) {
return [
h(
NButton,
{
size: 'small',
type: 'warning',
secondary: true,
onClick: () => {
restoreModel.value.file = row.path
restoreModal.value = true
}
},
{
default: () => '恢复',
icon: renderIcon('material-symbols:settings-backup-restore-rounded', { size: 14 })
}
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete(row.name)
},
{
default: () => {
return '确定删除备份吗?'
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 15px;'
},
{
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
}
}
)
]
}
}
]
const data = ref<Backup[]>([])
const pagination = reactive({
page: 1,
pageCount: 1,
pageSize: 10,
itemCount: 0,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [10, 20, 50, 100]
})
const getList = async (page: number, limit: number) => {
const { data } = await backup.list(type.value, page, limit)
return data
}
const onPageChange = (page: number) => {
pagination.page = page
getList(page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
})
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
onPageChange(1)
}
const handleRestore = async () => {
messageReactive = window.$message.loading('恢复中...', {
duration: 0
})
await backup.restore(type.value, restoreModel.value.file, restoreModel.value.target).then(() => {
messageReactive?.destroy()
window.$message.success('恢复成功')
onPageChange(pagination.page)
})
}
const handleDelete = async (file: string) => {
await backup.delete(type.value, file).then(() => {
window.$message.success('删除成功')
onPageChange(pagination.page)
})
}
onMounted(() => {
onPageChange(pagination.page)
})
</script>
<template>
<n-data-table
striped
remote
:loading="false"
:columns="columns"
:data="data"
:row-key="(row: any) => row.name"
@update:page="onPageChange"
@update:page-size="onPageSizeChange"
/>
<n-modal
v-model:show="restoreModal"
preset="card"
title="恢复备份"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="restoreModal = false"
>
<n-form :model="restoreModel">
<n-form-item path="name" label="恢复目标">
<n-input v-model:value="restoreModel.target" type="text" @keydown.enter.prevent />
</n-form-item>
</n-form>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<n-button type="info" block @click="handleRestore">提交</n-button>
</n-col>
</n-row>
</n-modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,25 @@
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/IndexView.vue')
export default {
name: 'backup',
path: '/backup',
component: Layout,
meta: {
order: 60
},
children: [
{
name: 'backup-index',
path: '',
component: () => import('./IndexView.vue'),
meta: {
title: '数据备份',
icon: 'mdi:backup-outline',
role: ['admin'],
requireAuth: true
}
}
]
} as RouteType

View File

@@ -0,0 +1,5 @@
export interface Backup {
name: string
path: string
size: string
}

View File

@@ -7,7 +7,7 @@ export default {
path: '/cert',
component: Layout,
meta: {
order: 2
order: 10
},
children: [
{

View File

@@ -11,16 +11,16 @@ const currentTab = ref('container')
<common-page show-footer>
<n-tabs v-model:value="currentTab" type="line" animated size="large">
<n-tab-pane name="container" tab="容器">
<ContainerView />
<container-view />
</n-tab-pane>
<n-tab-pane name="image" tab="镜像">
<ImageView />
<image-view />
</n-tab-pane>
<n-tab-pane name="network" tab="网络">
<NetworkView />
<network-view />
</n-tab-pane>
<n-tab-pane name="volume" tab="卷">
<VolumeView />
<volume-view />
</n-tab-pane>
</n-tabs>
</common-page>

View File

@@ -7,7 +7,7 @@ export default {
path: '/container',
component: Layout,
meta: {
order: 5
order: 40
},
children: [
{

View File

@@ -329,7 +329,7 @@ onMounted(() => {
<n-radio-group v-model:value="addModel.backup_type">
<n-radio value="website">网站目录</n-radio>
<n-radio value="mysql" :disabled="!mySQLInstalled"> MySQL 数据库</n-radio>
<n-radio value="postgresql" :disabled="!postgreSQLInstalled">
<n-radio value="postgres" :disabled="!postgreSQLInstalled">
PostgreSQL 数据库
</n-radio>
</n-radio-group>

View File

@@ -7,7 +7,7 @@ export default {
path: '/cron',
component: Layout,
meta: {
order: 5
order: 70
},
children: [
{

View File

@@ -127,7 +127,7 @@ const columns: DataTableColumns<RowData> = [
selected.value = [row.full]
compress.value = true
} else {
window.open('/api/panel/file/download?path=' + encodeURIComponent(row.full))
window.open('/api/file/download?path=' + encodeURIComponent(row.full))
}
}
},

View File

@@ -7,7 +7,7 @@ export default {
path: '/file',
component: Layout,
meta: {
order: 6
order: 50
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/monitor',
component: Layout,
meta: {
order: 3
order: 20
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/safe',
component: Layout,
meta: {
order: 4
order: 30
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/setting',
component: Layout,
meta: {
order: 10
order: 999
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/ssh',
component: Layout,
meta: {
order: 7
order: 80
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/task',
component: Layout,
meta: {
order: 9
order: 100
},
children: [
{