Added toast notification function and optimized related component styles

This commit is contained in:
2026-01-02 01:27:51 +08:00
parent 009274e4ad
commit 533f732c53
14 changed files with 909 additions and 199 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 {