refactor:优化settings代码结构

This commit is contained in:
Baobhan Sith
2025-05-11 21:12:43 +08:00
parent 7ee8ffb90a
commit 0b08a221b1
14 changed files with 1706 additions and 1062 deletions
@@ -0,0 +1,78 @@
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import pkg from '../../../package.json'; // 路径相对于当前文件
import { useI18n } from 'vue-i18n';
export function useAboutSection() {
const { t } = useI18n();
const appVersion = ref(pkg.version);
// --- Version Check State ---
const latestVersion = ref<string | null>(null);
const isCheckingVersion = ref(false);
const versionCheckError = ref<string | null>(null);
const isUpdateAvailable = computed(() => {
// 简单的字符串比较,假设 tag 格式为 vX.Y.Z 或 X.Y.Z
// 后端返回的 tag_name 可能包含 'v' 前缀,也可能不包含
// appVersion.value 通常不包含 'v'
if (!latestVersion.value) return false;
const cleanLatestVersion = latestVersion.value.startsWith('v')
? latestVersion.value.substring(1)
: latestVersion.value;
const cleanAppVersion = appVersion.value.startsWith('v')
? appVersion.value.substring(1)
: appVersion.value;
// 进行版本比较,更健壮的比较可能需要拆分版本号进行数字比较
// 此处简单比较字符串,对于 "1.0.10" > "1.0.9" 是有效的
// 但对于 "1.0.9" > "1.0.10" 可能会出错,如果需要更精确,可以引入 semver 库或手动比较
return cleanLatestVersion !== cleanAppVersion && cleanLatestVersion > cleanAppVersion;
});
const checkLatestVersion = async () => {
isCheckingVersion.value = true;
versionCheckError.value = null;
latestVersion.value = null; // Reset before check
try {
const response = await axios.get('https://api.github.com/repos/Heavrnl/nexus-terminal/releases/latest', {
// 移除 headers 以尝试解决潜在的CORS或请求问题,GitHub API 通常不需要特定 headers 进行公共读取
});
if (response.data && response.data.tag_name) {
latestVersion.value = response.data.tag_name;
} else {
throw new Error('Invalid API response format');
}
} catch (error: any) {
console.error('检查最新版本失败:', error);
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
versionCheckError.value = t('settings.about.error.noReleases', '没有找到发布版本。');
} else if (error.response?.status === 403) {
versionCheckError.value = t('settings.about.error.rateLimit', 'API 访问频率受限,请稍后再试。');
} else {
versionCheckError.value = t('settings.about.error.checkFailed', '检查更新失败,请检查网络连接或稍后再试。');
}
} else {
versionCheckError.value = t('settings.about.error.checkFailed', '检查更新失败,请检查网络连接或稍后再试。');
}
} finally {
isCheckingVersion.value = false;
}
};
onMounted(() => {
checkLatestVersion();
});
return {
appVersion,
latestVersion,
isCheckingVersion,
versionCheckError,
isUpdateAvailable,
checkLatestVersion, // Expose if manual refresh is needed
};
}
@@ -0,0 +1,18 @@
import { useAppearanceStore } from '../../stores/appearance.store';
// import { useI18n } from 'vue-i18n'; // t function might be needed if there were messages
export function useAppearanceSettings() {
const appearanceStore = useAppearanceStore();
// const { t } = useI18n(); // Not strictly needed for just opening a modal yet
const openStyleCustomizer = () => {
appearanceStore.toggleStyleCustomizer(true);
};
// No specific loading, message, or success states are typically needed for just opening a UI element.
// These would be managed within the StyleCustomizer component itself or its own composable if it gets complex.
return {
openStyleCustomizer,
};
}
@@ -0,0 +1,171 @@
import { ref, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '../../stores/settings.store';
import apiClient from '../../utils/apiClient';
// Define necessary types locally if not shared, or import from a shared types file
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'none';
export interface UpdateCaptchaSettingsDto {
enabled?: boolean;
provider?: CaptchaProvider;
hcaptchaSiteKey?: string;
hcaptchaSecretKey?: string;
recaptchaSiteKey?: string;
recaptchaSecretKey?: string;
}
export function useCaptchaSettings() {
const settingsStore = useSettingsStore();
const { t } = useI18n();
const { captchaSettings } = storeToRefs(settingsStore);
const captchaForm = reactive<UpdateCaptchaSettingsDto>({
enabled: false,
provider: 'none',
hcaptchaSiteKey: '',
hcaptchaSecretKey: '',
recaptchaSiteKey: '',
recaptchaSecretKey: '',
});
const captchaLoading = ref(false);
const captchaMessage = ref('');
const captchaSuccess = ref(false);
watch(captchaSettings, (newCaptchaSettings) => {
if (newCaptchaSettings) {
captchaForm.enabled = newCaptchaSettings.enabled;
captchaForm.provider = newCaptchaSettings.provider;
captchaForm.hcaptchaSiteKey = newCaptchaSettings.hcaptchaSiteKey || '';
// Secret keys are not pre-filled from store for security; form will only send if user inputs a new one
captchaForm.hcaptchaSecretKey = '';
captchaForm.recaptchaSiteKey = newCaptchaSettings.recaptchaSiteKey || '';
captchaForm.recaptchaSecretKey = '';
} else {
// Reset form if settings are null (e.g., on error or initial load without data)
captchaForm.enabled = false;
captchaForm.provider = 'none';
captchaForm.hcaptchaSiteKey = '';
captchaForm.hcaptchaSecretKey = '';
captchaForm.recaptchaSiteKey = '';
captchaForm.recaptchaSecretKey = '';
}
}, { immediate: true, deep: true }); // deep: true might be useful if captchaSettings structure is complex
const handleUpdateCaptchaSettings = async () => {
captchaLoading.value = true;
captchaMessage.value = '';
captchaSuccess.value = false;
try {
let needsVerification = false;
let providerForVerification: CaptchaProvider | null = null;
let siteKeyForVerification: string | undefined = undefined;
let secretKeyForVerification: string | undefined = undefined;
// Step 1: Determine if verification is needed
if (captchaForm.enabled && captchaForm.provider && captchaForm.provider !== 'none') {
const originalSettings = captchaSettings.value; // Persisted settings from store
if (captchaForm.provider === 'hcaptcha') {
const originalSiteKeyValue = originalSettings?.hcaptchaSiteKey || '';
const currentSiteKeyValue = captchaForm.hcaptchaSiteKey || '';
const currentSecretKeyValue = captchaForm.hcaptchaSecretKey || '';
if (currentSiteKeyValue !== originalSiteKeyValue) {
if (!currentSiteKeyValue || !currentSecretKeyValue) {
throw new Error(t('settings.captcha.error.hcaptchaKeysRequired'));
}
needsVerification = true;
providerForVerification = 'hcaptcha';
siteKeyForVerification = currentSiteKeyValue;
secretKeyForVerification = currentSecretKeyValue;
} else if (currentSecretKeyValue) {
if (!currentSiteKeyValue) {
throw new Error(t('settings.captcha.error.hcaptchaKeysRequired'));
}
needsVerification = true;
providerForVerification = 'hcaptcha';
siteKeyForVerification = currentSiteKeyValue;
secretKeyForVerification = currentSecretKeyValue;
}
} else if (captchaForm.provider === 'recaptcha') {
const originalSiteKeyValue = originalSettings?.recaptchaSiteKey || '';
const currentSiteKeyValue = captchaForm.recaptchaSiteKey || '';
const currentSecretKeyValue = captchaForm.recaptchaSecretKey || '';
if (currentSiteKeyValue !== originalSiteKeyValue) {
if (!currentSiteKeyValue || !currentSecretKeyValue) {
throw new Error(t('settings.captcha.error.recaptchaKeysRequired'));
}
needsVerification = true;
providerForVerification = 'recaptcha';
siteKeyForVerification = currentSiteKeyValue;
secretKeyForVerification = currentSecretKeyValue;
} else if (currentSecretKeyValue) {
if (!currentSiteKeyValue) {
throw new Error(t('settings.captcha.error.recaptchaKeysRequired'));
}
needsVerification = true;
providerForVerification = 'recaptcha';
siteKeyForVerification = currentSiteKeyValue;
secretKeyForVerification = currentSecretKeyValue;
}
}
}
// Step 2: Perform verification if needed
if (needsVerification && providerForVerification && siteKeyForVerification && secretKeyForVerification) {
try {
await apiClient.post('/settings/captcha/verify', {
provider: providerForVerification,
siteKey: siteKeyForVerification,
secretKey: secretKeyForVerification,
});
} catch (verifyError: any) {
console.error('CAPTCHA verification failed:', verifyError);
throw new Error(verifyError.response?.data?.message || verifyError.message || t('settings.captcha.error.verificationFailed'));
}
}
// Step 3: Prepare DTO for saving
const dtoToSave: UpdateCaptchaSettingsDto = {
enabled: captchaForm.enabled,
provider: captchaForm.provider,
hcaptchaSiteKey: captchaForm.hcaptchaSiteKey || undefined,
recaptchaSiteKey: captchaForm.recaptchaSiteKey || undefined,
hcaptchaSecretKey: captchaForm.hcaptchaSecretKey || undefined,
recaptchaSecretKey: captchaForm.recaptchaSecretKey || undefined,
};
// Step 4: Call save operation
await settingsStore.updateCaptchaSettings(dtoToSave);
captchaMessage.value = t('settings.captcha.success.saved');
captchaSuccess.value = true;
// Clear secret key fields from the form after successful save
captchaForm.hcaptchaSecretKey = '';
captchaForm.recaptchaSecretKey = '';
} catch (error: any) {
captchaMessage.value = error.message || t('settings.captcha.error.saveFailed');
captchaSuccess.value = false;
} finally {
captchaLoading.value = false;
}
};
// Load initial CAPTCHA settings when the composable is used
// settingsStore.loadCaptchaSettings(); // This is called in SettingsView onMounted, might be redundant here unless SettingsView stops calling it.
// For now, assume SettingsView still handles initial load on its onMounted.
return {
captchaForm,
captchaLoading,
captchaMessage,
captchaSuccess,
handleUpdateCaptchaSettings,
// Expose captchaSettings from store if needed by the template directly, though form uses captchaForm
// captchaSettingsStore: captchaSettings
};
}
@@ -0,0 +1,56 @@
import { ref } from 'vue';
import { useAuthStore } from '../../stores/auth.store';
import { useI18n } from 'vue-i18n';
export function useChangePassword() {
const authStore = useAuthStore();
const { t } = useI18n();
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const changePasswordLoading = ref(false);
const changePasswordMessage = ref('');
const changePasswordSuccess = ref(false);
const handleChangePassword = async () => {
changePasswordMessage.value = '';
changePasswordSuccess.value = false;
if (newPassword.value !== confirmPassword.value) {
changePasswordMessage.value = t('settings.changePassword.error.passwordsDoNotMatch');
return;
}
if (!currentPassword.value || !newPassword.value) {
changePasswordMessage.value = t('settings.changePassword.error.fieldsRequired'); // 您可能需要添加此翻译
return;
}
changePasswordLoading.value = true;
try {
await authStore.changePassword(currentPassword.value, newPassword.value);
changePasswordMessage.value = t('settings.changePassword.success');
changePasswordSuccess.value = true;
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
} catch (error: any) {
console.error('修改密码失败:', error);
changePasswordMessage.value = error.message || t('settings.changePassword.error.generic');
changePasswordSuccess.value = false;
} finally {
changePasswordLoading.value = false;
}
};
return {
currentPassword,
newPassword,
confirmPassword,
changePasswordLoading,
changePasswordMessage,
changePasswordSuccess,
handleChangePassword,
};
}
@@ -0,0 +1,75 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import apiClient from '../../utils/apiClient';
import { isAxiosError } from 'axios';
export function useDataManagement() {
const { t } = useI18n();
// --- Export Connections State & Method ---
const exportConnectionsLoading = ref(false);
const exportConnectionsMessage = ref('');
const exportConnectionsSuccess = ref(false);
const handleExportConnections = async () => {
exportConnectionsLoading.value = true;
exportConnectionsMessage.value = '';
exportConnectionsSuccess.value = false;
try {
const response = await apiClient.get('/settings/export-connections', {
responseType: 'blob',
});
let filename = 'nexus_connections_export.zip';
const disposition = response.headers['content-disposition'];
if (disposition && disposition.includes('attachment')) {
const filenameRegex = /filename[^;=\n]*=(?:(?:["'])(?<quoted>.*?)\1|(?<unquoted>[^;\n]*))/;
const matches = filenameRegex.exec(disposition);
if (matches?.groups?.quoted || matches?.groups?.unquoted) {
filename = matches.groups.quoted || matches.groups.unquoted;
}
}
const blob = new Blob([response.data], { type: response.headers['content-type'] || 'application/zip' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
exportConnectionsMessage.value = t('settings.exportConnections.success', '导出成功。文件已开始下载。');
exportConnectionsSuccess.value = true;
} catch (error: any) {
console.error('导出连接失败:', error);
let message = t('settings.exportConnections.error', '导出连接时发生错误。');
if (isAxiosError(error) && error.response && error.response.data) {
if (error.response.data instanceof Blob && error.response.data.type === 'application/json') {
try {
const errorJson = JSON.parse(await error.response.data.text());
message = errorJson.message || message;
} catch (e) { /* Blob not valid JSON */ }
} else if (typeof error.response.data === 'string' && error.response.data.length < 200) { // Avoid overly long string errors
message = error.response.data;
} else if (error.response.data && typeof error.response.data.message === 'string') {
message = error.response.data.message;
}
} else if (error.message) {
message = error.message;
}
exportConnectionsMessage.value = message;
exportConnectionsSuccess.value = false;
} finally {
exportConnectionsLoading.value = false;
}
};
return {
exportConnectionsLoading,
exportConnectionsMessage,
exportConnectionsSuccess,
handleExportConnections,
};
}
@@ -0,0 +1,74 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import apiClient from '../../utils/apiClient';
import { isAxiosError } from 'axios';
export function useExportConnections() {
const { t } = useI18n();
const exportConnectionsLoading = ref(false);
const exportConnectionsMessage = ref('');
const exportConnectionsSuccess = ref(false);
const handleExportConnections = async () => {
exportConnectionsLoading.value = true;
exportConnectionsMessage.value = '';
exportConnectionsSuccess.value = false;
try {
const response = await apiClient.get('/settings/export-connections', {
responseType: 'blob',
});
let filename = 'nexus_connections_export.zip';
const disposition = response.headers['content-disposition'];
if (disposition && disposition.includes('attachment')) {
const filenameRegex = /filename[^;=\n]*=(?:(['"])(.*?)\1|([^;\n]*))/;
const matches = filenameRegex.exec(disposition);
if (matches != null && (matches[2] || matches[3])) {
filename = matches[2] || matches[3];
}
}
const blob = new Blob([response.data], { type: response.headers['content-type'] || 'application/zip' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
exportConnectionsMessage.value = t('settings.exportConnections.success', '导出成功。文件已开始下载。');
exportConnectionsSuccess.value = true;
} catch (error: any) {
console.error('导出连接失败:', error);
let message = t('settings.exportConnections.error', '导出连接时发生错误。');
if (isAxiosError(error) && error.response && error.response.data) {
if (error.response.data instanceof Blob && error.response.data.type === 'application/json') {
try {
const errorJson = JSON.parse(await error.response.data.text());
message = errorJson.message || message;
} catch (e) { /* Blob not valid JSON */ }
} else if (typeof error.response.data === 'string') {
message = error.response.data;
} else if (error.response.data && typeof error.response.data.message === 'string') {
message = error.response.data.message;
}
} else if (error.message) {
message = error.message;
}
exportConnectionsMessage.value = message;
exportConnectionsSuccess.value = false;
} finally {
exportConnectionsLoading.value = false;
}
};
return {
exportConnectionsLoading,
exportConnectionsMessage,
exportConnectionsSuccess,
handleExportConnections,
};
}
@@ -0,0 +1,158 @@
import { ref, reactive, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '../../stores/settings.store';
import { useAuthStore } from '../../stores/auth.store';
export function useIpBlacklist() {
const settingsStore = useSettingsStore();
const authStore = useAuthStore();
const { t } = useI18n();
const { settings, ipBlacklistEnabledBoolean } = storeToRefs(settingsStore);
// --- IP Blacklist Enabled State & Method ---
const ipBlacklistEnabled = ref(true); // Local state for the switch
watch(ipBlacklistEnabledBoolean, (newVal) => {
ipBlacklistEnabled.value = newVal;
}, { immediate: true });
const handleUpdateIpBlacklistEnabled = async () => {
const originalValue = ipBlacklistEnabled.value;
// Toggle local state immediately for UI feedback if it's directly bound to the switch
// If the switch v-model is ipBlacklistEnabled, this line is not strictly needed before API call,
// but helps if we want to manage the state change explicitly.
// ipBlacklistEnabled.value = !ipBlacklistEnabled.value; // This line might be redundant if v-model handles it
try {
// The value to save is the new state of the switch
const valueToSave = ipBlacklistEnabled.value; // This should reflect the intended new state
await settingsStore.updateSetting('ipBlacklistEnabled', valueToSave ? 'true' : 'false');
// Success: ipBlacklistEnabledBoolean will update via store watcher, syncing ipBlacklistEnabled.value
} catch (error: any) {
console.error('更新 IP 黑名单启用状态失败:', error);
ipBlacklistEnabled.value = originalValue; // Revert on failure
// Optionally, show an error message to the user
}
};
// --- IP Blacklist Configuration Form State & Method ---
const blacklistSettingsForm = reactive({
maxLoginAttempts: '5',
loginBanDuration: '300',
});
const blacklistSettingsLoading = ref(false);
const blacklistSettingsMessage = ref('');
const blacklistSettingsSuccess = ref(false);
watch(settings, (newSettings) => {
blacklistSettingsForm.maxLoginAttempts = newSettings.maxLoginAttempts || '5';
blacklistSettingsForm.loginBanDuration = newSettings.loginBanDuration || '300';
}, { deep: true, immediate: true });
const handleUpdateBlacklistSettings = async () => {
blacklistSettingsLoading.value = true;
blacklistSettingsMessage.value = '';
blacklistSettingsSuccess.value = false;
try {
const maxAttempts = parseInt(blacklistSettingsForm.maxLoginAttempts, 10);
const banDuration = parseInt(blacklistSettingsForm.loginBanDuration, 10);
if (isNaN(maxAttempts) || maxAttempts <= 0) {
throw new Error(t('settings.ipBlacklist.error.invalidMaxAttempts'));
}
if (isNaN(banDuration) || banDuration <= 0) {
throw new Error(t('settings.ipBlacklist.error.invalidBanDuration'));
}
await settingsStore.updateMultipleSettings({
maxLoginAttempts: blacklistSettingsForm.maxLoginAttempts,
loginBanDuration: blacklistSettingsForm.loginBanDuration,
});
blacklistSettingsMessage.value = t('settings.ipBlacklist.success.configUpdated');
blacklistSettingsSuccess.value = true;
} catch (error: any) {
console.error('更新黑名单配置失败:', error);
blacklistSettingsMessage.value = error.message || t('settings.ipBlacklist.error.updateConfigFailed');
blacklistSettingsSuccess.value = false;
} finally {
blacklistSettingsLoading.value = false;
}
};
// --- IP Blacklist Table State & Methods ---
const ipBlacklist = reactive({
entries: [] as any[],
total: 0,
loading: false,
error: null as string | null,
currentPage: 1,
limit: 10,
});
const blacklistToDeleteIp = ref<string | null>(null);
const blacklistDeleteLoading = ref(false);
const blacklistDeleteError = ref<string | null>(null);
const fetchIpBlacklist = async (page = 1) => {
ipBlacklist.loading = true;
ipBlacklist.error = null;
const offset = (page - 1) * ipBlacklist.limit;
try {
const data = await authStore.fetchIpBlacklist(ipBlacklist.limit, offset);
ipBlacklist.entries = data.entries;
ipBlacklist.total = data.total;
ipBlacklist.currentPage = page;
} catch (error: any) {
ipBlacklist.error = error.message || t('settings.ipBlacklist.error.fetchFailed');
} finally {
ipBlacklist.loading = false;
}
};
const handleDeleteIp = async (ip: string) => {
blacklistToDeleteIp.value = ip;
if (confirm(t('settings.ipBlacklist.confirmRemoveIp', { ip }))) {
blacklistDeleteLoading.value = true;
blacklistDeleteError.value = null;
try {
await authStore.deleteIpFromBlacklist(ip);
await fetchIpBlacklist(ipBlacklist.currentPage); // Refresh list
} catch (error: any) {
blacklistDeleteError.value = error.message || t('settings.ipBlacklist.error.deleteFailed');
} finally {
blacklistDeleteLoading.value = false;
blacklistToDeleteIp.value = null;
}
} else {
blacklistToDeleteIp.value = null;
}
};
onMounted(() => {
if (ipBlacklistEnabled.value) { // Only fetch if enabled, or always fetch and let template hide
fetchIpBlacklist();
}
});
watch(ipBlacklistEnabled, (newValue) => {
if (newValue && ipBlacklist.entries.length === 0 && !ipBlacklist.loading) {
fetchIpBlacklist();
}
});
return {
ipBlacklistEnabled,
handleUpdateIpBlacklistEnabled,
blacklistSettingsForm,
blacklistSettingsLoading,
blacklistSettingsMessage,
blacklistSettingsSuccess,
handleUpdateBlacklistSettings,
ipBlacklist,
blacklistToDeleteIp,
blacklistDeleteLoading,
blacklistDeleteError,
fetchIpBlacklist, // Expose if pagination is handled in the template
handleDeleteIp,
};
}
@@ -0,0 +1,44 @@
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '../../stores/settings.store';
export function useIpWhitelist() {
const settingsStore = useSettingsStore();
const { t } = useI18n();
const { settings } = storeToRefs(settingsStore);
const ipWhitelistInput = ref('');
const ipWhitelistLoading = ref(false);
const ipWhitelistMessage = ref('');
const ipWhitelistSuccess = ref(false);
watch(() => settings.value.ipWhitelist, (newVal) => {
ipWhitelistInput.value = newVal || '';
}, { immediate: true });
const handleUpdateIpWhitelist = async () => {
ipWhitelistLoading.value = true;
ipWhitelistMessage.value = '';
ipWhitelistSuccess.value = false;
try {
await settingsStore.updateSetting('ipWhitelist', ipWhitelistInput.value.trim());
ipWhitelistMessage.value = t('settings.ipWhitelist.success.saved');
ipWhitelistSuccess.value = true;
} catch (error: any) {
console.error('更新 IP 白名单失败:', error);
ipWhitelistMessage.value = error.message || t('settings.ipWhitelist.error.saveFailed');
ipWhitelistSuccess.value = false;
} finally {
ipWhitelistLoading.value = false;
}
};
return {
ipWhitelistInput,
ipWhitelistLoading,
ipWhitelistMessage,
ipWhitelistSuccess,
handleUpdateIpWhitelist,
};
}
@@ -0,0 +1,157 @@
import { ref, reactive, computed } from 'vue';
import { useAuthStore } from '../../stores/auth.store';
import { useI18n } from 'vue-i18n';
import { startRegistration } from '@simplewebauthn/browser';
import { storeToRefs } from 'pinia';
export function usePasskeyManagement() {
const authStore = useAuthStore();
const { t } = useI18n();
const { user, passkeys, passkeysLoading } = storeToRefs(authStore);
// --- Passkey State ---
const passkeyRegistrationLoading = ref(false); // Renamed for clarity
const passkeyMessage = ref('');
const passkeySuccess = ref(false);
const passkeyDeleteLoadingStates = reactive<Record<string, boolean>>({});
const passkeyDeleteError = ref<string | null>(null);
// State for editing passkey name
const editingPasskeyId = ref<string | null>(null);
const editingPasskeyName = ref('');
const passkeyEditLoadingStates = reactive<Record<string, boolean>>({});
const handleRegisterNewPasskey = async () => {
passkeyRegistrationLoading.value = true;
passkeyMessage.value = '';
passkeySuccess.value = false;
const username = user.value?.username;
if (!username) {
passkeyMessage.value = t('settings.passkey.error.userNotLoggedIn');
passkeyRegistrationLoading.value = false;
return;
}
try {
const registrationOptions = await authStore.getPasskeyRegistrationOptions(username);
const registrationResult = await startRegistration(registrationOptions);
await authStore.registerPasskey(username, registrationResult);
passkeyMessage.value = t('settings.passkey.success.registered');
passkeySuccess.value = true;
await authStore.fetchPasskeys();
} catch (error: any) {
console.error('Passkey 注册失败:', error);
if (error.name === 'InvalidStateError' || error.message.includes('cancelled') || error.message.includes('excludeCredentials')) {
passkeyMessage.value = t('settings.passkey.error.registrationCancelledOrExists'); // 您可能需要添加或修改此翻译
} else {
passkeyMessage.value = error.response?.data?.message || error.message || t('settings.passkey.error.registrationFailed');
}
passkeySuccess.value = false;
} finally {
passkeyRegistrationLoading.value = false;
}
};
const startEditPasskeyName = (credentialID: string, currentName: string) => {
editingPasskeyId.value = credentialID;
editingPasskeyName.value = currentName || ''; // Ensure it's a string
passkeyMessage.value = '';
passkeySuccess.value = false;
};
const cancelEditPasskeyName = () => {
editingPasskeyId.value = null;
editingPasskeyName.value = '';
};
const savePasskeyName = async (credentialID: string) => {
if (!editingPasskeyName.value.trim()) {
passkeyMessage.value = t('settings.passkey.error.nameRequired', 'Passkey 名称不能为空。');
passkeySuccess.value = false;
return;
}
passkeyEditLoadingStates[credentialID] = true;
passkeyMessage.value = '';
passkeySuccess.value = false;
try {
await authStore.updatePasskeyName(credentialID, editingPasskeyName.value.trim());
passkeyMessage.value = t('settings.passkey.success.nameUpdated');
passkeySuccess.value = true;
await authStore.fetchPasskeys();
cancelEditPasskeyName();
} catch (error: any) {
console.error(`更新 Passkey ${credentialID} 名称失败:`, error);
passkeyMessage.value = error.response?.data?.message || error.message || t('settings.passkey.error.nameUpdateFailed', '更新 Passkey 名称失败。');
passkeySuccess.value = false;
} finally {
passkeyEditLoadingStates[credentialID] = false;
}
};
const handleDeletePasskey = async (credentialID: string) => {
if (editingPasskeyId.value === credentialID) {
cancelEditPasskeyName();
}
if (!credentialID || typeof credentialID !== 'string') {
console.error('Attempted to delete a passkey with an invalid or undefined credentialID:', credentialID);
passkeyDeleteError.value = t('settings.passkey.error.deleteFailedInvalidId', '删除失败:无效的凭证 ID。');
return;
}
// It's better to handle confirmation in the component itself if needed, or pass a confirm function
// For now, assuming confirmation is handled or not strictly needed in the composable.
// if (!confirm(t('settings.passkey.confirmDelete'))) return;
passkeyDeleteLoadingStates[credentialID] = true;
passkeyDeleteError.value = null;
passkeyMessage.value = '';
try {
await authStore.deletePasskey(credentialID);
passkeyMessage.value = t('settings.passkey.success.deleted');
passkeySuccess.value = true;
// authStore.fetchPasskeys() is usually called within deletePasskey in the store
} catch (error: any) {
console.error(`删除 Passkey ${credentialID} 失败:`, error);
passkeyDeleteError.value = error.response?.data?.message || error.message || t('settings.passkey.error.deleteFailedGeneral');
passkeySuccess.value = false;
} finally {
passkeyDeleteLoadingStates[credentialID] = false;
}
};
const formatDate = (dateInput: string | number | Date | undefined): string => {
if (!dateInput) return t('statusMonitor.notAvailable', 'N/A');
try {
const date = new Date(typeof dateInput === 'number' ? dateInput * 1000 : dateInput);
return !isNaN(date.getTime()) ? date.toLocaleString() : t('statusMonitor.notAvailable', 'N/A');
} catch (e) {
console.error("Error formatting date:", e);
return t('statusMonitor.notAvailable', 'N/A');
}
};
// Fetch passkeys on composable initialization if user is authenticated
if (authStore.isAuthenticated) {
authStore.fetchPasskeys();
}
return {
passkeys, // from store
passkeysLoading, // from store
passkeyRegistrationLoading,
passkeyMessage,
passkeySuccess,
passkeyDeleteLoadingStates,
passkeyDeleteError,
editingPasskeyId,
editingPasskeyName,
passkeyEditLoadingStates,
handleRegisterNewPasskey,
startEditPasskeyName,
cancelEditPasskeyName,
savePasskeyName,
handleDeletePasskey,
formatDate,
};
}
@@ -0,0 +1,200 @@
import { ref, watch } from 'vue';
import { useSettingsStore } from '../../stores/settings.store';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { availableLocales } from '../../i18n'; // 导入可用语言列表
export function useSystemSettings() {
const settingsStore = useSettingsStore();
const { t } = useI18n();
const {
settings,
language: storeLanguage,
statusMonitorIntervalSecondsNumber,
dockerDefaultExpandBoolean, // Assuming this comes from settings store
} = storeToRefs(settingsStore);
// --- Language ---
const selectedLanguage = ref<string>(storeLanguage.value);
const languageLoading = ref(false);
const languageMessage = ref('');
const languageSuccess = ref(false);
const languageNames: Record<string, string> = {
'en-US': 'English',
'zh-CN': '中文',
'ja-JP': '日本語',
};
const handleUpdateLanguage = async () => {
languageLoading.value = true;
languageMessage.value = '';
languageSuccess.value = false;
try {
await settingsStore.updateSetting('language', selectedLanguage.value);
languageMessage.value = t('settings.language.success.saved');
languageSuccess.value = true;
// The language change will be reflected globally by the i18n instance
// when settingsStore.language updates.
} catch (error: any) {
console.error('更新语言设置失败:', error);
languageMessage.value = error.message || t('settings.language.error.saveFailed');
languageSuccess.value = false;
} finally {
languageLoading.value = false;
}
};
// --- Timezone ---
const selectedTimezone = ref('UTC');
const timezoneLoading = ref(false);
const timezoneMessage = ref('');
const timezoneSuccess = ref(false);
const commonTimezones = ref([
'UTC',
'Etc/GMT+12', 'Pacific/Midway', 'Pacific/Honolulu', 'America/Anchorage',
'America/Los_Angeles', 'America/Denver', 'America/Chicago', 'America/New_York',
'America/Caracas', 'America/Halifax', 'America/Sao_Paulo', 'Atlantic/Azores',
'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Moscow',
'Asia/Dubai', 'Asia/Karachi', 'Asia/Dhaka', 'Asia/Bangkok',
'Asia/Shanghai', 'Asia/Tokyo', 'Australia/Sydney', 'Pacific/Auckland',
'Etc/GMT-14'
]);
const handleUpdateTimezone = async () => {
timezoneLoading.value = true;
timezoneMessage.value = '';
timezoneSuccess.value = false;
try {
await settingsStore.updateSetting('timezone', selectedTimezone.value);
timezoneMessage.value = t('settings.timezone.success.saved');
timezoneSuccess.value = true;
} catch (error: any) {
console.error('更新时区设置失败:', error);
timezoneMessage.value = error.message || t('settings.timezone.error.saveFailed');
timezoneSuccess.value = false;
} finally {
timezoneLoading.value = false;
}
};
// --- Status Monitor ---
const statusMonitorIntervalLocal = ref(3);
const statusMonitorLoading = ref(false);
const statusMonitorMessage = ref('');
const statusMonitorSuccess = ref(false);
const handleUpdateStatusMonitorInterval = async () => {
statusMonitorLoading.value = true;
statusMonitorMessage.value = '';
statusMonitorSuccess.value = false;
try {
const intervalValue = statusMonitorIntervalLocal.value;
if (isNaN(intervalValue) || intervalValue < 1 || !Number.isInteger(intervalValue)) {
throw new Error(t('settings.statusMonitor.error.invalidInterval'));
}
await settingsStore.updateSetting('statusMonitorIntervalSeconds', String(intervalValue));
statusMonitorMessage.value = t('settings.statusMonitor.success.saved');
statusMonitorSuccess.value = true;
} catch (error: any) {
console.error('更新状态监控间隔失败:', error);
statusMonitorMessage.value = error.message || t('settings.statusMonitor.error.saveFailed');
statusMonitorSuccess.value = false;
} finally {
statusMonitorLoading.value = false;
}
};
// --- Docker Settings ---
const dockerInterval = ref(2);
const dockerExpandDefault = ref(false);
const dockerSettingsLoading = ref(false);
const dockerSettingsMessage = ref('');
const dockerSettingsSuccess = ref(false);
const handleUpdateDockerSettings = async () => {
dockerSettingsLoading.value = true;
dockerSettingsMessage.value = '';
dockerSettingsSuccess.value = false;
try {
const intervalValue = dockerInterval.value;
if (isNaN(intervalValue) || intervalValue < 1) {
throw new Error(t('settings.docker.error.invalidInterval'));
}
await settingsStore.updateMultipleSettings({
dockerStatusIntervalSeconds: String(intervalValue),
dockerDefaultExpand: dockerExpandDefault.value ? 'true' : 'false'
});
dockerSettingsMessage.value = t('settings.docker.success.saved');
dockerSettingsSuccess.value = true;
} catch (error: any) {
console.error('更新 Docker 设置失败:', error);
dockerSettingsMessage.value = error.message || t('settings.docker.error.saveFailed');
dockerSettingsSuccess.value = false;
} finally {
dockerSettingsLoading.value = false;
}
};
// Watch for changes in settings from the store and update local refs
watch(settings, (newSettings) => {
if (newSettings) {
selectedLanguage.value = newSettings.language || 'en-US'; // Default to en-US
selectedTimezone.value = newSettings.timezone || 'UTC';
statusMonitorIntervalLocal.value = parseInt(newSettings.statusMonitorIntervalSeconds || '3', 10);
dockerInterval.value = parseInt(newSettings.dockerStatusIntervalSeconds || '2', 10);
// dockerExpandDefault.value is already reactive from storeToRefs (dockerDefaultExpandBoolean)
// but we keep a local ref for the form v-model and sync it.
dockerExpandDefault.value = newSettings.dockerDefaultExpand === 'true';
}
}, { deep: true, immediate: true });
// Sync local dockerExpandDefault with the store's boolean getter
watch(dockerDefaultExpandBoolean, (newValue) => {
dockerExpandDefault.value = newValue;
}, { immediate: true });
// Sync local statusMonitorIntervalLocal with the store's number getter
watch(statusMonitorIntervalSecondsNumber, (newValue) => {
statusMonitorIntervalLocal.value = newValue;
}, { immediate: true });
// Sync local selectedLanguage with the store's language getter
watch(storeLanguage, (newVal) => {
selectedLanguage.value = newVal;
}, { immediate: true });
return {
// Language
selectedLanguage,
languageLoading,
languageMessage,
languageSuccess,
languageNames,
availableLocales, // Export for template
handleUpdateLanguage,
// Timezone
selectedTimezone,
timezoneLoading,
timezoneMessage,
timezoneSuccess,
commonTimezones,
handleUpdateTimezone,
// Status Monitor
statusMonitorIntervalLocal,
statusMonitorLoading,
statusMonitorMessage,
statusMonitorSuccess,
handleUpdateStatusMonitorInterval,
// Docker Settings
dockerInterval,
dockerExpandDefault,
dockerSettingsLoading,
dockerSettingsMessage,
dockerSettingsSuccess,
handleUpdateDockerSettings,
};
}
@@ -0,0 +1,117 @@
import { ref, computed, onMounted } from 'vue';
import { useAuthStore } from '../../stores/auth.store';
import apiClient from '../../utils/apiClient';
import { useI18n } from 'vue-i18n';
export function useTwoFactorAuth() {
const authStore = useAuthStore();
const { t } = useI18n();
const twoFactorEnabled = ref(false);
const twoFactorLoading = ref(false);
const twoFactorMessage = ref('');
const twoFactorSuccess = ref(false);
const setupData = ref<{ secret: string; qrCodeUrl: string } | null>(null);
const verificationCode = ref('');
const disablePassword = ref('');
const isSettingUp2FA = computed(() => setupData.value !== null);
const checkTwoFactorStatus = async () => {
// Ensure user is loaded before checking 2FA status
if (!authStore.user) {
await authStore.checkAuthStatus(); // Attempt to load user if not already
}
twoFactorEnabled.value = authStore.user?.isTwoFactorEnabled ?? false;
};
const handleSetup2FA = async () => {
twoFactorMessage.value = '';
twoFactorSuccess.value = false;
twoFactorLoading.value = true;
setupData.value = null;
verificationCode.value = '';
try {
const response = await apiClient.post<{ secret: string; qrCodeUrl: string }>('/auth/2fa/setup');
setupData.value = response.data;
} catch (error: any) {
console.error('开始设置 2FA 失败:', error);
twoFactorMessage.value = error.response?.data?.message || t('settings.twoFactor.error.setupFailed');
} finally {
twoFactorLoading.value = false;
}
};
const handleVerifyAndActivate2FA = async () => {
if (!setupData.value || !verificationCode.value) {
twoFactorMessage.value = t('settings.twoFactor.error.codeRequired');
return;
}
twoFactorMessage.value = '';
twoFactorSuccess.value = false;
twoFactorLoading.value = true;
try {
await apiClient.post('/auth/2fa/verify', { token: verificationCode.value });
twoFactorMessage.value = t('settings.twoFactor.success.activated');
twoFactorSuccess.value = true;
twoFactorEnabled.value = true;
setupData.value = null;
verificationCode.value = '';
await authStore.checkAuthStatus(); // Refresh user data
} catch (error: any) {
console.error('验证并激活 2FA 失败:', error);
twoFactorMessage.value = error.response?.data?.message || t('settings.twoFactor.error.verificationFailed');
} finally {
twoFactorLoading.value = false;
}
};
const handleDisable2FA = async () => {
if (!disablePassword.value) {
twoFactorMessage.value = t('settings.twoFactor.error.passwordRequiredForDisable');
return;
}
twoFactorMessage.value = '';
twoFactorSuccess.value = false;
twoFactorLoading.value = true;
try {
await apiClient.delete('/auth/2fa', { data: { password: disablePassword.value } });
twoFactorMessage.value = t('settings.twoFactor.success.disabled');
twoFactorSuccess.value = true;
twoFactorEnabled.value = false;
disablePassword.value = '';
await authStore.checkAuthStatus(); // Refresh user data
} catch (error: any) {
console.error('禁用 2FA 失败:', error);
twoFactorMessage.value = error.response?.data?.message || t('settings.twoFactor.error.disableFailed');
} finally {
twoFactorLoading.value = false;
}
};
const cancelSetup = () => {
setupData.value = null;
verificationCode.value = '';
twoFactorMessage.value = '';
};
onMounted(async () => {
await checkTwoFactorStatus();
});
return {
twoFactorEnabled,
twoFactorLoading,
twoFactorMessage,
twoFactorSuccess,
setupData,
verificationCode,
disablePassword,
isSettingUp2FA,
checkTwoFactorStatus, // Expose if needed externally, though onMounted handles initial check
handleSetup2FA,
handleVerifyAndActivate2FA,
handleDisable2FA,
cancelSetup,
};
}
@@ -0,0 +1,51 @@
import { ref, computed } from 'vue';
import axios from 'axios';
import pkg from '../../../package.json'; // 调整路径以正确导入 package.json
import { useI18n } from 'vue-i18n';
export function useVersionCheck() {
const { t } = useI18n();
const appVersion = ref(pkg.version);
const latestVersion = ref<string | null>(null);
const isCheckingVersion = ref(false);
const versionCheckError = ref<string | null>(null);
const isUpdateAvailable = computed(() => {
// 简单的字符串比较,假设 tag 格式为 vX.Y.Z
return latestVersion.value && latestVersion.value !== `v${appVersion.value}`;
});
const checkLatestVersion = async () => {
isCheckingVersion.value = true;
versionCheckError.value = null;
latestVersion.value = null;
try {
const response = await axios.get('https://api.github.com/repos/Heavrnl/nexus-terminal/releases/latest');
if (response.data && response.data.tag_name) {
latestVersion.value = response.data.tag_name;
} else {
throw new Error('Invalid API response format');
}
} catch (error: any) {
console.error('检查最新版本失败:', error);
if (axios.isAxiosError(error) && error.response?.status === 404) {
versionCheckError.value = t('settings.about.error.noReleases');
} else if (axios.isAxiosError(error) && error.response?.status === 403) {
versionCheckError.value = t('settings.about.error.rateLimit');
} else {
versionCheckError.value = t('settings.about.error.checkFailed');
}
} finally {
isCheckingVersion.value = false;
}
};
return {
appVersion,
latestVersion,
isCheckingVersion,
versionCheckError,
isUpdateAvailable,
checkLatestVersion,
};
}
@@ -0,0 +1,306 @@
import { ref, watch } from 'vue';
import { useSettingsStore } from '../../stores/settings.store';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
export function useWorkspaceSettings() {
const settingsStore = useSettingsStore();
const { t } = useI18n();
const {
showPopupFileEditorBoolean,
shareFileEditorTabsBoolean,
autoCopyOnSelectBoolean,
workspaceSidebarPersistentBoolean,
commandInputSyncTarget,
showConnectionTagsBoolean,
showQuickCommandTagsBoolean,
terminalScrollbackLimitNumber,
fileManagerShowDeleteConfirmationBoolean,
} = storeToRefs(settingsStore);
// --- Popup Editor ---
const popupEditorEnabled = ref(true);
const popupEditorLoading = ref(false);
const popupEditorMessage = ref('');
const popupEditorSuccess = ref(false);
const handleUpdatePopupEditorSetting = async () => {
popupEditorLoading.value = true;
popupEditorMessage.value = '';
popupEditorSuccess.value = false;
try {
const valueToSave = popupEditorEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('showPopupFileEditor', valueToSave);
popupEditorMessage.value = t('settings.popupEditor.success.saved');
popupEditorSuccess.value = true;
} catch (error: any) {
console.error('更新弹窗编辑器设置失败:', error);
popupEditorMessage.value = error.message || t('settings.popupEditor.error.saveFailed');
popupEditorSuccess.value = false;
} finally {
popupEditorLoading.value = false;
}
};
// --- Share Editor Tabs ---
const shareTabsEnabled = ref(true);
const shareTabsLoading = ref(false);
const shareTabsMessage = ref('');
const shareTabsSuccess = ref(false);
const handleUpdateShareTabsSetting = async () => {
shareTabsLoading.value = true;
shareTabsMessage.value = '';
shareTabsSuccess.value = false;
try {
const valueToSave = shareTabsEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('shareFileEditorTabs', valueToSave);
shareTabsMessage.value = t('settings.shareEditorTabs.success.saved');
shareTabsSuccess.value = true;
} catch (error: any) {
console.error('更新共享编辑器标签页设置失败:', error);
shareTabsMessage.value = error.message || t('settings.shareEditorTabs.error.saveFailed');
shareTabsSuccess.value = false;
} finally {
shareTabsLoading.value = false;
}
};
// --- Auto Copy on Select ---
const autoCopyEnabled = ref(false);
const autoCopyLoading = ref(false);
const autoCopyMessage = ref('');
const autoCopySuccess = ref(false);
const handleUpdateAutoCopySetting = async () => {
autoCopyLoading.value = true;
autoCopyMessage.value = '';
autoCopySuccess.value = false;
try {
const valueToSave = autoCopyEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('autoCopyOnSelect', valueToSave);
autoCopyMessage.value = t('settings.autoCopyOnSelect.success.saved');
autoCopySuccess.value = true;
} catch (error: any) {
console.error('更新自动复制设置失败:', error);
autoCopyMessage.value = error.message || t('settings.autoCopyOnSelect.error.saveFailed');
autoCopySuccess.value = false;
} finally {
autoCopyLoading.value = false;
}
};
// --- Workspace Sidebar Persistent ---
const workspaceSidebarPersistentEnabled = ref(false);
const workspaceSidebarPersistentLoading = ref(false);
const workspaceSidebarPersistentMessage = ref('');
const workspaceSidebarPersistentSuccess = ref(false);
const handleUpdateWorkspaceSidebarSetting = async () => {
workspaceSidebarPersistentLoading.value = true;
workspaceSidebarPersistentMessage.value = '';
workspaceSidebarPersistentSuccess.value = false;
try {
const valueToSave = workspaceSidebarPersistentEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('workspaceSidebarPersistent', valueToSave);
workspaceSidebarPersistentMessage.value = t('settings.workspace.success.sidebarPersistentSaved');
workspaceSidebarPersistentSuccess.value = true;
} catch (error: any) {
console.error('更新侧边栏固定设置失败:', error);
workspaceSidebarPersistentMessage.value = error.message || t('settings.workspace.error.sidebarPersistentSaveFailed');
workspaceSidebarPersistentSuccess.value = false;
} finally {
workspaceSidebarPersistentLoading.value = false;
}
};
// --- Command Input Sync Target ---
const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none');
const commandInputSyncLoading = ref(false);
const commandInputSyncMessage = ref('');
const commandInputSyncSuccess = ref(false);
const handleUpdateCommandInputSyncTarget = async () => {
commandInputSyncLoading.value = true;
commandInputSyncMessage.value = '';
commandInputSyncSuccess.value = false;
try {
await settingsStore.updateSetting('commandInputSyncTarget', commandInputSyncTargetLocal.value);
commandInputSyncMessage.value = t('settings.commandInputSync.success.saved', '同步目标已保存');
commandInputSyncSuccess.value = true;
} catch (error: any) {
console.error('更新命令输入同步目标失败:', error);
commandInputSyncMessage.value = error.message || t('settings.commandInputSync.error.saveFailed', '保存同步目标失败');
commandInputSyncSuccess.value = false;
} finally {
commandInputSyncLoading.value = false;
}
};
// --- Show Connection Tags ---
const showConnectionTagsLocal = ref(true);
const showConnectionTagsLoading = ref(false);
const showConnectionTagsMessage = ref('');
const showConnectionTagsSuccess = ref(false);
const handleUpdateShowConnectionTags = async () => {
showConnectionTagsLoading.value = true;
showConnectionTagsMessage.value = '';
showConnectionTagsSuccess.value = false;
try {
await settingsStore.updateSetting('showConnectionTags', showConnectionTagsLocal.value);
showConnectionTagsMessage.value = t('settings.workspace.success.showConnectionTagsSaved', '连接标签显示设置已保存');
showConnectionTagsSuccess.value = true;
} catch (error: any) {
console.error('更新显示连接标签设置失败:', error);
showConnectionTagsMessage.value = error.message || t('settings.workspace.error.showConnectionTagsSaveFailed', '保存连接标签显示设置失败');
showConnectionTagsSuccess.value = false;
} finally {
showConnectionTagsLoading.value = false;
}
};
// --- Show Quick Command Tags ---
const showQuickCommandTagsLocal = ref(true);
const showQuickCommandTagsLoading = ref(false);
const showQuickCommandTagsMessage = ref('');
const showQuickCommandTagsSuccess = ref(false);
const handleUpdateShowQuickCommandTags = async () => {
showQuickCommandTagsLoading.value = true;
showQuickCommandTagsMessage.value = '';
showQuickCommandTagsSuccess.value = false;
try {
await settingsStore.updateSetting('showQuickCommandTags', showQuickCommandTagsLocal.value);
showQuickCommandTagsMessage.value = t('settings.workspace.success.showQuickCommandTagsSaved', '快捷指令标签显示设置已保存');
showQuickCommandTagsSuccess.value = true;
} catch (error: any) {
console.error('更新显示快捷指令标签设置失败:', error);
showQuickCommandTagsMessage.value = error.message || t('settings.workspace.error.showQuickCommandTagsSaveFailed', '保存快捷指令标签显示设置失败');
showQuickCommandTagsSuccess.value = false;
} finally {
showQuickCommandTagsLoading.value = false;
}
};
// --- Terminal Scrollback Limit ---
const terminalScrollbackLimitLocal = ref<number | null>(null);
const terminalScrollbackLimitLoading = ref(false);
const terminalScrollbackLimitMessage = ref('');
const terminalScrollbackLimitSuccess = ref(false);
const handleUpdateTerminalScrollbackLimit = async () => {
terminalScrollbackLimitLoading.value = true;
terminalScrollbackLimitMessage.value = '';
terminalScrollbackLimitSuccess.value = false;
try {
const limitValue = terminalScrollbackLimitLocal.value;
if (limitValue !== null && limitValue !== undefined && (isNaN(limitValue) || !Number.isInteger(limitValue) || limitValue < 0)) {
throw new Error(t('settings.terminalScrollback.error.invalidInput', '请输入一个有效的非负整数。'));
}
const valueToSave = (limitValue === null || limitValue === undefined) ? '5000' : String(limitValue);
await settingsStore.updateSetting('terminalScrollbackLimit', valueToSave);
terminalScrollbackLimitMessage.value = t('settings.terminalScrollback.success.saved', '终端回滚行数设置已保存。');
terminalScrollbackLimitSuccess.value = true;
} catch (error: any) {
console.error('更新终端回滚行数设置失败:', error);
terminalScrollbackLimitMessage.value = error.message || t('settings.terminalScrollback.error.saveFailed', '保存终端回滚行数设置失败。');
terminalScrollbackLimitSuccess.value = false;
} finally {
terminalScrollbackLimitLoading.value = false;
}
};
// --- File Manager Delete Confirmation ---
const fileManagerShowDeleteConfirmationLocal = ref(true);
const fileManagerShowDeleteConfirmationLoading = ref(false);
const fileManagerShowDeleteConfirmationMessage = ref('');
const fileManagerShowDeleteConfirmationSuccess = ref(false);
const handleUpdateFileManagerDeleteConfirmation = async () => {
fileManagerShowDeleteConfirmationLoading.value = true;
fileManagerShowDeleteConfirmationMessage.value = '';
fileManagerShowDeleteConfirmationSuccess.value = false;
try {
const valueToSave = fileManagerShowDeleteConfirmationLocal.value ? 'true' : 'false';
await settingsStore.updateSetting('fileManagerShowDeleteConfirmation', valueToSave);
fileManagerShowDeleteConfirmationMessage.value = t('settings.workspace.fileManagerDeleteConfirmSuccess', '文件管理器删除确认设置已保存。');
fileManagerShowDeleteConfirmationSuccess.value = true;
} catch (error: any) {
console.error('更新文件管理器删除确认设置失败:', error);
fileManagerShowDeleteConfirmationMessage.value = error.message || t('settings.workspace.fileManagerDeleteConfirmError', '保存文件管理器删除确认设置失败。');
fileManagerShowDeleteConfirmationSuccess.value = false;
} finally {
fileManagerShowDeleteConfirmationLoading.value = false;
}
};
// Watchers to sync local state with store state
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
watch(autoCopyOnSelectBoolean, (newValue) => { autoCopyEnabled.value = newValue; }, { immediate: true });
watch(workspaceSidebarPersistentBoolean, (newValue) => { workspaceSidebarPersistentEnabled.value = newValue; }, { immediate: true });
watch(commandInputSyncTarget, (newValue) => { commandInputSyncTargetLocal.value = newValue; }, { immediate: true });
watch(showConnectionTagsBoolean, (newValue) => { showConnectionTagsLocal.value = newValue; }, { immediate: true });
watch(showQuickCommandTagsBoolean, (newValue) => { showQuickCommandTagsLocal.value = newValue; }, { immediate: true });
watch(terminalScrollbackLimitNumber, (newValue) => { terminalScrollbackLimitLocal.value = newValue; }, { immediate: true });
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
return {
popupEditorEnabled,
popupEditorLoading,
popupEditorMessage,
popupEditorSuccess,
handleUpdatePopupEditorSetting,
shareTabsEnabled,
shareTabsLoading,
shareTabsMessage,
shareTabsSuccess,
handleUpdateShareTabsSetting,
autoCopyEnabled,
autoCopyLoading,
autoCopyMessage,
autoCopySuccess,
handleUpdateAutoCopySetting,
workspaceSidebarPersistentEnabled,
workspaceSidebarPersistentLoading,
workspaceSidebarPersistentMessage,
workspaceSidebarPersistentSuccess,
handleUpdateWorkspaceSidebarSetting,
commandInputSyncTargetLocal,
commandInputSyncLoading,
commandInputSyncMessage,
commandInputSyncSuccess,
handleUpdateCommandInputSyncTarget,
showConnectionTagsLocal,
showConnectionTagsLoading,
showConnectionTagsMessage,
showConnectionTagsSuccess,
handleUpdateShowConnectionTags,
showQuickCommandTagsLocal,
showQuickCommandTagsLoading,
showQuickCommandTagsMessage,
showQuickCommandTagsSuccess,
handleUpdateShowQuickCommandTags,
terminalScrollbackLimitLocal,
terminalScrollbackLimitLoading,
terminalScrollbackLimitMessage,
terminalScrollbackLimitSuccess,
handleUpdateTerminalScrollbackLimit,
fileManagerShowDeleteConfirmationLocal,
fileManagerShowDeleteConfirmationLoading,
fileManagerShowDeleteConfirmationMessage,
fileManagerShowDeleteConfirmationSuccess,
handleUpdateFileManagerDeleteConfirmation,
};
}
File diff suppressed because it is too large Load Diff