✨ Added toast notification function and optimized related component styles
This commit is contained in:
@@ -2,48 +2,18 @@
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useBackupStore} from '@/stores/backupStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, ref, watch, onUnmounted} from 'vue';
|
||||
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';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
const {t} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const backupStore = useBackupStore();
|
||||
|
||||
// 消息显示状态
|
||||
const message = ref<string | null>(null);
|
||||
const isError = ref(false);
|
||||
let messageTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearMessage = () => {
|
||||
if (messageTimer) {
|
||||
clearTimeout(messageTimer);
|
||||
messageTimer = null;
|
||||
}
|
||||
message.value = null;
|
||||
};
|
||||
|
||||
// 监听同步完成,显示消息并自动消失
|
||||
watch(() => backupStore.isSyncing, (syncing, wasSyncing) => {
|
||||
if (wasSyncing && !syncing) {
|
||||
clearMessage();
|
||||
if (backupStore.error) {
|
||||
message.value = backupStore.error;
|
||||
isError.value = true;
|
||||
messageTimer = setTimeout(clearMessage, 5000);
|
||||
} else {
|
||||
message.value = 'Sync successful';
|
||||
isError.value = false;
|
||||
messageTimer = setTimeout(clearMessage, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(clearMessage);
|
||||
|
||||
const authMethodOptions = computed(() => [
|
||||
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
|
||||
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
|
||||
@@ -64,6 +34,15 @@ const selectSshKeyFile = async () => {
|
||||
configStore.setSshKeyPath(selectedPath.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
await backupStore.sync();
|
||||
toast.success('Sync successful');
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -202,14 +181,10 @@ const selectSshKeyFile = async () => {
|
||||
|
||||
<!-- 备份操作 -->
|
||||
<SettingSection :title="t('settings.backup.backupOperations')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.syncToRemote')"
|
||||
:description="message || undefined"
|
||||
:descriptionType="message ? (isError ? 'error' : 'success') : 'default'"
|
||||
>
|
||||
<SettingItem :title="t('settings.backup.syncToRemote')">
|
||||
<button
|
||||
class="sync-button"
|
||||
@click="backupStore.sync"
|
||||
@click="handleSync"
|
||||
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isSyncing"
|
||||
:class="{ 'syncing': backupStore.isSyncing }"
|
||||
>
|
||||
@@ -222,10 +197,6 @@ const selectSshKeyFile = async () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//max-width: 800px;
|
||||
}
|
||||
|
||||
// 统一的输入控件样式
|
||||
.repo-url-input,
|
||||
.branch-input,
|
||||
|
||||
@@ -9,6 +9,7 @@ import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useSystemStore} from "@/stores/systemStore";
|
||||
import {useConfirm, usePolling} from '@/composables';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
const {t} = useI18n();
|
||||
const {
|
||||
@@ -29,7 +30,6 @@ const tabStore = useTabStore();
|
||||
|
||||
// 进度条显示控制
|
||||
const showBar = ref(false);
|
||||
const manualError = ref(''); // 用于捕获 MigrateDirectory 抛出的错误
|
||||
let hideTimer = 0;
|
||||
|
||||
// 轮询迁移进度
|
||||
@@ -39,15 +39,20 @@ const {data: progress, error: pollError, isActive: migrating, start, stop, reset
|
||||
interval: 300,
|
||||
shouldStop: ({progress, error}) => !!error || progress >= 100,
|
||||
onStop: () => {
|
||||
const hasError = pollError.value || progress.value?.error;
|
||||
hideTimer = window.setTimeout(hideAll, hasError ? 5000 : 3000);
|
||||
const error = pollError.value || progress.value?.error;
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
} else if ((progress.value?.progress ?? 0) >= 100) {
|
||||
toast.success('Migration successful');
|
||||
}
|
||||
hideTimer = window.setTimeout(hideAll, 3000);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 派生状态
|
||||
const migrationError = computed(() => manualError.value || pollError.value || progress.value?.error || '');
|
||||
const currentProgress = computed(() => progress.value?.progress ?? 0);
|
||||
const migrationError = computed(() => pollError.value || progress.value?.error || '');
|
||||
|
||||
const barClass = computed(() => {
|
||||
if (!showBar.value) return '';
|
||||
@@ -64,8 +69,7 @@ const hideAll = () => {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = 0;
|
||||
showBar.value = false;
|
||||
manualError.value = '';
|
||||
reset(); // 清除轮询状态
|
||||
reset();
|
||||
};
|
||||
|
||||
// 重置设置确认
|
||||
@@ -193,10 +197,8 @@ const selectDataDirectory = async () => {
|
||||
|
||||
const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()];
|
||||
|
||||
// 清除之前的状态并开始轮询
|
||||
hideAll();
|
||||
showBar.value = true;
|
||||
manualError.value = '';
|
||||
start();
|
||||
|
||||
try {
|
||||
@@ -204,10 +206,9 @@ const selectDataDirectory = async () => {
|
||||
await setDataPath(newPath);
|
||||
} catch (e) {
|
||||
stop();
|
||||
// 设置手动捕获的错误(当轮询还没获取到错误时)
|
||||
manualError.value = String(e).replace(/^Error:\s*/i, '') || 'Migration failed';
|
||||
toast.error(String(e).replace(/^Error:\s*/i, '') || 'Migration failed');
|
||||
showBar.value = true;
|
||||
hideTimer = window.setTimeout(hideAll, 5000);
|
||||
hideTimer = window.setTimeout(hideAll, 3000);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -300,11 +301,6 @@ const selectDataDirectory = async () => {
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<Transition name="error-fade">
|
||||
<div v-if="migrationError" class="progress-error">{{ migrationError }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
@@ -537,13 +533,6 @@ const selectDataDirectory = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-error {
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
@@ -602,35 +591,4 @@ const selectDataDirectory = async () => {
|
||||
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 消息点脉冲动画
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误提示动画
|
||||
.error-fade-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.error-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
</style>
|
||||
@@ -72,6 +72,72 @@
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<!-- Toast 通知测试区域 -->
|
||||
<SettingSection title="Toast Notification Test">
|
||||
<SettingItem title="Toast Message">
|
||||
<input
|
||||
v-model="toastMessage"
|
||||
type="text"
|
||||
placeholder="Enter toast message"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Toast Title (Optional)">
|
||||
<input
|
||||
v-model="toastTitle"
|
||||
type="text"
|
||||
placeholder="Enter toast title"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Position">
|
||||
<select v-model="toastPosition" class="select-input">
|
||||
<option value="top-right">Top Right</option>
|
||||
<option value="top-left">Top Left</option>
|
||||
<option value="top-center">Top Center</option>
|
||||
<option value="bottom-right">Bottom Right</option>
|
||||
<option value="bottom-left">Bottom Left</option>
|
||||
<option value="bottom-center">Bottom Center</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
<SettingItem title="Duration (ms)">
|
||||
<input
|
||||
v-model.number="toastDuration"
|
||||
type="number"
|
||||
min="0"
|
||||
step="500"
|
||||
placeholder="4000"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Toast Types">
|
||||
<div class="button-group">
|
||||
<button @click="showToast('success')" class="test-button toast-success-btn">
|
||||
Success
|
||||
</button>
|
||||
<button @click="showToast('error')" class="test-button toast-error-btn">
|
||||
Error
|
||||
</button>
|
||||
<button @click="showToast('warning')" class="test-button toast-warning-btn">
|
||||
Warning
|
||||
</button>
|
||||
<button @click="showToast('info')" class="test-button toast-info-btn">
|
||||
Info
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="Quick Tests">
|
||||
<div class="button-group">
|
||||
<button @click="showMultipleToasts" class="test-button">
|
||||
Show Multiple Toasts
|
||||
</button>
|
||||
<button @click="clearAllToasts" class="test-button">
|
||||
Clear All Toasts
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 清除所有测试状态 -->
|
||||
<SettingSection title="Cleanup">
|
||||
<SettingItem title="Clear All">
|
||||
@@ -91,6 +157,8 @@ import { ref } from 'vue';
|
||||
import * as TestService from '@/../bindings/voidraft/internal/services/testservice';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import toast from '@/components/toast';
|
||||
import type { ToastPosition, ToastType } from '@/components/toast/types';
|
||||
|
||||
// Badge测试状态
|
||||
const badgeText = ref('');
|
||||
@@ -102,6 +170,12 @@ const notificationSubtitle = ref('');
|
||||
const notificationBody = ref('');
|
||||
const notificationStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// Toast 测试状态
|
||||
const toastMessage = ref('This is a test toast notification!');
|
||||
const toastTitle = ref('');
|
||||
const toastPosition = ref<ToastPosition>('top-right');
|
||||
const toastDuration = ref(4000);
|
||||
|
||||
// 清除状态
|
||||
const clearStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
@@ -172,13 +246,57 @@ const clearAll = async () => {
|
||||
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Toast 相关函数
|
||||
const showToast = (type: ToastType) => {
|
||||
const message = toastMessage.value || `This is a ${type} toast notification!`;
|
||||
const title = toastTitle.value || undefined;
|
||||
|
||||
const options = {
|
||||
position: toastPosition.value,
|
||||
duration: toastDuration.value,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
toast.success(message, title, options);
|
||||
break;
|
||||
case 'error':
|
||||
toast.error(message, title, options);
|
||||
break;
|
||||
case 'warning':
|
||||
toast.warning(message, title, options);
|
||||
break;
|
||||
case 'info':
|
||||
toast.info(message, title, options);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const showMultipleToasts = () => {
|
||||
const positions: ToastPosition[] = ['top-right', 'top-left', 'bottom-right', 'bottom-left'];
|
||||
const types: ToastType[] = ['success', 'error', 'warning', 'info'];
|
||||
|
||||
positions.forEach((position, index) => {
|
||||
setTimeout(() => {
|
||||
const type = types[index % types.length];
|
||||
toast.show({
|
||||
type,
|
||||
message: `Toast from ${position}`,
|
||||
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Toast`,
|
||||
position,
|
||||
duration: 5000,
|
||||
});
|
||||
}, index * 200);
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllToasts = () => {
|
||||
toast.clear();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//padding: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.dev-description {
|
||||
color: var(--settings-text-secondary);
|
||||
font-size: 12px;
|
||||
@@ -249,6 +367,50 @@ const clearAll = async () => {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-success-btn {
|
||||
background-color: #16a34a;
|
||||
color: white;
|
||||
border-color: #16a34a;
|
||||
|
||||
&:hover {
|
||||
background-color: #15803d;
|
||||
border-color: #15803d;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-error-btn {
|
||||
background-color: #dc2626;
|
||||
color: white;
|
||||
border-color: #dc2626;
|
||||
|
||||
&:hover {
|
||||
background-color: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning-btn {
|
||||
background-color: #f59e0b;
|
||||
color: white;
|
||||
border-color: #f59e0b;
|
||||
|
||||
&:hover {
|
||||
background-color: #d97706;
|
||||
border-color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info-btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
|
||||
&:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.test-status {
|
||||
|
||||
Reference in New Issue
Block a user