From 3750d5cba0a50caa2097f63d348c78970d8bcea0 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 10 Sep 2025 14:46:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E9=85=8D=E7=BD=AE=E5=90=91?= =?UTF-8?q?=E5=AF=BC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useSetupWizard.ts | 634 ++++++++++++++++--- src/locales/en-US.ts | 47 +- src/locales/zh-CN.ts | 67 +- src/locales/zh-TW.ts | 36 +- src/pages/setup.vue | 17 +- src/views/setup/BasicSettingsStep.vue | 77 ++- src/views/setup/DownloaderSettingsStep.vue | 42 +- src/views/setup/MediaServerSettingsStep.vue | 71 ++- src/views/setup/NotificationSettingsStep.vue | 129 ++-- src/views/setup/StorageSettingsStep.vue | 20 +- 10 files changed, 911 insertions(+), 229 deletions(-) diff --git a/src/composables/useSetupWizard.ts b/src/composables/useSetupWizard.ts index 890ec6cd..da61f322 100644 --- a/src/composables/useSetupWizard.ts +++ b/src/composables/useSetupWizard.ts @@ -54,59 +54,134 @@ export interface ConnectivityTestState { showResult: boolean } +export interface ValidationErrorState { + downloader: { + name: boolean + host: boolean + username: boolean + password: boolean + } + mediaServer: { + name: boolean + host: boolean + apikey: boolean + token: boolean + username: boolean + password: boolean + } + notification: { + name: boolean + [key: string]: boolean + } +} + +// 全局状态,所有组件共享 +const currentStep = ref(1) +const totalSteps = 6 + +// 选中的预设规则 +const selectedPreset = ref('') + +// 向导数据 +const wizardData = ref({ + basic: { + appDomain: '', + apiToken: '', + username: '', + password: '', + confirmPassword: '', + proxyHost: '', + githubToken: '', + }, + storage: { + downloadPath: '', + libraryPath: '', + transferType: 'link', + overwriteMode: 'never', + }, + downloader: { + type: '', + name: '', + config: {}, + }, + mediaServer: { + type: '', + name: '', + config: {}, + sync_libraries: [], + }, + notification: { + type: '', + name: '', + enabled: false, + config: {}, + switchs: [], + }, + preferences: { + quality: '4K', + subtitle: 'chinese', + resolution: '2160p', + }, +}) + +// 连通性测试状态 +const connectivityTest = ref({ + isTesting: false, + testMessage: '', + testProgress: 0, + testResult: null, + showResult: false, +}) + +// 验证错误状态 +const validationErrors = ref({ + downloader: { + name: false, + host: false, + username: false, + password: false, + }, + mediaServer: { + name: false, + host: false, + apikey: false, + token: false, + username: false, + password: false, + }, + notification: { + name: false, + }, +}) + export function useSetupWizard() { const { t } = useI18n() const router = useRouter() const $toast = useToast() - // 当前步骤 - const currentStep = ref(1) - const totalSteps = 6 - - // 选中的预设规则 - const selectedPreset = ref('') - - // 向导数据 - const wizardData = ref({ - basic: { - appDomain: '', - apiToken: '', - username: '', - password: '', - confirmPassword: '', - proxyHost: '', - githubToken: '', - }, - storage: { - downloadPath: '', - libraryPath: '', - transferType: 'link', - overwriteMode: 'never', - }, + // 类型到模块ID的映射关系 + const typeToModuleMapping = { + // 下载器映射 downloader: { - type: '', - name: '', - config: {}, + 'qbittorrent': 'QbittorrentModule', + 'transmission': 'TransmissionModule', }, + // 媒体服务器映射 mediaServer: { - type: '', - name: '', - config: {}, - sync_libraries: [], + 'emby': 'EmbyModule', + 'jellyfin': 'JellyfinModule', + 'plex': 'PlexModule', }, + // 通知映射 notification: { - type: '', - name: '', - enabled: false, - config: {}, - switchs: [], + 'telegram': 'TelegramModule', + 'wechat': 'WechatModule', + 'slack': 'SlackModule', + 'synologychat': 'SynologyChatModule', + 'vocechat': 'VoceChatModule', + 'webpush': 'WebPushModule', }, - preferences: { - quality: '4K', - subtitle: 'chinese', - resolution: '2160p', - }, - }) + } // 步骤标题 const stepTitles = computed(() => [ @@ -128,15 +203,6 @@ export function useSetupWizard() { t('setupWizard.preferences.description'), ]) - // 连通性测试状态 - const connectivityTest = ref({ - isTesting: false, - testMessage: '', - testProgress: 0, - testResult: null, - showResult: false, - }) - // 创建随机API Token function createRandomString() { const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_' @@ -162,23 +228,50 @@ export function useSetupWizard() { // 选择下载器 function selectDownloader(type: string) { - wizardData.value.downloader.type = type - wizardData.value.downloader.name = `${type} 下载器` - wizardData.value.downloader.config = {} + if (wizardData.value.downloader.type === type) { + // 重复点击已选中的类型,取消选择 + wizardData.value.downloader.type = '' + wizardData.value.downloader.name = '' + wizardData.value.downloader.config = {} + } else { + wizardData.value.downloader.type = type + wizardData.value.downloader.name = `${type} 下载器` + wizardData.value.downloader.config = {} + } } // 选择媒体服务器 function selectMediaServer(type: string) { - wizardData.value.mediaServer.type = type - wizardData.value.mediaServer.name = `${type} 服务器` - wizardData.value.mediaServer.config = {} + if (wizardData.value.mediaServer.type === type) { + // 重复点击已选中的类型,取消选择 + wizardData.value.mediaServer.type = '' + wizardData.value.mediaServer.name = '' + wizardData.value.mediaServer.config = {} + wizardData.value.mediaServer.sync_libraries = [] + } else { + wizardData.value.mediaServer.type = type + wizardData.value.mediaServer.name = `${type} 服务器` + wizardData.value.mediaServer.config = {} + wizardData.value.mediaServer.sync_libraries = [] + } } // 选择通知 function selectNotification(type: string) { - wizardData.value.notification.type = type - wizardData.value.notification.name = `${type} 通知` - wizardData.value.notification.config = {} + if (wizardData.value.notification.type === type) { + // 重复点击已选中的类型,取消选择 + wizardData.value.notification.type = '' + wizardData.value.notification.name = '' + wizardData.value.notification.enabled = false + wizardData.value.notification.config = {} + wizardData.value.notification.switchs = [] + } else { + wizardData.value.notification.type = type + wizardData.value.notification.name = `${type} 通知` + wizardData.value.notification.enabled = true + wizardData.value.notification.config = {} + wizardData.value.notification.switchs = [] + } } // 选择预设规则 @@ -204,6 +297,264 @@ export function useSetupWizard() { } } + // 清除验证错误状态 + function clearValidationErrors() { + validationErrors.value.downloader = { + name: false, + host: false, + username: false, + password: false, + } + validationErrors.value.mediaServer = { + name: false, + host: false, + apikey: false, + token: false, + username: false, + password: false, + } + validationErrors.value.notification = { + name: false, + } + } + + // 验证下载器字段 + function validateDownloaderFields(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + clearValidationErrors() + + // 名称必输 + if (!wizardData.value.downloader.name?.trim()) { + errors.push(t('downloader.nameRequired')) + validationErrors.value.downloader.name = true + } + + // 主机地址必输 + if (!wizardData.value.downloader.config?.host?.trim()) { + errors.push(t('downloader.hostRequired')) + validationErrors.value.downloader.host = true + } + + // 根据下载器类型验证其他必输项 + if (wizardData.value.downloader.type === 'qbittorrent' || wizardData.value.downloader.type === 'transmission') { + if (!wizardData.value.downloader.config?.username?.trim()) { + errors.push(t('downloader.usernameRequired')) + validationErrors.value.downloader.username = true + } + if (!wizardData.value.downloader.config?.password?.trim()) { + errors.push(t('downloader.passwordRequired')) + validationErrors.value.downloader.password = true + } + } + + return { + isValid: errors.length === 0, + errors, + } + } + + // 验证媒体服务器字段 + function validateMediaServerFields(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + clearValidationErrors() + + // 名称必输 + if (!wizardData.value.mediaServer.name?.trim()) { + errors.push(t('mediaserver.nameRequired')) + validationErrors.value.mediaServer.name = true + } + + // 主机地址必输 + if (!wizardData.value.mediaServer.config?.host?.trim()) { + errors.push(t('mediaserver.hostRequired')) + validationErrors.value.mediaServer.host = true + } + + // 根据媒体服务器类型验证API密钥或Token + if (wizardData.value.mediaServer.type === 'emby' || wizardData.value.mediaServer.type === 'jellyfin') { + if (!wizardData.value.mediaServer.config?.apikey?.trim()) { + errors.push(t('mediaserver.apiKeyRequired')) + validationErrors.value.mediaServer.apikey = true + } + } else if (wizardData.value.mediaServer.type === 'plex') { + if (!wizardData.value.mediaServer.config?.token?.trim()) { + errors.push(t('mediaserver.tokenRequired')) + validationErrors.value.mediaServer.token = true + } + } else if (wizardData.value.mediaServer.type === 'trimemedia') { + if (!wizardData.value.mediaServer.config?.username?.trim()) { + errors.push(t('mediaserver.usernameRequired')) + validationErrors.value.mediaServer.username = true + } + if (!wizardData.value.mediaServer.config?.password?.trim()) { + errors.push(t('mediaserver.passwordRequired')) + validationErrors.value.mediaServer.password = true + } + } + + return { + isValid: errors.length === 0, + errors, + } + } + + // 验证通知字段 + function validateNotificationFields(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + clearValidationErrors() + + // 名称必输 + if (!wizardData.value.notification.name?.trim()) { + errors.push(t('notification.nameRequired')) + validationErrors.value.notification.name = true + } + + // 根据通知类型验证必输项 + const config = wizardData.value.notification.config || {} + switch (wizardData.value.notification.type) { + case 'wechat': + if (!config.WECHAT_CORPID?.trim()) { + errors.push(t('notification.wechat.corpIdRequired')) + validationErrors.value.notification.WECHAT_CORPID = true + } + if (!config.WECHAT_APP_ID?.trim()) { + errors.push(t('notification.wechat.appIdRequired')) + validationErrors.value.notification.WECHAT_APP_ID = true + } + if (!config.WECHAT_APP_SECRET?.trim()) { + errors.push(t('notification.wechat.appSecretRequired')) + validationErrors.value.notification.WECHAT_APP_SECRET = true + } + break + case 'telegram': + if (!config.TELEGRAM_TOKEN?.trim()) { + errors.push(t('notification.telegram.tokenRequired')) + validationErrors.value.notification.TELEGRAM_TOKEN = true + } + if (!config.TELEGRAM_CHAT_ID?.trim()) { + errors.push(t('notification.telegram.chatIdRequired')) + validationErrors.value.notification.TELEGRAM_CHAT_ID = true + } + break + case 'slack': + if (!config.SLACK_OAUTH_TOKEN?.trim()) { + errors.push(t('notification.slack.oauthTokenRequired')) + validationErrors.value.notification.SLACK_OAUTH_TOKEN = true + } + if (!config.SLACK_CHANNEL?.trim()) { + errors.push(t('notification.slack.channelRequired')) + validationErrors.value.notification.SLACK_CHANNEL = true + } + break + case 'synologychat': + if (!config.SYNOLOGYCHAT_WEBHOOK?.trim()) { + errors.push(t('notification.synologychat.webhookRequired')) + validationErrors.value.notification.SYNOLOGYCHAT_WEBHOOK = true + } + break + case 'vocechat': + if (!config.VOCECHAT_HOST?.trim()) { + errors.push(t('notification.vocechat.hostRequired')) + validationErrors.value.notification.VOCECHAT_HOST = true + } + if (!config.VOCECHAT_API_KEY?.trim()) { + errors.push(t('notification.vocechat.apiKeyRequired')) + validationErrors.value.notification.VOCECHAT_API_KEY = true + } + break + } + + return { + isValid: errors.length === 0, + errors, + } + } + + // 验证当前步骤的必输项 + function validateCurrentStep(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + + switch (currentStep.value) { + case 1: // 基础设置 + if (!wizardData.value.basic.username) { + errors.push(t('dialog.userAddEdit.usernameRequired')) + } + // 密码是可选的,但如果输入了密码则需要验证 + if (wizardData.value.basic.password) { + if (wizardData.value.basic.password.length < 6) { + errors.push(t('dialog.userAddEdit.passwordMinLength')) + } + if (!wizardData.value.basic.confirmPassword) { + errors.push(t('dialog.userAddEdit.confirmPasswordRequired')) + } else if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) { + errors.push(t('dialog.userAddEdit.passwordMismatch')) + } + } + if (!wizardData.value.basic.apiToken) { + errors.push(t('setupWizard.basic.apiTokenRequired')) + } + break + + case 2: // 存储设置 + if (!wizardData.value.storage.downloadPath) { + errors.push(t('setupWizard.storage.downloadPathRequired')) + } + if (!wizardData.value.storage.libraryPath) { + errors.push(t('setupWizard.storage.libraryPathRequired')) + } + break + + case 3: // 下载器设置 + if (wizardData.value.downloader.type) { + // 如果选择了下载器,则验证必输项 + const validation = validateDownloaderFields() + errors.push(...validation.errors) + } + break + + case 4: // 媒体服务器设置 + if (wizardData.value.mediaServer.type) { + // 如果选择了媒体服务器,则验证必输项 + const validation = validateMediaServerFields() + errors.push(...validation.errors) + } + break + + case 5: // 通知设置 + if (wizardData.value.notification.type) { + // 如果选择了通知,则验证必输项 + const validation = validateNotificationFields() + errors.push(...validation.errors) + } + break + + case 6: // 偏好设置 + // 偏好设置有默认值,不需要验证 + break + } + + return { + isValid: errors.length === 0, + errors, + } + } + + // 检查是否需要进行测试 + function shouldPerformTest(step: number): boolean { + switch (step) { + case 2: // 存储目录测试 - 总是需要测试 + return true + case 3: // 下载器测试 - 只有选择了下载器才测试 + return !!wizardData.value.downloader.type + case 4: // 媒体服务器测试 - 只有选择了媒体服务器才测试 + return !!wizardData.value.mediaServer.type + case 5: // 消息通知测试 - 只有选择了通知才测试 + return !!wizardData.value.notification.type + default: + return false + } + } + // 连通性测试函数 async function testConnectivity(step: number) { connectivityTest.value.isTesting = true @@ -238,17 +589,17 @@ export function useSetupWizard() { // 根据结果显示不同的消息 if (testResult.success) { connectivityTest.value.testMessage = t('setupWizard.connectivityTestSuccess') + $toast.success(t('setupWizard.connectivityTestSuccess')) } else { // 显示API返回的具体错误原因 connectivityTest.value.testMessage = testResult.message || t('setupWizard.connectivityTestFailed') + $toast.error(testResult.message || t('setupWizard.connectivityTestFailed')) } // 成功时2秒后隐藏结果,失败时保持显示直到用户操作 if (testResult.success) { - setTimeout(() => { - connectivityTest.value.showResult = false - connectivityTest.value.testResult = null - }, 2000) + connectivityTest.value.showResult = false + connectivityTest.value.testResult = null } return testResult.success @@ -258,6 +609,7 @@ export function useSetupWizard() { connectivityTest.value.testResult = 'error' connectivityTest.value.showResult = true connectivityTest.value.testMessage = (error as Error).message || t('setupWizard.connectivityTestFailed') + $toast.error((error as Error).message || t('setupWizard.connectivityTestFailed')) return false } } @@ -274,14 +626,14 @@ export function useSetupWizard() { connectivityTest.value.testProgress = 60 connectivityTest.value.testMessage = t('setupWizard.checkingStorage') - // 调用存储测试API - const result = await api.get('system/storagetest') + // 调用存储测试API - 使用FileManagerModule + const result: { [key: string]: any } = await api.get('system/moduletest/FileManagerModule') connectivityTest.value.testProgress = 100 - if (result.data?.success) { + if (result.success) { return { success: true, message: null } } else { - return { success: false, message: result.data?.message || t('setupWizard.storageTestFailed') } + return { success: false, message: result.message || t('setupWizard.storageTestFailed') } } } catch (error) { console.error('Storage test failed:', error) @@ -301,19 +653,24 @@ export function useSetupWizard() { connectivityTest.value.testProgress = 60 connectivityTest.value.testMessage = t('setupWizard.checkingDownloader') - // 调用下载器测试API - const moduleid = wizardData.value.downloader.type - if (!moduleid) { + // 获取正确的模块ID + const downloaderType = wizardData.value.downloader.type + if (!downloaderType) { return { success: false, message: t('setupWizard.downloaderNotSelected') } } + const moduleid = typeToModuleMapping.downloader[downloaderType as keyof typeof typeToModuleMapping.downloader] + if (!moduleid) { + return { success: false, message: t('setupWizard.unsupportedDownloaderType', { type: downloaderType }) } + } + const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`) connectivityTest.value.testProgress = 100 - if (result.data?.success) { + if (result.success) { return { success: true, message: null } } else { - return { success: false, message: result.data?.message || t('setupWizard.downloaderTestFailed') } + return { success: false, message: result.message || t('setupWizard.downloaderTestFailed') } } } catch (error) { console.error('Downloader test failed:', error) @@ -333,19 +690,24 @@ export function useSetupWizard() { connectivityTest.value.testProgress = 60 connectivityTest.value.testMessage = t('setupWizard.checkingMediaServer') - // 调用媒体服务器测试API - const moduleid = wizardData.value.mediaServer.type - if (!moduleid) { + // 获取正确的模块ID + const mediaServerType = wizardData.value.mediaServer.type + if (!mediaServerType) { return { success: false, message: t('setupWizard.mediaServerNotSelected') } } + const moduleid = typeToModuleMapping.mediaServer[mediaServerType as keyof typeof typeToModuleMapping.mediaServer] + if (!moduleid) { + return { success: false, message: t('setupWizard.unsupportedMediaServerType', { type: mediaServerType }) } + } + const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`) connectivityTest.value.testProgress = 100 - if (result.data?.success) { + if (result.success) { return { success: true, message: null } } else { - return { success: false, message: result.data?.message || t('setupWizard.mediaServerTestFailed') } + return { success: false, message: result.message || t('setupWizard.mediaServerTestFailed') } } } catch (error) { console.error('Media server test failed:', error) @@ -365,19 +727,25 @@ export function useSetupWizard() { connectivityTest.value.testProgress = 60 connectivityTest.value.testMessage = t('setupWizard.checkingNotification') - // 调用通知测试API - const moduleid = wizardData.value.notification.type - if (!moduleid) { + // 获取正确的模块ID + const notificationType = wizardData.value.notification.type + if (!notificationType) { return { success: false, message: t('setupWizard.notificationNotSelected') } } + const moduleid = + typeToModuleMapping.notification[notificationType as keyof typeof typeToModuleMapping.notification] + if (!moduleid) { + return { success: false, message: t('setupWizard.unsupportedNotificationType', { type: notificationType }) } + } + const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`) connectivityTest.value.testProgress = 100 - if (result.data?.success) { + if (result.success) { return { success: true, message: null } } else { - return { success: false, message: result.data?.message || t('setupWizard.notificationTestFailed') } + return { success: false, message: result.message || t('setupWizard.notificationTestFailed') } } } catch (error) { console.error('Notification test failed:', error) @@ -388,11 +756,22 @@ export function useSetupWizard() { // 下一步 async function nextStep() { if (currentStep.value < totalSteps) { + // 验证当前步骤的必输项 + const validation = validateCurrentStep() + if (!validation.isValid) { + // 显示验证错误 + validation.errors.forEach(error => { + $toast.error(error) + }) + return + } + // 保存当前步骤的设置 await saveCurrentStepSettings() - // 对于需要测试的步骤,进行连通性测试 - if ([2, 3, 4, 5].includes(currentStep.value)) { + // 检查是否需要进行测试 + const needsTest = shouldPerformTest(currentStep.value) + if (needsTest) { const testResult = await testConnectivity(currentStep.value) if (!testResult) { return @@ -400,6 +779,7 @@ export function useSetupWizard() { } currentStep.value++ + connectivityTest.value.showResult = false } } @@ -408,6 +788,7 @@ export function useSetupWizard() { if (currentStep.value > 1) { currentStep.value-- } + connectivityTest.value.showResult = false } // 保存当前步骤的设置 @@ -442,15 +823,16 @@ export function useSetupWizard() { // 完成向导 async function completeWizard() { try { - // 验证密码一致性 - if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) { - $toast.error(t('user.passwordMismatch')) - return + // 如果输入了密码,验证密码一致性 + if (wizardData.value.basic.password) { + if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) { + $toast.error(t('dialog.userAddEdit.passwordMismatch')) + return + } + // 更新用户密码 + await updateUserPassword() } - // 创建用户(如果还没有创建) - await createUser() - // 保存最后一步的设置(资源偏好) await savePreferenceSettings() @@ -462,16 +844,28 @@ export function useSetupWizard() { } } - // 创建用户 - async function createUser() { + // 更新用户密码 + async function updateUserPassword() { if (wizardData.value.basic.username && wizardData.value.basic.password) { try { - // 检查用户是否已存在 + // 获取当前用户信息 const response = await api.get('user/') const existingUsers = response.data || [] - const userExists = existingUsers.some((user: any) => user.name === wizardData.value.basic.username) + const currentUser = existingUsers.find((user: any) => user.name === wizardData.value.basic.username) - if (!userExists) { + if (currentUser) { + // 更新现有用户的密码 + const userData = { + name: wizardData.value.basic.username, + password: wizardData.value.basic.password, + is_active: currentUser.is_active, + is_superuser: currentUser.is_superuser, + } + + await api.put(`user/${currentUser.id}`, userData) + $toast.success(t('setupWizard.passwordUpdateSuccess')) + } else { + // 如果用户不存在,创建新用户(通常不会发生) const userData = { name: wizardData.value.basic.username, password: wizardData.value.basic.password, @@ -480,9 +874,12 @@ export function useSetupWizard() { } await api.post('user/', userData) + $toast.success(t('setupWizard.userCreateSuccess')) } } catch (error) { - console.log('Create user failed or user already exists:', error) + console.error('Update user password failed:', error) + $toast.error(t('setupWizard.passwordUpdateFailed')) + throw error } } } @@ -526,11 +923,18 @@ export function useSetupWizard() { download_path: wizardData.value.storage.downloadPath, library_path: wizardData.value.storage.libraryPath, priority: 0, - monitor_type: 'compatibility', - media_type: 'movie,tv', - media_category: 'default', + monitor_type: 'downloader', + media_type: '', + media_category: '', + download_type_folder: true, + download_category_folder: true, transfer_type: wizardData.value.storage.transferType, overwrite_mode: wizardData.value.storage.overwriteMode, + renaming: true, + scraping: true, + notify: true, + library_type_folder: true, + library_category_folder: true, } const response = await api.post('system/setting/Directories', [directory]) @@ -563,6 +967,9 @@ export function useSetupWizard() { console.error('Save downloader settings failed:', error) $toast.error(t('setupWizard.saveDownloaderSettingsFailed')) } + } else { + // 没有选择下载器时,清空现有配置 + console.log('No downloader selected, skipping save') } } @@ -585,6 +992,9 @@ export function useSetupWizard() { console.error('Save media server settings failed:', error) $toast.error(t('setupWizard.saveMediaServerSettingsFailed')) } + } else { + // 没有选择媒体服务器时,清空现有配置 + console.log('No media server selected, skipping save') } } @@ -607,6 +1017,9 @@ export function useSetupWizard() { console.error('Save notification settings failed:', error) $toast.error(t('setupWizard.saveNotificationSettingsFailed')) } + } else { + // 没有选择通知时,清空现有配置 + console.log('No notification selected, skipping save') } } @@ -649,6 +1062,9 @@ export function useSetupWizard() { if (result.data.GITHUB_TOKEN) { wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN } + if (result.data.SUPERUSER) { + wizardData.value.basic.username = result.data.SUPERUSER + } } } catch (error) { console.log('Load system settings failed:', error) @@ -741,13 +1157,17 @@ export function useSetupWizard() { // 初始化 async function initialize() { - createRandomString() await loadSystemSettings() await loadStorageSettings() await loadDownloaderSettings() await loadMediaServerSettings() await loadNotificationSettings() await loadPreferenceSettings() + + // 如果没有API Token,则创建一个随机的 + if (!wizardData.value.basic.apiToken) { + createRandomString() + } } return { @@ -759,7 +1179,8 @@ export function useSetupWizard() { wizardData, selectedPreset, connectivityTest, - + validationErrors, + // 方法 createRandomString, copyValue, @@ -767,10 +1188,15 @@ export function useSetupWizard() { selectMediaServer, selectNotification, selectPreset, + validateCurrentStep, + validateDownloaderFields, + validateMediaServerFields, + validateNotificationFields, + clearValidationErrors, testConnectivity, nextStep, prevStep, completeWizard, initialize, } -} \ No newline at end of file +} diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 7283d2ed..e49ef646 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -414,10 +414,13 @@ export default { name: 'WeChat Work', corpId: 'Corp ID', corpIdHint: 'Corp ID in WeChat Work backend enterprise information', + corpIdRequired: 'Corp ID cannot be empty', appId: 'App AgentId', appIdHint: 'AgentId of self-built app in WeChat Work', + appIdRequired: 'App AgentId cannot be empty', appSecret: 'App Secret', appSecretHint: 'Secret of self-built app in WeChat Work', + appSecretRequired: 'App Secret cannot be empty', proxy: 'Proxy Address', proxyHint: 'Proxy address for WeChat message forwarding, required for self-built apps created after June 20, 2022', @@ -433,8 +436,10 @@ export default { name: 'Telegram', token: 'Bot Token', tokenHint: 'Telegram bot token, format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', + tokenRequired: 'Bot Token cannot be empty', chatId: 'Chat ID', chatIdHint: 'Chat ID of user, group or channel that receives notifications', + chatIdRequired: 'Chat ID cannot be empty', users: 'User Whitelist', usersHint: 'User IDs that can use Telegram bot, separated by commas. Leave empty to allow all users', admins: 'Admin Whitelist', @@ -449,15 +454,18 @@ export default { name: 'Slack', oauthToken: 'Slack Bot User OAuth Token', oauthTokenHint: 'Bot User OAuth Token in Slack app OAuth & Permissions page', + oauthTokenRequired: 'OAuth Token cannot be empty', appToken: 'Slack App-Level Token', appTokenHint: 'App-Level Token in Slack app OAuth & Permissions page', channel: 'Channel Name', channelHint: 'Channel to send messages, default is "all"', + channelRequired: 'Channel Name cannot be empty', }, synologychat: { name: 'Synology Chat', webhook: 'Webhook URL', webhookHint: 'Synology Chat bot webhook URL', + webhookRequired: 'Webhook URL cannot be empty', token: 'Token', tokenHint: 'Synology Chat bot token', }, @@ -465,8 +473,10 @@ export default { name: 'VoceChat', host: 'Address', hostHint: 'VoceChat server address, format: http(s)://ip:port', + hostRequired: 'Address cannot be empty', apiKey: 'Bot API Key', apiKeyHint: 'VoceChat bot API key', + apiKeyRequired: 'API Key cannot be empty', channelId: 'Channel ID', channelIdHint: 'VoceChat channel ID, without #', }, @@ -474,6 +484,7 @@ export default { name: 'WebPush', username: 'Login Username', usernameHint: 'Only push messages to the corresponding logged-in user', + usernameRequired: 'Username cannot be empty', }, }, shortcut: { @@ -1778,8 +1789,12 @@ export default { add: 'Add User', edit: 'Edit User', username: 'Username', + usernameRequired: 'Username cannot be empty', password: 'Password', + passwordMinLength: 'Password must be at least 6 characters', confirmPassword: 'Confirm Password', + confirmPasswordRequired: 'Please confirm password', + passwordMismatch: 'Passwords do not match', email: 'Email', nickname: 'Nickname', status: 'Status', @@ -1800,9 +1815,7 @@ export default { webPush: 'WebPush', creatingUser: 'Creating user [{name}], please wait', updatingUser: 'Updating user [{name}], please wait', - usernameRequired: 'Username cannot be empty', usernameExists: 'Username already exists', - passwordMismatch: 'The two passwords do not match', userCreated: 'User [{name}] created successfully', userCreateFailed: 'Failed to create user: {message}', userUpdateSuccess: 'User [{name}] updated successfully', @@ -2633,6 +2646,9 @@ export default { nameRequired: 'Name cannot be empty', nameDuplicate: 'Name already exists', defaultChanged: 'Default downloader exists, has been replaced', + hostRequired: 'Host cannot be empty', + usernameRequired: 'Username cannot be empty', + passwordRequired: 'Password cannot be empty', }, filterRule: { title: 'Filter Rule', @@ -2680,6 +2696,11 @@ export default { password: 'Password', syncLibraries: 'Sync Libraries', syncLibrariesHint: 'Only selected libraries will be synchronized', + hostRequired: 'Host cannot be empty', + apiKeyRequired: 'API Key cannot be empty', + tokenRequired: 'Token cannot be empty', + usernameRequired: 'Username cannot be empty', + passwordRequired: 'Password cannot be empty', nameExists: '【{name}】 already exists, please use a different name', }, bangumi: { @@ -2904,6 +2925,12 @@ export default { testingNotification: 'Testing notification', checkingNotification: 'Checking notification connectivity', testFailedHint: 'Please check if the configuration is correct, you can retest after modification', + unsupportedDownloaderType: 'Unsupported downloader type: {type}', + unsupportedMediaServerType: 'Unsupported media server type: {type}', + unsupportedNotificationType: 'Unsupported notification type: {type}', + passwordUpdateSuccess: 'Password updated successfully', + userCreateSuccess: 'User created successfully', + passwordUpdateFailed: 'Failed to update password', basic: { title: 'Basic Settings', description: 'Set access domain, username/password and network configuration', @@ -2915,6 +2942,10 @@ export default { recognizeSourceHint: 'Set the default media info recognition data source', apiToken: 'API Token', apiTokenHint: 'System automatically generated API access token', + currentUserHint: 'Current user, cannot be modified', + passwordOptionalHint: 'Leave blank to keep current password', + confirmPasswordHint: 'Confirm new password', + apiTokenRequired: 'API Token is required', }, storage: { title: 'Storage Configuration', @@ -2925,12 +2956,14 @@ export default { downloadPathHint: 'Set the storage path for downloaded files', libraryPath: 'Media Library Directory', libraryPathHint: 'Set the storage path for media files', + downloadPathRequired: 'Download directory is required', + libraryPathRequired: 'Media library directory is required', }, downloader: { title: 'Downloader Configuration', - description: 'Configure downloader (optional)', + description: 'Configure downloader', info: 'Downloader Configuration', - infoDesc: 'Configure downloader for automatic resource download (optional)', + infoDesc: 'Configure downloader for resource download, can choose qBittorrent or Transmission', type: 'Downloader Type', typeHint: 'Select the type of downloader to use', name: 'Downloader Name', @@ -2944,9 +2977,9 @@ export default { }, mediaServer: { title: 'Media Server', - description: 'Configure media server (optional)', + description: 'Configure media server', info: 'Media Server Configuration', - infoDesc: 'Configure media server for media library management (optional)', + infoDesc: 'Configure media server for media library management, can choose Emby, Jellyfin or Plex etc.', type: 'Media Server Type', typeHint: 'Select the type of media server to use', name: 'Server Name', @@ -2960,7 +2993,7 @@ export default { }, notification: { title: 'Notification Settings', - description: 'Configure notification channels (optional)', + description: 'Configure notification channels', info: 'Notification Configuration', infoDesc: 'Configure notification channels for receiving system messages (optional)', type: 'Notification Type', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index f6f2dac0..527e08c9 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -412,10 +412,13 @@ export default { name: '企业微信', corpId: '企业ID', corpIdHint: '企业微信后台企业信息中的企业ID', + corpIdRequired: '企业ID不能为空', appId: '应用 AgentId', appIdHint: '企业微信自建应用的AgentId', + appIdRequired: '应用AgentId不能为空', appSecret: '应用 Secret', appSecretHint: '企业微信自建应用的Secret', + appSecretRequired: '应用Secret不能为空', proxy: '代理地址', proxyHint: '微信消息的转发代理地址,2022年6月20日后创建的自建应用才需要,不使用代理时需要保留默认值', token: 'Token', @@ -430,8 +433,10 @@ export default { name: 'Telegram', token: 'Bot Token', tokenHint: 'Telegram机器人token,格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', + tokenRequired: 'Bot Token不能为空', chatId: 'Chat ID', chatIdHint: '接受消息通知的用户、群组或频道Chat ID', + chatIdRequired: 'Chat ID不能为空', users: '用户白名单', usersHint: '可使用Telegram机器人的用户ID清单,多个用户用,分隔,不填写则所有用户都能使用', admins: '管理员白名单', @@ -446,15 +451,18 @@ export default { name: 'Slack', oauthToken: 'Slack Bot User OAuth Token', oauthTokenHint: 'Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`', + oauthTokenRequired: 'OAuth Token不能为空', appToken: 'Slack App-Level Token', appTokenHint: 'Slack应用`OAuth & Permissions`页面中的`App-Level Token`', channel: '频道名称', channelHint: '消息发送频道,默认`全体`', + channelRequired: '频道名称不能为空', }, synologychat: { name: 'Synology Chat', webhook: '机器人传入URL', webhookHint: 'Synology Chat机器人传入URL', + webhookRequired: 'Webhook URL不能为空', token: '令牌', tokenHint: 'Synology Chat机器人令牌', }, @@ -462,8 +470,10 @@ export default { name: 'VoceChat', host: '地址', hostHint: 'VoceChat服务端地址,格式:http(s)://ip:port', + hostRequired: '地址不能为空', apiKey: '机器人密钥', apiKeyHint: 'VoceChat机器人密钥', + apiKeyRequired: 'API密钥不能为空', channelId: '频道ID', channelIdHint: 'VoceChat的频道ID,不包含#号', }, @@ -471,6 +481,7 @@ export default { name: 'WebPush', username: '登录用户名', usernameHint: '只有对应的用户登录后才会推送消息', + usernameRequired: '用户名不能为空', }, }, shortcut: { @@ -1754,8 +1765,12 @@ export default { add: '添加用户', edit: '编辑用户', username: '用户名', + usernameRequired: '用户名不能为空', password: '密码', + passwordMinLength: '密码长度不能少于6位', confirmPassword: '确认密码', + confirmPasswordRequired: '请确认密码', + passwordMismatch: '两次输入的密码不一致', email: '邮箱', nickname: '昵称', status: '状态', @@ -1778,6 +1793,8 @@ export default { updatingUser: '正在更新【{name}】用户,请稍后', usernameRequired: '用户名不能为空', usernameExists: '用户名已存在', + passwordMinLength: '密码长度不能少于6位字符', + confirmPasswordRequired: '请确认密码', passwordMismatch: '两次输入的密码不一致', userCreated: '用户【{name}】创建成功', userCreateFailed: '创建用户失败:{message}', @@ -2601,6 +2618,9 @@ export default { nameRequired: '不能为空,且不能重名', nameDuplicate: '名称已存在', defaultChanged: '存在默认下载器,已替换', + hostRequired: '地址不能为空', + usernameRequired: '用户名不能为空', + passwordRequired: '密码不能为空', }, filterRule: { title: '过滤规则', @@ -2649,6 +2669,11 @@ export default { syncLibraries: '同步媒体库', syncLibrariesHint: '只有选中的媒体库才会被同步', nameExists: '【{name}】已存在,请替换为其他名称', + hostRequired: '地址不能为空', + apiKeyRequired: 'API密钥不能为空', + tokenRequired: 'Token不能为空', + usernameRequired: '用户名不能为空', + passwordRequired: '密码不能为空', }, bangumi: { category: '类别', @@ -2864,13 +2889,39 @@ export default { connectivityTestFailed: '连通性测试失败', testingStorage: '正在测试存储目录', checkingStorage: '检查存储目录连通性', + storageTestFailed: '存储目录测试失败', testingDownloader: '正在测试下载器', checkingDownloader: '检查下载器连通性', + downloaderTestFailed: '下载器测试失败', + downloaderNotSelected: '未选择下载器', + unsupportedDownloaderType: '不支持的下载器类型: {type}', testingMediaServer: '正在测试媒体服务器', checkingMediaServer: '检查媒体服务器连通性', + mediaServerTestFailed: '媒体服务器测试失败', + mediaServerNotSelected: '未选择媒体服务器', + unsupportedMediaServerType: '不支持的媒体服务器类型: {type}', testingNotification: '正在测试消息通知', checkingNotification: '检查消息通知连通性', + notificationTestFailed: '消息通知测试失败', + notificationNotSelected: '未选择通知类型', + unsupportedNotificationType: '不支持的通知类型: {type}', testFailedHint: '请检查配置是否正确,修改后可以重新测试', + saveStepFailed: '保存步骤设置失败', + basicSettingsSaved: '基础设置保存成功', + saveBasicSettingsFailed: '保存基础设置失败', + storageSettingsSaved: '存储设置保存成功', + saveStorageSettingsFailed: '保存存储设置失败', + downloaderSettingsSaved: '下载器设置保存成功', + saveDownloaderSettingsFailed: '保存下载器设置失败', + mediaServerSettingsSaved: '媒体服务器设置保存成功', + saveMediaServerSettingsFailed: '保存媒体服务器设置失败', + notificationSettingsSaved: '通知设置保存成功', + saveNotificationSettingsFailed: '保存通知设置失败', + preferenceSettingsSaved: '偏好设置保存成功', + savePreferenceSettingsFailed: '保存偏好设置失败', + passwordUpdateSuccess: '密码更新成功', + passwordUpdateFailed: '密码更新失败', + userCreateSuccess: '用户创建成功', basic: { title: '基础设置', description: '设置访问域名、用户名密码和网络配置', @@ -2882,6 +2933,10 @@ export default { recognizeSourceHint: '设置默认媒体信息识别数据源', apiToken: 'API 令牌', apiTokenHint: '系统自动生成的 API 访问令牌', + currentUserHint: '当前用户,不可修改', + passwordOptionalHint: '留空表示不修改密码', + confirmPasswordHint: '确认新密码', + apiTokenRequired: 'API Token不能为空', }, storage: { title: '存储配置', @@ -2892,12 +2947,14 @@ export default { downloadPathHint: '设置下载文件的存储路径', libraryPath: '媒体库目录', libraryPathHint: '设置媒体文件的存储路径', + downloadPathRequired: '下载目录不能为空', + libraryPathRequired: '媒体库目录不能为空', }, downloader: { title: '下载器配置', - description: '配置下载器(可选)', + description: '配置下载器', info: '下载器配置说明', - infoDesc: '配置下载器用于自动下载资源(可选)', + infoDesc: '配置下载器用于下载资源,可选择qBittorrent或Transmission', type: '下载器类型', typeHint: '选择要使用的下载器类型', name: '下载器名称', @@ -2911,9 +2968,9 @@ export default { }, mediaServer: { title: '媒体服务器', - description: '配置媒体服务器(可选)', + description: '配置媒体服务器', info: '媒体服务器配置说明', - infoDesc: '配置媒体服务器用于媒体库管理(可选)', + infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin或Plex等', type: '媒体服务器类型', typeHint: '选择要使用的媒体服务器类型', name: '服务器名称', @@ -2927,7 +2984,7 @@ export default { }, notification: { title: '通知设置', - description: '配置通知渠道(可选)', + description: '配置通知渠道', info: '通知配置说明', infoDesc: '配置通知渠道用于接收系统消息(可选)', type: '通知类型', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index e542c318..462ad763 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1753,8 +1753,12 @@ export default { add: '添加用戶', edit: '編輯用戶', username: '用戶名', + usernameRequired: '用戶名不能為空', password: '密碼', + passwordMinLength: '密碼長度不能少於6位', confirmPassword: '確認密碼', + confirmPasswordRequired: '請確認密碼', + passwordMismatch: '兩次輸入的密碼不一致', email: '郵箱', nickname: '暱稱', status: '狀態', @@ -1775,9 +1779,7 @@ export default { webPush: 'WebPush', creatingUser: '正在創建【{name}】用戶,請稍後', updatingUser: '正在更新【{name}】用戶,請稍後', - usernameRequired: '用戶名不能為空', usernameExists: '用戶名已存在', - passwordMismatch: '兩次輸入的密碼不一致', userCreated: '用戶【{name}】創建成功', userCreateFailed: '創建用戶失敗:{message}', userUpdateSuccess: '用戶【{name}】更新成功', @@ -2600,6 +2602,9 @@ export default { nameRequired: '名稱不能為空', nameDuplicate: '名稱已存在', defaultChanged: '存在預設下載器,已替換', + hostRequired: '地址不能為空', + usernameRequired: '用戶名不能為空', + passwordRequired: '密碼不能為空', }, filterRule: { title: '過濾規則', @@ -2635,13 +2640,18 @@ export default { host: '地址', hostPlaceholder: 'http(s)://ip:port', hostHint: '服務端地址,格式:http(s)://ip:port', + hostRequired: '地址不能為空', playHost: '外網播放地址', playHostPlaceholder: 'http(s)://domain:port', playHostHint: '跳轉播放頁面使用的地址,格式:http(s)://domain:port', apiKey: 'API密鑰', + apiKeyRequired: 'API密鑰不能為空', embyApiKeyHint: 'Emby設置->高級->API密鑰中生成的密鑰', jellyfinApiKeyHint: 'Jellyfin設置->高級->API密鑰中生成的密鑰', plexToken: 'X-Plex-Token', + tokenRequired: 'Token不能為空', + usernameRequired: '用戶名不能為空', + passwordRequired: '密碼不能為空', plexTokenHint: '瀏覽器F12->網絡,從Plex請求URL中獲取的X-Plex-Token', username: '用戶名', password: '密碼', @@ -2870,6 +2880,12 @@ export default { testingNotification: '正在測試消息通知', checkingNotification: '檢查消息通知連通性', testFailedHint: '請檢查配置是否正確,修改後可以重新測試', + unsupportedDownloaderType: '不支援的下載器類型: {type}', + unsupportedMediaServerType: '不支援的媒體服務器類型: {type}', + unsupportedNotificationType: '不支援的通知類型: {type}', + passwordUpdateSuccess: '密碼更新成功', + userCreateSuccess: '使用者建立成功', + passwordUpdateFailed: '密碼更新失敗', basic: { title: '基礎設定', description: '設定存取網域、用戶名密碼和網路配置', @@ -2881,6 +2897,10 @@ export default { recognizeSourceHint: '設定預設媒體資訊識別資料來源', apiToken: 'API 權杖', apiTokenHint: '系統自動產生的 API 存取權杖', + currentUserHint: '目前使用者,不可修改', + passwordOptionalHint: '留空表示不修改密碼', + confirmPasswordHint: '確認新密碼', + apiTokenRequired: 'API Token 不能為空', }, storage: { title: '儲存配置', @@ -2891,12 +2911,14 @@ export default { downloadPathHint: '設定下載檔案的儲存路徑', libraryPath: '媒體庫目錄', libraryPathHint: '設定媒體檔案的儲存路徑', + downloadPathRequired: '下載目錄不能為空', + libraryPathRequired: '媒體庫目錄不能為空', }, downloader: { title: '下載器配置', - description: '設定下載器(可選)', + description: '設定下載器', info: '下載器設定說明', - infoDesc: '設定下載器用於自動下載資源(可選)', + infoDesc: '設定下載器用於下載資源,可選擇qBittorrent或Transmission', type: '下載器類型', typeHint: '選擇要使用的下載器類型', name: '下載器名稱', @@ -2910,9 +2932,9 @@ export default { }, mediaServer: { title: '媒體伺服器', - description: '設定媒體伺服器(可選)', + description: '設定媒體伺服器', info: '媒體伺服器設定說明', - infoDesc: '設定媒體伺服器用於媒體庫管理(可選)', + infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin或Plex等', type: '媒體伺服器類型', typeHint: '選擇要使用的媒體伺服器類型', name: '伺服器名稱', @@ -2926,7 +2948,7 @@ export default { }, notification: { title: '通知設定', - description: '設定通知管道(可選)', + description: '設定通知管道', info: '通知設定說明', infoDesc: '設定通知管道用於接收系統訊息(可選)', type: '通知類型', diff --git a/src/pages/setup.vue b/src/pages/setup.vue index e79b66d1..9f7dc7ea 100644 --- a/src/pages/setup.vue +++ b/src/pages/setup.vue @@ -2,7 +2,6 @@ import { onMounted } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' -import { useToast } from 'vue-toastification' import { useSetupWizard } from '@/composables/useSetupWizard' import BasicSettingsStep from '@/views/setup/BasicSettingsStep.vue' import StorageSettingsStep from '@/views/setup/StorageSettingsStep.vue' @@ -14,19 +13,9 @@ import ConnectivityTest from '@/views/setup/ConnectivityTest.vue' const { t } = useI18n() const router = useRouter() -const $toast = useToast() -const { - currentStep, - totalSteps, - stepTitles, - stepDescriptions, - connectivityTest, - nextStep, - prevStep, - completeWizard, - initialize, -} = useSetupWizard() +const { currentStep, totalSteps, stepTitles, connectivityTest, nextStep, prevStep, completeWizard, initialize } = + useSetupWizard() // 初始化 onMounted(async () => { @@ -184,4 +173,4 @@ onMounted(async () => { margin-inline: auto; max-inline-size: 800px; } - \ No newline at end of file + diff --git a/src/views/setup/BasicSettingsStep.vue b/src/views/setup/BasicSettingsStep.vue index a22b4933..a2064423 100644 --- a/src/views/setup/BasicSettingsStep.vue +++ b/src/views/setup/BasicSettingsStep.vue @@ -3,11 +3,59 @@ import { useI18n } from 'vue-i18n' import { useSetupWizard } from '@/composables/useSetupWizard' const { t } = useI18n() -const { wizardData, createRandomString, copyValue } = useSetupWizard() +const { wizardData, createRandomString, copyValue, validateCurrentStep } = useSetupWizard() // 密码可见性控制 const isPasswordVisible = ref(false) const isConfirmPasswordVisible = ref(false) + +// 验证状态 +const validation = computed(() => validateCurrentStep()) +const hasErrors = computed(() => !validation.value.isValid) + +// 密码相关验证 +const passwordError = computed(() => { + if (!wizardData.value.basic.password) return false + return wizardData.value.basic.password.length < 6 +}) + +const confirmPasswordError = computed(() => { + if (!wizardData.value.basic.password) return false + if (!wizardData.value.basic.confirmPassword) return true + return wizardData.value.basic.password !== wizardData.value.basic.confirmPassword +}) + +const passwordErrorMessage = computed(() => { + if (passwordError.value) return t('dialog.userAddEdit.passwordMinLength') + return '' +}) + +const confirmPasswordErrorMessage = computed(() => { + if (!wizardData.value.basic.password) return '' + if (!wizardData.value.basic.confirmPassword) return t('dialog.userAddEdit.confirmPasswordRequired') + if (confirmPasswordError.value) return t('dialog.userAddEdit.passwordMismatch') + return '' +}) + +// API Token验证 +const apiTokenError = computed(() => { + return !wizardData.value.basic.apiToken && hasErrors.value +}) + +const apiTokenErrorMessage = computed(() => { + if (apiTokenError.value) return t('setupWizard.basic.apiTokenRequired') + return '' +}) + +// 用户名验证(虽然是只读的,但为了完整性) +const usernameError = computed(() => { + return !wizardData.value.basic.username && hasErrors.value +}) + +const usernameErrorMessage = computed(() => { + if (usernameError.value) return t('dialog.userAddEdit.usernameRequired') + return '' +}) \ No newline at end of file + diff --git a/src/views/setup/DownloaderSettingsStep.vue b/src/views/setup/DownloaderSettingsStep.vue index f1cb04e4..54733b2c 100644 --- a/src/views/setup/DownloaderSettingsStep.vue +++ b/src/views/setup/DownloaderSettingsStep.vue @@ -3,7 +3,7 @@ import { useI18n } from 'vue-i18n' import { useSetupWizard } from '@/composables/useSetupWizard' const { t } = useI18n() -const { wizardData, selectDownloader } = useSetupWizard() +const { wizardData, selectDownloader, validationErrors } = useSetupWizard()