✨ add image backup function and user information detailed display and editing function
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
<template>
|
||||
<div class="account-setting-backup">
|
||||
<div class="account-setting-backup-header">
|
||||
<span>图像备份</span>
|
||||
<AButton type="text" size="large" shape="circle" @click="getStorageList">
|
||||
<template #icon>
|
||||
<RedoOutlined />
|
||||
</template>
|
||||
</AButton>
|
||||
</div>
|
||||
<div class="account-setting-backup-body" v-if="storageList && storageList.length > 1">
|
||||
<div class="backup-container">
|
||||
<div class="backup-section source-section">
|
||||
<div class="section-title">
|
||||
<AAvatar shape="square" size="small" :src="sourceIcon" />
|
||||
<span>源存储</span>
|
||||
</div>
|
||||
<div class="storage-selection">
|
||||
<AForm layout="vertical">
|
||||
<AFormItem label="存储商" name="sourceProvider">
|
||||
<ASelect
|
||||
v-model:value="sourceStorage.provider"
|
||||
placeholder="请选择源存储商"
|
||||
@change="handleSourceProviderChange"
|
||||
>
|
||||
<ASelectOption
|
||||
v-for="item in storageList"
|
||||
:key="item.provider + '-' + item.bucket"
|
||||
:value="item.provider + '-' + item.bucket"
|
||||
>
|
||||
<AFlex align="center" gap="small">
|
||||
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
||||
<span>{{ ProviderNameMap[item.provider] }} - {{ item.bucket }}</span>
|
||||
</AFlex>
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<div class="selected-storage-card" v-if="selectedSourceStorage">
|
||||
<div class="storage-card-header">
|
||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedSourceStorage.provider]" />
|
||||
<div class="storage-info">
|
||||
<div class="storage-name">{{ selectedSourceStorage.bucket }}</div>
|
||||
<ATag :color="ProviderColorMap[selectedSourceStorage.provider]">{{ ProviderNameMap[selectedSourceStorage.provider] }}</ATag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="storage-card-content">
|
||||
<div class="storage-detail">
|
||||
<AAvatar size="small" shape="square" :src="bucket" />
|
||||
<span>{{ selectedSourceStorage.capacity }}GB</span>
|
||||
</div>
|
||||
<div class="storage-detail">
|
||||
<AAvatar size="small" shape="circle" :src="location" />
|
||||
<span>{{ AliRegionMap[selectedSourceStorage.region] || selectedSourceStorage.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backup-arrow">
|
||||
<ArrowRightOutlined />
|
||||
</div>
|
||||
|
||||
<div class="backup-section target-section">
|
||||
<div class="section-title">
|
||||
<AAvatar shape="square" size="small" :src="targetIcon" />
|
||||
<span>目标存储</span>
|
||||
</div>
|
||||
<div class="storage-selection">
|
||||
<AForm layout="vertical">
|
||||
<AFormItem label="存储商" name="targetProvider">
|
||||
<ASelect
|
||||
v-model:value="targetStorage.provider"
|
||||
placeholder="请选择目标存储商"
|
||||
@change="handleTargetProviderChange"
|
||||
:disabled="!sourceStorage.provider"
|
||||
>
|
||||
<ASelectOption
|
||||
v-for="item in availableTargetStorages"
|
||||
:key="item.provider + '-' + item.bucket"
|
||||
:value="item.provider + '-' + item.bucket"
|
||||
>
|
||||
<AFlex align="center" gap="small">
|
||||
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
||||
<span>{{ ProviderNameMap[item.provider] }} - {{ item.bucket }}</span>
|
||||
</AFlex>
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<div class="selected-storage-card" v-if="selectedTargetStorage">
|
||||
<div class="storage-card-header">
|
||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedTargetStorage.provider]" />
|
||||
<div class="storage-info">
|
||||
<div class="storage-name">{{ selectedTargetStorage.bucket }}</div>
|
||||
<ATag :color="ProviderColorMap[selectedTargetStorage.provider]">{{ ProviderNameMap[selectedTargetStorage.provider] }}</ATag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="storage-card-content">
|
||||
<div class="storage-detail">
|
||||
<AAvatar size="small" shape="square" :src="bucket" />
|
||||
<span>{{ selectedTargetStorage.capacity }}GB</span>
|
||||
</div>
|
||||
<div class="storage-detail">
|
||||
<AAvatar size="small" shape="circle" :src="location" />
|
||||
<span>{{ AliRegionMap[selectedTargetStorage.region] || selectedTargetStorage.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backup-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!isBackupReady"
|
||||
@click="startBackup"
|
||||
:loading="backupInProgress"
|
||||
>
|
||||
开始备份
|
||||
</AButton>
|
||||
</div>
|
||||
|
||||
<AModal v-model:open="backupModalVisible" title="备份进度" :footer="null" :closable="!backupInProgress" :maskClosable="false">
|
||||
<div class="backup-progress">
|
||||
<AProgress :percent="backupProgress" status="active" />
|
||||
<div class="backup-status">{{ backupStatus }}</div>
|
||||
<div class="backup-actions" v-if="backupInProgress && backupTaskId">
|
||||
<AButton danger @click="cancelBackup">取消备份</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</AModal>
|
||||
</div>
|
||||
<div class="account-setting-backup-empty" v-else>
|
||||
<AEmpty description="请先添加至少两个存储策略才能进行备份操作" />
|
||||
<AButton type="primary" @click="goToStoragePage">前往添加存储</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RedoOutlined, ArrowRightOutlined } from '@ant-design/icons-vue';
|
||||
import { listUserStorageConfigApi } from "@/api/storage";
|
||||
import { backupStorageApi, getBackupProgressApi, cancelBackupTaskApi } from "@/api/storage/backup";
|
||||
import { message } from "ant-design-vue";
|
||||
import { AliRegionMap, ProviderColorMap, ProviderIcon, ProviderNameMap } from "@/constant/provider_map.ts";
|
||||
import bucket from "@/assets/svgs/bucket.svg";
|
||||
import location from "@/assets/svgs/location-album.svg";
|
||||
import sourceIcon from "@/assets/svgs/source-storage.svg";
|
||||
import targetIcon from "@/assets/svgs/target-storage.svg";
|
||||
|
||||
const router = useRouter();
|
||||
const storageList = ref<any[]>([]);
|
||||
const sourceStorage = ref({ provider: '', bucket: '' });
|
||||
const targetStorage = ref({ provider: '', bucket: '' });
|
||||
const selectedSourceStorage = ref<any>(null);
|
||||
const selectedTargetStorage = ref<any>(null);
|
||||
const backupModalVisible = ref(false);
|
||||
const backupProgress = ref(0);
|
||||
const backupStatus = ref('准备开始备份...');
|
||||
const backupInProgress = ref(false);
|
||||
const backupTaskId = ref('');
|
||||
|
||||
// 获取存储列表
|
||||
async function getStorageList() {
|
||||
const res: any = await listUserStorageConfigApi();
|
||||
if (res && res.code === 200) {
|
||||
storageList.value = res.data.records;
|
||||
// 重置选择
|
||||
sourceStorage.value = { provider: '', bucket: '' };
|
||||
targetStorage.value = { provider: '', bucket: '' };
|
||||
selectedSourceStorage.value = null;
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可用的目标存储(排除已选择的源存储)
|
||||
const availableTargetStorages = computed(() => {
|
||||
if (!sourceStorage.value.provider) return [];
|
||||
return storageList.value.filter(item => {
|
||||
const sourceKey = sourceStorage.value.provider;
|
||||
const itemKey = item.provider + '-' + item.bucket;
|
||||
return sourceKey !== itemKey;
|
||||
});
|
||||
});
|
||||
|
||||
// 判断是否可以开始备份
|
||||
const isBackupReady = computed(() => {
|
||||
return selectedSourceStorage.value && selectedTargetStorage.value;
|
||||
});
|
||||
|
||||
// 处理源存储商选择变化
|
||||
function handleSourceProviderChange(value) {
|
||||
if (!value) {
|
||||
selectedSourceStorage.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const [provider, bucket] = value.split('-');
|
||||
const selected = storageList.value.find(item =>
|
||||
item.provider === provider && item.bucket === bucket
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
selectedSourceStorage.value = selected;
|
||||
|
||||
// 如果目标存储与源存储相同,则清空目标存储
|
||||
if (targetStorage.value.provider) {
|
||||
const [targetProvider, targetBucket] = targetStorage.value.provider.split('-');
|
||||
if (targetProvider === provider && targetBucket === bucket) {
|
||||
targetStorage.value.provider = '';
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理目标存储商选择变化
|
||||
function handleTargetProviderChange(value) {
|
||||
if (!value) {
|
||||
selectedTargetStorage.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const [provider, bucket] = value.split('-');
|
||||
const selected = storageList.value.find(item =>
|
||||
item.provider === provider && item.bucket === bucket
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
selectedTargetStorage.value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始备份
|
||||
async function startBackup() {
|
||||
if (!isBackupReady.value) return;
|
||||
|
||||
backupModalVisible.value = true;
|
||||
backupInProgress.value = true;
|
||||
backupProgress.value = 0;
|
||||
backupStatus.value = '正在准备备份...';
|
||||
|
||||
try {
|
||||
// 调用备份API
|
||||
const sourceProviderInfo = selectedSourceStorage.value.provider;
|
||||
const sourceBucketInfo = selectedSourceStorage.value.bucket;
|
||||
const targetProviderInfo = selectedTargetStorage.value.provider;
|
||||
const targetBucketInfo = selectedTargetStorage.value.bucket;
|
||||
|
||||
const res: any = await backupStorageApi(
|
||||
sourceProviderInfo,
|
||||
sourceBucketInfo,
|
||||
targetProviderInfo,
|
||||
targetBucketInfo
|
||||
);
|
||||
|
||||
if (res && res.code === 200) {
|
||||
backupTaskId.value = res.data.task_id;
|
||||
message.success('备份任务已启动');
|
||||
|
||||
// 定时获取备份进度
|
||||
const progressTimer = setInterval(async () => {
|
||||
if (backupTaskId.value) {
|
||||
const progressRes: any = await getBackupProgressApi(backupTaskId.value);
|
||||
|
||||
if (progressRes && progressRes.code === 200) {
|
||||
const { progress, status, message: statusMessage } = progressRes.data;
|
||||
|
||||
backupProgress.value = progress;
|
||||
backupStatus.value = statusMessage || status;
|
||||
|
||||
// 备份完成
|
||||
if (progress >= 100 || status === 'completed') {
|
||||
backupProgress.value = 100;
|
||||
backupStatus.value = '备份完成!';
|
||||
backupInProgress.value = false;
|
||||
backupTaskId.value = '';
|
||||
clearInterval(progressTimer);
|
||||
|
||||
message.success('备份成功!');
|
||||
setTimeout(() => {
|
||||
backupModalVisible.value = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// 备份失败
|
||||
if (status === 'failed') {
|
||||
backupInProgress.value = false;
|
||||
backupTaskId.value = '';
|
||||
clearInterval(progressTimer);
|
||||
|
||||
message.error(statusMessage || '备份失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearInterval(progressTimer);
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
message.error(res?.message || '启动备份任务失败');
|
||||
backupInProgress.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('备份错误:', error);
|
||||
message.error('备份过程中发生错误');
|
||||
backupInProgress.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 前往存储页面
|
||||
function goToStoragePage() {
|
||||
router.push('/main/user/setting/storage');
|
||||
}
|
||||
|
||||
// 取消备份
|
||||
async function cancelBackup() {
|
||||
if (!backupTaskId.value) return;
|
||||
|
||||
try {
|
||||
const res: any = await cancelBackupTaskApi(backupTaskId.value);
|
||||
|
||||
if (res && res.code === 200) {
|
||||
message.success('备份任务已取消');
|
||||
backupInProgress.value = false;
|
||||
backupTaskId.value = '';
|
||||
backupStatus.value = '备份已取消';
|
||||
} else {
|
||||
message.error(res?.message || '取消备份任务失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消备份错误:', error);
|
||||
message.error('取消备份过程中发生错误');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStorageList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-setting-backup {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.account-setting-backup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--white-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #1890ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-setting-backup-body {
|
||||
background-color: var(--white-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 24px;
|
||||
|
||||
.backup-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.backup-section {
|
||||
flex: 1;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed #e8e8e8;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.storage-selection {
|
||||
.selected-storage-card {
|
||||
margin-top: 16px;
|
||||
background: linear-gradient(145deg, #ffffff, #f5f7fa);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.storage-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.storage-info {
|
||||
.storage-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.storage-card-content {
|
||||
.storage-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.source-section {
|
||||
border-left: 3px solid #52c41a;
|
||||
}
|
||||
|
||||
.target-section {
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.backup-arrow {
|
||||
font-size: 24px;
|
||||
color: #1890ff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #e6f7ff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
|
||||
animation: pulse 1.5s infinite;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
transform: rotate(90deg);
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backup-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
|
||||
.ant-btn {
|
||||
height: 48px;
|
||||
padding: 0 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #1890ff, #096dd9);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backup-progress {
|
||||
text-align: center;
|
||||
|
||||
.backup-status {
|
||||
margin-top: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.backup-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.ant-btn {
|
||||
border-radius: 20px;
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-setting-backup-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
background-color: var(--white-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.ant-btn {
|
||||
margin-top: 20px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
padding: 0 24px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user