refactor:优化settings代码结构
This commit is contained in:
@@ -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
Reference in New Issue
Block a user