Files
voidraft/frontend/src/views/settings/pages/BackupPage.vue
2025-11-16 21:23:59 +08:00

319 lines
10 KiB
Vue

<script setup lang="ts">
import {useConfigStore} from '@/stores/configStore';
import {useBackupStore} from '@/stores/backupStore';
import {useI18n} from 'vue-i18n';
import {computed} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
import {DialogService} from '@/../bindings/voidraft/internal/services';
const {t} = useI18n();
const configStore = useConfigStore();
const backupStore = useBackupStore();
const authMethodOptions = computed(() => [
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
{value: AuthMethod.UserPass, label: t('settings.backup.authMethods.userPass')}
]);
const backupIntervalOptions = computed(() => [
{value: 5, label: t('settings.backup.intervals.5min')},
{value: 10, label: t('settings.backup.intervals.10min')},
{value: 15, label: t('settings.backup.intervals.15min')},
{value: 30, label: t('settings.backup.intervals.30min')},
{value: 60, label: t('settings.backup.intervals.1hour')}
]);
const selectSshKeyFile = async () => {
const selectedPath = await DialogService.SelectFile();
if (selectedPath.trim()) {
configStore.setSshKeyPath(selectedPath.trim());
}
};
</script>
<template>
<div class="settings-page">
<!-- 基本设置 -->
<SettingSection :title="t('settings.backup.basicSettings')">
<SettingItem :title="t('settings.backup.enableBackup')">
<ToggleSwitch
:modelValue="configStore.config.backup.enabled"
@update:modelValue="configStore.setEnableBackup"
/>
</SettingItem>
<SettingItem
:title="t('settings.backup.autoBackup')"
:class="{ 'disabled-setting': !configStore.config.backup.enabled }"
>
<ToggleSwitch
:modelValue="configStore.config.backup.auto_backup"
@update:modelValue="configStore.setAutoBackup"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
<SettingItem
:title="t('settings.backup.backupInterval')"
:class="{ 'disabled-setting': !configStore.config.backup.enabled || !configStore.config.backup.auto_backup }"
>
<select
class="backup-interval-select"
:value="configStore.config.backup.backup_interval"
@change="(e) => configStore.setBackupInterval(Number((e.target as HTMLSelectElement).value))"
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.auto_backup"
>
<option v-for="option in backupIntervalOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</SettingItem>
</SettingSection>
<!-- 仓库配置 -->
<SettingSection :title="t('settings.backup.repositoryConfig')">
<SettingItem :title="t('settings.backup.repoUrl')">
<input
type="text"
class="repo-url-input"
:value="configStore.config.backup.repo_url"
@input="(e) => configStore.setRepoUrl((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.repoUrlPlaceholder')"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</SettingSection>
<!-- 认证配置 -->
<SettingSection :title="t('settings.backup.authConfig')">
<SettingItem :title="t('settings.backup.authMethod')">
<select
class="auth-method-select"
:value="configStore.config.backup.auth_method"
@change="(e) => configStore.setAuthMethod((e.target as HTMLSelectElement).value as AuthMethod)"
:disabled="!configStore.config.backup.enabled"
>
<option v-for="option in authMethodOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</SettingItem>
<!-- 用户名密码认证 -->
<template v-if="configStore.config.backup.auth_method === AuthMethod.UserPass">
<SettingItem :title="t('settings.backup.username')">
<input
type="text"
class="username-input"
:value="configStore.config.backup.username"
@input="(e) => configStore.setUsername((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.usernamePlaceholder')"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
<SettingItem :title="t('settings.backup.password')">
<input
type="password"
class="password-input"
:value="configStore.config.backup.password"
@input="(e) => configStore.setPassword((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.passwordPlaceholder')"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
<!-- 访问令牌认证 -->
<template v-if="configStore.config.backup.auth_method === AuthMethod.Token">
<SettingItem :title="t('settings.backup.token')">
<input
type="password"
class="token-input"
:value="configStore.config.backup.token"
@input="(e) => configStore.setToken((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.tokenPlaceholder')"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
<!-- SSH密钥认证 -->
<template v-if="configStore.config.backup.auth_method === AuthMethod.SSHKey">
<SettingItem :title="t('settings.backup.sshKeyPath')">
<input
type="text"
class="ssh-key-path-input"
:value="configStore.config.backup.ssh_key_path"
:placeholder="t('settings.backup.sshKeyPathPlaceholder')"
:disabled="!configStore.config.backup.enabled"
readonly
@click="configStore.config.backup.enabled && selectSshKeyFile()"
/>
</SettingItem>
<SettingItem :title="t('settings.backup.sshKeyPassphrase')">
<input
type="password"
class="ssh-passphrase-input"
:value="configStore.config.backup.ssh_key_passphrase"
@input="(e) => configStore.setSshKeyPassphrase((e.target as HTMLInputElement).value)"
:placeholder="t('settings.backup.sshKeyPassphrasePlaceholder')"
:disabled="!configStore.config.backup.enabled"
/>
</SettingItem>
</template>
</SettingSection>
<!-- 备份操作 -->
<SettingSection :title="t('settings.backup.backupOperations')">
<SettingItem
:title="t('settings.backup.pushToRemote')"
:description="backupStore.message || undefined"
:descriptionType="backupStore.message ? (backupStore.isError ? 'error' : 'success') : 'default'"
>
<button
class="push-button"
@click="backupStore.pushToRemote"
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isPushing"
:class="{ 'backing-up': backupStore.isPushing }"
>
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
</button>
</SettingItem>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
//max-width: 800px;
}
// 统一的输入控件样式
.repo-url-input,
.branch-input,
.username-input,
.password-input,
.token-input,
.ssh-key-path-input,
.ssh-passphrase-input,
.backup-interval-select,
.auth-method-select {
width: 50%;
min-width: 200px;
padding: 10px 12px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
color: var(--settings-text);
font-size: 12px;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: #4a9eff;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--settings-hover);
}
&::placeholder {
color: var(--settings-text-secondary);
}
&[readonly]:not(:disabled) {
cursor: pointer;
&:hover {
border-color: var(--settings-hover);
background-color: var(--settings-hover);
}
}
}
// 选择框特有样式
.backup-interval-select,
.auth-method-select {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
padding-right: 30px;
option {
background-color: var(--settings-input-bg);
color: var(--settings-text);
}
}
// 按钮样式
.push-button {
padding: 8px 16px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
color: var(--settings-text);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
&:hover:not(:disabled) {
background-color: var(--settings-hover);
border-color: var(--settings-border);
}
&:active:not(:disabled) {
transform: translateY(1px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--settings-text);
animation: spin 1s linear infinite;
}
&.backing-up {
background-color: #2196f3;
border-color: #2196f3;
color: white;
}
}
// 禁用状态
.disabled-setting {
opacity: 0.5;
pointer-events: none;
}
// 加载动画
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>