mirror of
https://github.com/acepanel/panel.git
synced 2026-02-06 13:37:13 +08:00
feat: 数据备份前端
This commit is contained in:
24
web/src/api/panel/backup/index.ts
Normal file
24
web/src/api/panel/backup/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取备份列表
|
||||
list: (type: string, page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/backup/${type}`, { params: { page, limit } }),
|
||||
// 创建备份
|
||||
create: (type: string, target: string, path: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/backup/${type}`, { target, path }),
|
||||
// 上传备份
|
||||
upload: (type: string, formData: FormData): Promise<AxiosResponse<any>> => {
|
||||
return request.post(`/backup/${type}/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
// 删除备份
|
||||
delete: (type: string, file: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete(`/backup/${type}/delete`, { params: { file } }),
|
||||
// 恢复备份
|
||||
restore: (type: string, file: string, target: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/backup/${type}/restore`, { file, target })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
87
web/src/views/backup/IndexView.vue
Normal file
87
web/src/views/backup/IndexView.vue
Normal 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>
|
||||
165
web/src/views/backup/ListView.vue
Normal file
165
web/src/views/backup/ListView.vue
Normal 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>
|
||||
25
web/src/views/backup/route.ts
Normal file
25
web/src/views/backup/route.ts
Normal 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
|
||||
5
web/src/views/backup/types.ts
Normal file
5
web/src/views/backup/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Backup {
|
||||
name: string
|
||||
path: string
|
||||
size: string
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/cert',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 2
|
||||
order: 10
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/container',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 5
|
||||
order: 40
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/cron',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 5
|
||||
order: 70
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/file',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 6
|
||||
order: 50
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/monitor',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 3
|
||||
order: 20
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/safe',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 4
|
||||
order: 30
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/setting',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 10
|
||||
order: 999
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/ssh',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 7
|
||||
order: 80
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/task',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 9
|
||||
order: 100
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user