mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-27 18:41:58 +08:00
@@ -10,7 +10,6 @@ const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
default: '/',
|
||||
required: true,
|
||||
},
|
||||
storage: {
|
||||
type: String,
|
||||
|
||||
1232
src/composables/useSetupWizard.ts
Normal file
1232
src/composables/useSetupWizard.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import api from '@/api'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
|
||||
import { useAuthStore, useUserStore } from '@/stores'
|
||||
import { useAuthStore, useUserStore, useGlobalSettingsStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||
@@ -20,6 +20,8 @@ import { themeManager } from '@/utils/themeManager'
|
||||
const authStore = useAuthStore()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
// 全局设置 Store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
// 显示器
|
||||
@@ -217,6 +219,11 @@ const userName = computed(() => userStore.userName)
|
||||
const avatar = computed(() => userStore.avatar || avatar1)
|
||||
const userLevel = computed(() => userStore.level)
|
||||
|
||||
// 检查是否为高级模式
|
||||
const isAdvancedMode = computed(() => {
|
||||
return globalSettingsStore.get('ADVANCED_MODE') !== false
|
||||
})
|
||||
|
||||
// 主题相关功能
|
||||
const { name: themeName, global: globalTheme } = useTheme()
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||
@@ -509,11 +516,17 @@ onUnmounted(() => {
|
||||
<VListItemTitle>{{ t('user.profile') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem v-if="superUser" link @click="router.push('/setting')" class="mb-1 rounded-lg" hover>
|
||||
<VListItem
|
||||
v-if="superUser"
|
||||
link
|
||||
@click="isAdvancedMode ? router.push('/setting') : router.push('/setup-wizard')"
|
||||
class="mb-1 rounded-lg"
|
||||
hover
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog-outline" />
|
||||
<VIcon :icon="isAdvancedMode ? 'mdi-cog-outline' : 'mdi-wizard-hat'" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('user.systemSettings') }}</VListItemTitle>
|
||||
<VListItemTitle>{{ isAdvancedMode ? t('user.systemSettings') : t('user.wizardSettings') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 Site Auth -->
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
skip: 'Skip',
|
||||
loadingText: 'Loading...',
|
||||
networkRequired: 'This feature requires network connection',
|
||||
networkDisconnected: 'Network connection lost',
|
||||
@@ -369,6 +372,7 @@ export default {
|
||||
deleteFailed: 'Failed to delete user!',
|
||||
profile: 'Profile',
|
||||
systemSettings: 'System Settings',
|
||||
wizardSettings: 'Setup Wizard',
|
||||
siteAuth: 'User Authentication',
|
||||
helpDocs: 'Help Documents',
|
||||
restart: 'Restart',
|
||||
@@ -378,8 +382,11 @@ export default {
|
||||
addUser: 'Add User',
|
||||
editUser: 'Edit User',
|
||||
username: 'Username',
|
||||
usernameHint: 'Username for system login',
|
||||
password: 'Password',
|
||||
passwordHint: 'Password for system login',
|
||||
confirmPassword: 'Confirm Password',
|
||||
confirmPasswordHint: 'Please enter the password again to confirm',
|
||||
role: 'Role',
|
||||
email: 'Email',
|
||||
enabled: 'Enabled',
|
||||
@@ -408,10 +415,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',
|
||||
@@ -427,8 +437,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',
|
||||
@@ -443,15 +455,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',
|
||||
},
|
||||
@@ -459,8 +474,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 #',
|
||||
},
|
||||
@@ -468,6 +485,7 @@ export default {
|
||||
name: 'WebPush',
|
||||
username: 'Login Username',
|
||||
usernameHint: 'Only push messages to the corresponding logged-in user',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
@@ -1772,8 +1790,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',
|
||||
@@ -1794,9 +1816,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',
|
||||
@@ -2627,6 +2647,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',
|
||||
@@ -2674,6 +2697,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: {
|
||||
@@ -2836,7 +2864,9 @@ export default {
|
||||
libraryStorage: 'Library Storage',
|
||||
libraryDirectory: 'Library Directory',
|
||||
transferType: 'Transfer Type',
|
||||
transferTypeHint: 'File operation organization method, hard link saves space, copy is safer',
|
||||
overwriteMode: 'Overwrite Mode',
|
||||
overwriteModeHint: 'How to handle when target file already exists',
|
||||
smartRename: 'Smart Rename',
|
||||
scrapingMetadata: 'Scrape Metadata',
|
||||
sendNotification: 'Send Notification',
|
||||
@@ -2878,4 +2908,150 @@ export default {
|
||||
customBackgroundImageHint: 'Supports web image URLs, leave blank for gradient background',
|
||||
pluginCount: '{count} Plugins',
|
||||
},
|
||||
setupWizard: {
|
||||
title: 'Welcome to MoviePilot!',
|
||||
subtitle: 'Complete the configuration by the wizard, and start using it immediately.',
|
||||
completed: 'Setup Wizard completed!',
|
||||
failed: 'Setup Wizard failed, please try again',
|
||||
complete: 'Complete Configuration',
|
||||
loading: 'Loading configuration data...',
|
||||
testing: 'Testing',
|
||||
connectivityTestSuccess: 'Connectivity test passed',
|
||||
connectivityTestFailed: 'Connectivity test failed',
|
||||
testingStorage: 'Testing storage',
|
||||
checkingStorage: 'Checking storage connectivity',
|
||||
testingDownloader: 'Testing downloader',
|
||||
checkingDownloader: 'Checking downloader connectivity',
|
||||
testingMediaServer: 'Testing media server',
|
||||
checkingMediaServer: 'Checking media server connectivity',
|
||||
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',
|
||||
appDomain: 'App Domain',
|
||||
appDomainHint: 'Used to add quick jump links when sending notifications',
|
||||
wallpaper: 'Background Wallpaper',
|
||||
wallpaperHint: 'Choose the source of the login page background',
|
||||
recognizeSource: 'Recognize Source',
|
||||
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',
|
||||
description: 'Configure download directory and media library directory',
|
||||
info: 'Storage Configuration',
|
||||
infoDesc: 'Configure local storage directories for download and media library management',
|
||||
downloadPath: 'Download Directory',
|
||||
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',
|
||||
description: 'Configure downloader',
|
||||
info: 'Downloader Configuration',
|
||||
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',
|
||||
nameHint: 'Set a name for the downloader',
|
||||
qbittorrentConfig: 'qBittorrent Configuration',
|
||||
transmissionConfig: 'Transmission Configuration',
|
||||
host: 'Server Address',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
downloadPath: 'Download Path',
|
||||
},
|
||||
mediaServer: {
|
||||
title: 'Media Server',
|
||||
description: 'Configure media server',
|
||||
info: 'Media Server Configuration',
|
||||
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',
|
||||
nameHint: 'Set a name for the media server',
|
||||
embyConfig: 'Emby Configuration',
|
||||
jellyfinConfig: 'Jellyfin Configuration',
|
||||
plexConfig: 'Plex Configuration',
|
||||
host: 'Server Address',
|
||||
apiKey: 'API Key',
|
||||
token: 'Access Token',
|
||||
},
|
||||
notification: {
|
||||
title: 'Notification',
|
||||
description: 'Configure notification channels',
|
||||
info: 'Notification Configuration',
|
||||
infoDesc: 'Configure notification channels for receiving system messages (optional)',
|
||||
type: 'Notification Type',
|
||||
typeHint: 'Select the type of notification channel to use',
|
||||
name: 'Notification Name',
|
||||
nameHint: 'Set a name for the notification channel',
|
||||
telegramConfig: 'Telegram Configuration',
|
||||
emailConfig: 'Email Configuration',
|
||||
botToken: 'Bot Token',
|
||||
chatId: 'Chat ID',
|
||||
smtpServer: 'SMTP Server',
|
||||
smtpPort: 'SMTP Port',
|
||||
senderEmail: 'Sender Email',
|
||||
senderPassword: 'Sender Password',
|
||||
receiverEmail: 'Receiver Email',
|
||||
},
|
||||
preferences: {
|
||||
title: 'Resource Preferences',
|
||||
description: 'Set resource download preferences',
|
||||
info: 'Resource Preferences',
|
||||
infoDesc:
|
||||
'Set resource download preferences, the system will automatically select the best resources based on these preferences',
|
||||
quality: 'Quality Preference',
|
||||
qualityHint: 'Select preferred video quality',
|
||||
subtitle: 'Subtitle Preference',
|
||||
subtitleHint: 'Select preferred subtitle type',
|
||||
resolution: 'Resolution Preference',
|
||||
resolutionHint: 'Select preferred video resolution',
|
||||
presetRules: 'Preset Rules',
|
||||
detailedConfig: 'Detailed Configuration',
|
||||
quickPresets: 'Quick Presets',
|
||||
quickPresetsDesc: 'Select preset configuration, system will automatically apply corresponding rules',
|
||||
personalizationOptions: 'Personalization Options',
|
||||
personalizationOptionsDesc: 'Adjust rules according to your needs',
|
||||
excludeDolbyVision: 'Exclude Dolby Vision',
|
||||
excludeDolbyVisionHint: 'Exclude Dolby Vision resources from rules when selected',
|
||||
excludeBluray: 'Exclude Blu-ray',
|
||||
excludeBlurayHint: 'Exclude Blu-ray resources from rules when selected',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K Enthusiast',
|
||||
description: 'Pursue the highest quality, prioritize 4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: 'Balanced Mode',
|
||||
description: 'Balance between quality and storage space',
|
||||
},
|
||||
'space-saver': {
|
||||
name: 'Space Saver',
|
||||
description: 'Prioritize smaller files to save storage space',
|
||||
},
|
||||
'free-priority': {
|
||||
name: 'Free Priority',
|
||||
description: 'Prioritize free resources, no other requirements',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
next: '下一步',
|
||||
previous: '上一步',
|
||||
skip: '跳过',
|
||||
loadingText: '加载中...',
|
||||
networkRequired: '此功能需要网络连接',
|
||||
networkDisconnected: '网络连接已断开',
|
||||
@@ -367,6 +370,7 @@ export default {
|
||||
deleteFailed: '用户删除失败!',
|
||||
profile: '个人信息',
|
||||
systemSettings: '系统设定',
|
||||
wizardSettings: '设置向导',
|
||||
siteAuth: '用户认证',
|
||||
helpDocs: '帮助文档',
|
||||
restart: '重启',
|
||||
@@ -376,8 +380,11 @@ export default {
|
||||
addUser: '添加用户',
|
||||
editUser: '编辑用户',
|
||||
username: '用户名',
|
||||
usernameHint: '用于登录系统的用户名',
|
||||
password: '密码',
|
||||
passwordHint: '用于登录系统的密码',
|
||||
confirmPassword: '确认密码',
|
||||
confirmPasswordHint: '请再次输入密码以确认',
|
||||
role: '角色',
|
||||
email: '邮箱',
|
||||
enabled: '启用',
|
||||
@@ -406,10 +413,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',
|
||||
@@ -424,8 +434,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: '管理员白名单',
|
||||
@@ -440,15 +452,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机器人令牌',
|
||||
},
|
||||
@@ -456,8 +471,10 @@ export default {
|
||||
name: 'VoceChat',
|
||||
host: '地址',
|
||||
hostHint: 'VoceChat服务端地址,格式:http(s)://ip:port',
|
||||
hostRequired: '地址不能为空',
|
||||
apiKey: '机器人密钥',
|
||||
apiKeyHint: 'VoceChat机器人密钥',
|
||||
apiKeyRequired: 'API密钥不能为空',
|
||||
channelId: '频道ID',
|
||||
channelIdHint: 'VoceChat的频道ID,不包含#号',
|
||||
},
|
||||
@@ -465,6 +482,7 @@ export default {
|
||||
name: 'WebPush',
|
||||
username: '登录用户名',
|
||||
usernameHint: '只有对应的用户登录后才会推送消息',
|
||||
usernameRequired: '用户名不能为空',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
@@ -1748,8 +1766,12 @@ export default {
|
||||
add: '添加用户',
|
||||
edit: '编辑用户',
|
||||
username: '用户名',
|
||||
usernameRequired: '用户名不能为空',
|
||||
password: '密码',
|
||||
passwordMinLength: '密码长度不能少于6位',
|
||||
confirmPassword: '确认密码',
|
||||
confirmPasswordRequired: '请确认密码',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
email: '邮箱',
|
||||
nickname: '昵称',
|
||||
status: '状态',
|
||||
@@ -1770,9 +1792,7 @@ export default {
|
||||
webPush: 'WebPush',
|
||||
creatingUser: '正在创建【{name}】用户,请稍后',
|
||||
updatingUser: '正在更新【{name}】用户,请稍后',
|
||||
usernameRequired: '用户名不能为空',
|
||||
usernameExists: '用户名已存在',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
userCreated: '用户【{name}】创建成功',
|
||||
userCreateFailed: '创建用户失败:{message}',
|
||||
userUpdateSuccess: '用户【{name}】更新成功',
|
||||
@@ -2595,6 +2615,9 @@ export default {
|
||||
nameRequired: '不能为空,且不能重名',
|
||||
nameDuplicate: '名称已存在',
|
||||
defaultChanged: '存在默认下载器,已替换',
|
||||
hostRequired: '地址不能为空',
|
||||
usernameRequired: '用户名不能为空',
|
||||
passwordRequired: '密码不能为空',
|
||||
},
|
||||
filterRule: {
|
||||
title: '过滤规则',
|
||||
@@ -2643,6 +2666,11 @@ export default {
|
||||
syncLibraries: '同步媒体库',
|
||||
syncLibrariesHint: '只有选中的媒体库才会被同步',
|
||||
nameExists: '【{name}】已存在,请替换为其他名称',
|
||||
hostRequired: '地址不能为空',
|
||||
apiKeyRequired: 'API密钥不能为空',
|
||||
tokenRequired: 'Token不能为空',
|
||||
usernameRequired: '用户名不能为空',
|
||||
passwordRequired: '密码不能为空',
|
||||
},
|
||||
bangumi: {
|
||||
category: '类别',
|
||||
@@ -2804,7 +2832,9 @@ export default {
|
||||
libraryStorage: '媒体库存储',
|
||||
libraryDirectory: '媒体库目录',
|
||||
transferType: '整理方式',
|
||||
transferTypeHint: '文件操作整理方式,硬链接节省空间,复制更安全',
|
||||
overwriteMode: '覆盖模式',
|
||||
overwriteModeHint: '当目标文件已存在时的处理方式',
|
||||
smartRename: '智能重命名',
|
||||
scrapingMetadata: '刮削元数据',
|
||||
sendNotification: '发送通知',
|
||||
@@ -2845,4 +2875,169 @@ export default {
|
||||
customBackgroundImageHint: '支持网络图片URL,留空则使用渐变背景',
|
||||
pluginCount: '{count} 个插件',
|
||||
},
|
||||
setupWizard: {
|
||||
title: '欢迎使用 MoviePilot !',
|
||||
subtitle: '按向导完成配置,即刻开始使用。',
|
||||
completed: '配置向导完成!',
|
||||
failed: '配置向导失败,请重试',
|
||||
complete: '完成配置',
|
||||
loading: '正在加载配置数据...',
|
||||
testing: '正在测试',
|
||||
connectivityTestSuccess: '连通性测试通过',
|
||||
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: '设置访问域名、用户名密码和网络配置',
|
||||
appDomain: '访问域名',
|
||||
appDomainHint: '用于发送通知时,添加快捷跳转地址',
|
||||
wallpaper: '背景壁纸',
|
||||
wallpaperHint: '选择登录页面背景来源',
|
||||
recognizeSource: '识别数据源',
|
||||
recognizeSourceHint: '设置默认媒体信息识别数据源',
|
||||
apiToken: 'API 令牌',
|
||||
apiTokenHint: '系统自动生成的 API 访问令牌',
|
||||
currentUserHint: '当前用户,不可修改',
|
||||
passwordOptionalHint: '留空表示不修改密码',
|
||||
confirmPasswordHint: '确认新密码',
|
||||
apiTokenRequired: 'API Token不能为空',
|
||||
},
|
||||
storage: {
|
||||
title: '存储',
|
||||
description: '配置下载目录和媒体库目录',
|
||||
info: '存储配置说明',
|
||||
infoDesc: '配置本地存储目录,用于下载和媒体库管理',
|
||||
downloadPath: '下载目录',
|
||||
downloadPathHint: '设置下载文件的存储路径',
|
||||
libraryPath: '媒体库目录',
|
||||
libraryPathHint: '设置媒体文件的存储路径',
|
||||
downloadPathRequired: '下载目录不能为空',
|
||||
libraryPathRequired: '媒体库目录不能为空',
|
||||
},
|
||||
downloader: {
|
||||
title: '下载器',
|
||||
description: '配置下载器',
|
||||
info: '下载器配置说明',
|
||||
infoDesc: '配置下载器用于下载资源,可选择qBittorrent或Transmission',
|
||||
type: '下载器类型',
|
||||
typeHint: '选择要使用的下载器类型',
|
||||
name: '下载器名称',
|
||||
nameHint: '为下载器设置一个名称',
|
||||
qbittorrentConfig: 'qBittorrent 配置',
|
||||
transmissionConfig: 'Transmission 配置',
|
||||
host: '服务器地址',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
downloadPath: '下载路径',
|
||||
},
|
||||
mediaServer: {
|
||||
title: '媒体服务器',
|
||||
description: '配置媒体服务器',
|
||||
info: '媒体服务器配置说明',
|
||||
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin或Plex等',
|
||||
type: '媒体服务器类型',
|
||||
typeHint: '选择要使用的媒体服务器类型',
|
||||
name: '服务器名称',
|
||||
nameHint: '为媒体服务器设置一个名称',
|
||||
embyConfig: 'Emby 配置',
|
||||
jellyfinConfig: 'Jellyfin 配置',
|
||||
plexConfig: 'Plex 配置',
|
||||
host: '服务器地址',
|
||||
apiKey: 'API 密钥',
|
||||
token: '访问令牌',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '配置通知渠道',
|
||||
info: '通知配置说明',
|
||||
infoDesc: '配置通知渠道用于接收系统消息(可选)',
|
||||
type: '通知类型',
|
||||
typeHint: '选择要使用的通知渠道类型',
|
||||
name: '通知名称',
|
||||
nameHint: '为通知渠道设置一个名称',
|
||||
telegramConfig: 'Telegram 配置',
|
||||
emailConfig: '邮件配置',
|
||||
botToken: '机器人令牌',
|
||||
chatId: '聊天ID',
|
||||
smtpServer: 'SMTP 服务器',
|
||||
smtpPort: 'SMTP 端口',
|
||||
senderEmail: '发送邮箱',
|
||||
senderPassword: '发送密码',
|
||||
receiverEmail: '接收邮箱',
|
||||
},
|
||||
preferences: {
|
||||
title: '资源偏好',
|
||||
description: '设置资源下载偏好',
|
||||
info: '资源偏好说明',
|
||||
infoDesc: '设置资源下载的偏好,系统将根据这些偏好自动选择最佳资源',
|
||||
quality: '质量偏好',
|
||||
qualityHint: '选择偏好的视频质量',
|
||||
subtitle: '字幕偏好',
|
||||
subtitleHint: '选择偏好的字幕类型',
|
||||
resolution: '分辨率偏好',
|
||||
resolutionHint: '选择偏好的视频分辨率',
|
||||
presetRules: '预设规则',
|
||||
detailedConfig: '详细配置',
|
||||
quickPresets: '快速预设',
|
||||
quickPresetsDesc: '选择预设配置,系统将自动应用对应的规则',
|
||||
personalizationOptions: '个性化选项',
|
||||
personalizationOptionsDesc: '根据您的需求调整规则',
|
||||
excludeDolbyVision: '排除杜比视界',
|
||||
excludeDolbyVisionHint: '选中后规则中将排除杜比视界资源',
|
||||
excludeBluray: '排除蓝光原盘',
|
||||
excludeBlurayHint: '选中后规则中将排除蓝光原盘资源',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K发烧友',
|
||||
description: '追求最高画质,优先4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: '平衡模式',
|
||||
description: '画质与存储空间的平衡选择',
|
||||
},
|
||||
'space-saver': {
|
||||
name: '节省空间',
|
||||
description: '优先较小文件,节省存储空间',
|
||||
},
|
||||
'free-priority': {
|
||||
name: '免费优先',
|
||||
description: '优先免费资源,其它的没有要求',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
next: '下一步',
|
||||
previous: '上一步',
|
||||
skip: '跳過',
|
||||
loadingText: '加載中...',
|
||||
networkRequired: '此功能需要網絡連接',
|
||||
networkDisconnected: '網絡連接已斷開',
|
||||
@@ -368,6 +371,7 @@ export default {
|
||||
deleteFailed: '用戶刪除失敗!',
|
||||
profile: '個人信息',
|
||||
systemSettings: '系統設定',
|
||||
wizardSettings: '設定向導',
|
||||
siteAuth: '用戶認證',
|
||||
helpDocs: '幫助文檔',
|
||||
restart: '重啟',
|
||||
@@ -377,8 +381,11 @@ export default {
|
||||
addUser: '添加用戶',
|
||||
editUser: '編輯用戶',
|
||||
username: '用戶名',
|
||||
usernameHint: '用於登入系統的用戶名',
|
||||
password: '密碼',
|
||||
passwordHint: '用於登入系統的密碼',
|
||||
confirmPassword: '確認密碼',
|
||||
confirmPasswordHint: '請再次輸入密碼以確認',
|
||||
role: '角色',
|
||||
email: '郵箱',
|
||||
enabled: '啟用',
|
||||
@@ -1747,8 +1754,12 @@ export default {
|
||||
add: '添加用戶',
|
||||
edit: '編輯用戶',
|
||||
username: '用戶名',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
password: '密碼',
|
||||
passwordMinLength: '密碼長度不能少於6位',
|
||||
confirmPassword: '確認密碼',
|
||||
confirmPasswordRequired: '請確認密碼',
|
||||
passwordMismatch: '兩次輸入的密碼不一致',
|
||||
email: '郵箱',
|
||||
nickname: '暱稱',
|
||||
status: '狀態',
|
||||
@@ -1769,9 +1780,7 @@ export default {
|
||||
webPush: 'WebPush',
|
||||
creatingUser: '正在創建【{name}】用戶,請稍後',
|
||||
updatingUser: '正在更新【{name}】用戶,請稍後',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
usernameExists: '用戶名已存在',
|
||||
passwordMismatch: '兩次輸入的密碼不一致',
|
||||
userCreated: '用戶【{name}】創建成功',
|
||||
userCreateFailed: '創建用戶失敗:{message}',
|
||||
userUpdateSuccess: '用戶【{name}】更新成功',
|
||||
@@ -2594,6 +2603,9 @@ export default {
|
||||
nameRequired: '名稱不能為空',
|
||||
nameDuplicate: '名稱已存在',
|
||||
defaultChanged: '存在預設下載器,已替換',
|
||||
hostRequired: '地址不能為空',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
passwordRequired: '密碼不能為空',
|
||||
},
|
||||
filterRule: {
|
||||
title: '過濾規則',
|
||||
@@ -2629,13 +2641,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: '密碼',
|
||||
@@ -2803,7 +2820,9 @@ export default {
|
||||
libraryStorage: '媒體庫存儲',
|
||||
libraryDirectory: '媒體庫目錄',
|
||||
transferType: '轉移方式',
|
||||
transferTypeHint: '文件操作整理方式,硬連結節省空間,複製更安全',
|
||||
overwriteMode: '覆蓋模式',
|
||||
overwriteModeHint: '當目標文件已存在時的處理方式',
|
||||
smartRename: '智能重命名',
|
||||
scrapingMetadata: '刮削元數據',
|
||||
sendNotification: '發送通知',
|
||||
@@ -2844,4 +2863,149 @@ export default {
|
||||
customBackgroundImageHint: '支援網路圖片URL,留空則使用漸變背景',
|
||||
pluginCount: '{count} 個插件',
|
||||
},
|
||||
setupWizard: {
|
||||
title: '歡迎使用 MoviePilot !',
|
||||
subtitle: '按向導完成配置,即刻開始使用。',
|
||||
completed: '設定精靈完成!',
|
||||
failed: '設定精靈失敗,請重試',
|
||||
complete: '完成設定',
|
||||
loading: '正在載入配置資料...',
|
||||
testing: '正在測試',
|
||||
connectivityTestSuccess: '連通性測試通過',
|
||||
connectivityTestFailed: '連通性測試失敗',
|
||||
testingStorage: '正在測試存儲目錄',
|
||||
checkingStorage: '檢查存儲目錄連通性',
|
||||
testingDownloader: '正在測試下載器',
|
||||
checkingDownloader: '檢查下載器連通性',
|
||||
testingMediaServer: '正在測試媒體服務器',
|
||||
checkingMediaServer: '檢查媒體服務器連通性',
|
||||
testingNotification: '正在測試消息通知',
|
||||
checkingNotification: '檢查消息通知連通性',
|
||||
testFailedHint: '請檢查配置是否正確,修改後可以重新測試',
|
||||
unsupportedDownloaderType: '不支援的下載器類型: {type}',
|
||||
unsupportedMediaServerType: '不支援的媒體服務器類型: {type}',
|
||||
unsupportedNotificationType: '不支援的通知類型: {type}',
|
||||
passwordUpdateSuccess: '密碼更新成功',
|
||||
userCreateSuccess: '使用者建立成功',
|
||||
passwordUpdateFailed: '密碼更新失敗',
|
||||
basic: {
|
||||
title: '基礎設定',
|
||||
description: '設定存取網域、用戶名密碼和網路配置',
|
||||
appDomain: '存取網域',
|
||||
appDomainHint: '用於發送通知時,新增快速跳轉位址',
|
||||
wallpaper: '背景桌布',
|
||||
wallpaperHint: '選擇登入頁面背景來源',
|
||||
recognizeSource: '識別資料來源',
|
||||
recognizeSourceHint: '設定預設媒體資訊識別資料來源',
|
||||
apiToken: 'API 權杖',
|
||||
apiTokenHint: '系統自動產生的 API 存取權杖',
|
||||
currentUserHint: '目前使用者,不可修改',
|
||||
passwordOptionalHint: '留空表示不修改密碼',
|
||||
confirmPasswordHint: '確認新密碼',
|
||||
apiTokenRequired: 'API Token 不能為空',
|
||||
},
|
||||
storage: {
|
||||
title: '儲存',
|
||||
description: '設定下載目錄和媒體庫目錄',
|
||||
info: '儲存設定說明',
|
||||
infoDesc: '設定本機儲存目錄,用於下載和媒體庫管理',
|
||||
downloadPath: '下載目錄',
|
||||
downloadPathHint: '設定下載檔案的儲存路徑',
|
||||
libraryPath: '媒體庫目錄',
|
||||
libraryPathHint: '設定媒體檔案的儲存路徑',
|
||||
downloadPathRequired: '下載目錄不能為空',
|
||||
libraryPathRequired: '媒體庫目錄不能為空',
|
||||
},
|
||||
downloader: {
|
||||
title: '下載器',
|
||||
description: '設定下載器',
|
||||
info: '下載器設定說明',
|
||||
infoDesc: '設定下載器用於下載資源,可選擇qBittorrent或Transmission',
|
||||
type: '下載器類型',
|
||||
typeHint: '選擇要使用的下載器類型',
|
||||
name: '下載器名稱',
|
||||
nameHint: '為下載器設定一個名稱',
|
||||
qbittorrentConfig: 'qBittorrent 設定',
|
||||
transmissionConfig: 'Transmission 設定',
|
||||
host: '伺服器位址',
|
||||
username: '使用者名稱',
|
||||
password: '密碼',
|
||||
downloadPath: '下載路徑',
|
||||
},
|
||||
mediaServer: {
|
||||
title: '媒體伺服器',
|
||||
description: '設定媒體伺服器',
|
||||
info: '媒體伺服器設定說明',
|
||||
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin或Plex等',
|
||||
type: '媒體伺服器類型',
|
||||
typeHint: '選擇要使用的媒體伺服器類型',
|
||||
name: '伺服器名稱',
|
||||
nameHint: '為媒體伺服器設定一個名稱',
|
||||
embyConfig: 'Emby 設定',
|
||||
jellyfinConfig: 'Jellyfin 設定',
|
||||
plexConfig: 'Plex 設定',
|
||||
host: '伺服器位址',
|
||||
apiKey: 'API 金鑰',
|
||||
token: '存取權杖',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '設定通知管道',
|
||||
info: '通知設定說明',
|
||||
infoDesc: '設定通知管道用於接收系統訊息(可選)',
|
||||
type: '通知類型',
|
||||
typeHint: '選擇要使用的通知管道類型',
|
||||
name: '通知名稱',
|
||||
nameHint: '為通知管道設定一個名稱',
|
||||
telegramConfig: 'Telegram 設定',
|
||||
emailConfig: '郵件設定',
|
||||
botToken: '機器人權杖',
|
||||
chatId: '聊天ID',
|
||||
smtpServer: 'SMTP 伺服器',
|
||||
smtpPort: 'SMTP 連接埠',
|
||||
senderEmail: '發送信箱',
|
||||
senderPassword: '發送密碼',
|
||||
receiverEmail: '接收信箱',
|
||||
},
|
||||
preferences: {
|
||||
title: '資源偏好',
|
||||
description: '設定資源下載偏好',
|
||||
info: '資源偏好說明',
|
||||
infoDesc: '設定資源下載的偏好,系統將根據這些偏好自動選擇最佳資源',
|
||||
quality: '品質偏好',
|
||||
qualityHint: '選擇偏好的影片品質',
|
||||
subtitle: '字幕偏好',
|
||||
subtitleHint: '選擇偏好的字幕類型',
|
||||
resolution: '解析度偏好',
|
||||
resolutionHint: '選擇偏好的影片解析度',
|
||||
presetRules: '預設規則',
|
||||
detailedConfig: '詳細設定',
|
||||
quickPresets: '快速預設',
|
||||
quickPresetsDesc: '選擇預設配置,系統將自動應用對應的規則',
|
||||
personalizationOptions: '個性化選項',
|
||||
personalizationOptionsDesc: '根據您的需求調整規則',
|
||||
excludeDolbyVision: '排除杜比視界',
|
||||
excludeDolbyVisionHint: '選中後規則中將排除杜比視界資源',
|
||||
excludeBluray: '排除藍光原盤',
|
||||
excludeBlurayHint: '選中後規則中將排除藍光原盤資源',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K發燒友',
|
||||
description: '追求最高畫質,優先4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: '平衡模式',
|
||||
description: '畫質與儲存空間的平衡選擇',
|
||||
},
|
||||
'space-saver': {
|
||||
name: '節省空間',
|
||||
description: '優先較小檔案,節省儲存空間',
|
||||
},
|
||||
'free-priority': {
|
||||
name: '免費優先',
|
||||
description: '優先免費資源,其它的沒有要求',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,12 +117,17 @@ async function subscribeForPushNotifications() {
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
// 如果需要显示设置向导,跳转到设置向导页面
|
||||
if (userPayload.wizard) {
|
||||
router.push('/setup-wizard')
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅推送通知
|
||||
@@ -165,6 +170,7 @@ function login() {
|
||||
avatar: response.avatar,
|
||||
level: response.level,
|
||||
permissions: response.permissions,
|
||||
wizard: response.widzard,
|
||||
}
|
||||
|
||||
// 在保存用户信息之前检查权限
|
||||
|
||||
191
src/pages/setup.vue
Normal file
191
src/pages/setup.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import BasicSettingsStep from '@/views/setup/BasicSettingsStep.vue'
|
||||
import StorageSettingsStep from '@/views/setup/StorageSettingsStep.vue'
|
||||
import DownloaderSettingsStep from '@/views/setup/DownloaderSettingsStep.vue'
|
||||
import MediaServerSettingsStep from '@/views/setup/MediaServerSettingsStep.vue'
|
||||
import NotificationSettingsStep from '@/views/setup/NotificationSettingsStep.vue'
|
||||
import PreferencesSettingsStep from '@/views/setup/PreferencesSettingsStep.vue'
|
||||
import ConnectivityTest from '@/views/setup/ConnectivityTest.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
const {
|
||||
currentStep,
|
||||
totalSteps,
|
||||
stepTitles,
|
||||
connectivityTest,
|
||||
nextStep,
|
||||
prevStep,
|
||||
completeWizard,
|
||||
initialize,
|
||||
isLoading,
|
||||
} = useSetupWizard()
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setup-wizard-fullscreen">
|
||||
<!-- 全屏头部 -->
|
||||
<div class="setup-wizard-header">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<!-- 左侧占位 -->
|
||||
<div style="inline-size: 72px"></div>
|
||||
|
||||
<!-- 中间标题 -->
|
||||
<div class="d-flex align-center text-center">
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-bold text-moviepilot mb-3">{{ t('setupWizard.title') }}</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
variant="text"
|
||||
icon="mdi-cog"
|
||||
@click="router.push('/setting')"
|
||||
size="small"
|
||||
class="text-medium-emphasis"
|
||||
/>
|
||||
<VBtn variant="text" icon="mdi-close" @click="router.push('/')" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 向导内容 -->
|
||||
<VCard max-width="800px" class="mx-auto my-7">
|
||||
<VCardText>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="d-flex flex-column align-center justify-center py-16">
|
||||
<VProgressCircular indeterminate color="primary" size="64" class="mb-4" />
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 使用 VStepper 组件 -->
|
||||
<VStepper v-else v-model="currentStep" class="elevation-0" flat alt-labels :mobile="display.smAndDown.value">
|
||||
<!-- 步骤标题 -->
|
||||
<VStepperHeader class="elevation-0">
|
||||
<template v-for="(step, index) in stepTitles" :key="index">
|
||||
<VStepperItem
|
||||
:value="index + 1"
|
||||
:complete="currentStep > index + 1"
|
||||
:color="currentStep >= index + 1 ? 'primary' : 'default'"
|
||||
complete-icon="mdi-check-circle"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-caption">{{ step }}</span>
|
||||
</template>
|
||||
</VStepperItem>
|
||||
<VDivider v-if="index < stepTitles.length - 1" />
|
||||
</template>
|
||||
</VStepperHeader>
|
||||
|
||||
<!-- 步骤内容 -->
|
||||
<VStepperWindow>
|
||||
<!-- 步骤1:基础参数 -->
|
||||
<VStepperWindowItem :value="1">
|
||||
<BasicSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤2:存储目录 -->
|
||||
<VStepperWindowItem :value="2">
|
||||
<StorageSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤3:下载器 -->
|
||||
<VStepperWindowItem :value="3">
|
||||
<DownloaderSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤4:媒体服务器 -->
|
||||
<VStepperWindowItem :value="4">
|
||||
<MediaServerSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤5:通知 -->
|
||||
<VStepperWindowItem :value="5">
|
||||
<NotificationSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤6:资源偏好 -->
|
||||
<VStepperWindowItem :value="6">
|
||||
<PreferencesSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
</VStepperWindow>
|
||||
|
||||
<!-- 连通性测试进度条 -->
|
||||
<ConnectivityTest />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<VCardActions class="justify-space-between">
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
v-if="currentStep !== 1"
|
||||
prepend-icon="mdi-chevron-left"
|
||||
@click="prevStep"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ t('common.previous') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
v-if="currentStep < totalSteps"
|
||||
color="primary"
|
||||
append-icon="mdi-chevron-right"
|
||||
@click="nextStep"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ connectivityTest.isTesting ? t('setupWizard.testing') : t('common.next') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="success"
|
||||
prepend-icon="mdi-check"
|
||||
@click="completeWizard"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ t('setupWizard.complete') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VStepper>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setup-wizard-fullscreen {
|
||||
position: fixed;
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setup-wizard-header {
|
||||
position: sticky;
|
||||
z-index: 2000;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
border-block-end: 1px solid rgb(var(--v-theme-outline-variant));
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 4%);
|
||||
inset-block-start: 0;
|
||||
padding-block: 16px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// 构建路由菜单,每次调用时使用当前的语言环境
|
||||
export function getNavMenus() {
|
||||
const { t } = useI18n()
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 检查是否为高级模式
|
||||
const isAdvancedMode = globalSettingsStore.get('ADVANCED_MODE') !== false
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -127,14 +132,18 @@ export function getNavMenus() {
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
...(isAdvancedMode
|
||||
? [
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,13 @@ const router = createRouter({
|
||||
path: 'login',
|
||||
component: () => import('../pages/login.vue'),
|
||||
},
|
||||
{
|
||||
path: 'setup-wizard',
|
||||
component: () => import('../pages/setup.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('../pages/[...all].vue'),
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface userState {
|
||||
level: number
|
||||
// 权限
|
||||
permissions: { [key: string]: any }
|
||||
// 是否需要显示设置向导
|
||||
wizard: boolean
|
||||
}
|
||||
|
||||
export interface globalSettingsState {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const useUserStore = defineStore('user', {
|
||||
avatar: '',
|
||||
level: 1,
|
||||
permissions: DEFAULT_PERMISSIONS,
|
||||
wizard: false,
|
||||
}),
|
||||
|
||||
// 全局持久化
|
||||
@@ -34,6 +35,9 @@ export const useUserStore = defineStore('user', {
|
||||
setPermissions(permissions: object) {
|
||||
this.permissions = { ...DEFAULT_PERMISSIONS, ...permissions }
|
||||
},
|
||||
setWizard(wizard: boolean) {
|
||||
this.wizard = wizard
|
||||
},
|
||||
loginUser(payload: userState) {
|
||||
this.setSuperUser(payload.superUser)
|
||||
this.setUserID(payload.userID)
|
||||
@@ -41,6 +45,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.setAvatar(payload.avatar)
|
||||
this.setLevel(payload.level)
|
||||
this.setPermissions(payload.permissions)
|
||||
this.setWizard(payload.wizard)
|
||||
},
|
||||
reset() {
|
||||
this.setSuperUser(false)
|
||||
@@ -49,6 +54,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.setAvatar('')
|
||||
this.setLevel(1)
|
||||
this.setPermissions(DEFAULT_PERMISSIONS)
|
||||
this.setWizard(false)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -59,5 +65,6 @@ export const useUserStore = defineStore('user', {
|
||||
getAvatar: state => state.avatar,
|
||||
getLevel: state => state.level,
|
||||
getPermissions: state => state.permissions,
|
||||
getWizard: state => state.wizard,
|
||||
},
|
||||
})
|
||||
|
||||
160
src/views/setup/BasicSettingsStep.vue
Normal file
160
src/views/setup/BasicSettingsStep.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
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 ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.basic.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.basic.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.appDomain"
|
||||
:label="t('setupWizard.basic.appDomain')"
|
||||
:hint="t('setupWizard.basic.appDomainHint')"
|
||||
placeholder="http://localhost:3000"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.username"
|
||||
:label="t('user.username')"
|
||||
:hint="t('setupWizard.basic.currentUserHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
readonly
|
||||
:error="usernameError"
|
||||
:error-messages="usernameError ? [usernameErrorMessage] : []"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.password"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:label="t('user.password')"
|
||||
:hint="t('setupWizard.basic.passwordOptionalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
:error="passwordError"
|
||||
:error-messages="passwordError ? [passwordErrorMessage] : []"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.confirmPassword"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:label="t('user.confirmPassword')"
|
||||
:hint="t('setupWizard.basic.confirmPasswordHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock-check"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
:disabled="!wizardData.basic.password"
|
||||
:error="confirmPasswordError"
|
||||
:error-messages="confirmPasswordError ? [confirmPasswordErrorMessage] : []"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.proxyHost"
|
||||
:label="t('setting.system.proxyHost')"
|
||||
:hint="t('setting.system.proxyHostHint')"
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.githubToken"
|
||||
:label="t('setting.system.githubToken')"
|
||||
:placeholder="t('setting.system.githubTokenFormat')"
|
||||
:hint="t('setting.system.githubTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-github"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.apiToken"
|
||||
:label="t('setupWizard.basic.apiToken')"
|
||||
:hint="t('setupWizard.basic.apiTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
:append-inner-icon="wizardData.basic.apiToken ? 'mdi-content-copy' : 'mdi-reload'"
|
||||
@click:append-inner="
|
||||
wizardData.basic.apiToken ? copyValue(wizardData.basic.apiToken) : createRandomString()
|
||||
"
|
||||
:error="apiTokenError"
|
||||
:error-messages="apiTokenError ? [apiTokenErrorMessage] : []"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
64
src/views/setup/ConnectivityTest.vue
Normal file
64
src/views/setup/ConnectivityTest.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { connectivityTest } = useSetupWizard()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 连通性测试进度条 -->
|
||||
<VCard v-if="connectivityTest.isTesting || connectivityTest.showResult" variant="outlined" class="mx-4 mb-4">
|
||||
<VCardText class="text-center py-4">
|
||||
<!-- 测试中 -->
|
||||
<div v-if="connectivityTest.isTesting">
|
||||
<VIcon icon="mdi-cog-sync" class="rotating mb-2" color="primary" size="24" />
|
||||
<div class="text-body-2 mb-2">{{ connectivityTest.testMessage }}</div>
|
||||
<VProgressLinear
|
||||
v-model="connectivityTest.testProgress"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="text-caption text-medium-emphasis">{{ Math.round(connectivityTest.testProgress) }}%</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<div v-else-if="connectivityTest.showResult">
|
||||
<VIcon
|
||||
:icon="connectivityTest.testResult === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
:color="connectivityTest.testResult === 'success' ? 'success' : 'error'"
|
||||
size="24"
|
||||
class="mb-2"
|
||||
/>
|
||||
<div
|
||||
:class="connectivityTest.testResult === 'success' ? 'text-success' : 'text-error'"
|
||||
class="text-body-2 mb-2 font-weight-medium"
|
||||
>
|
||||
{{ connectivityTest.testMessage }}
|
||||
</div>
|
||||
<div v-if="connectivityTest.testResult === 'error'" class="text-caption text-medium-emphasis">
|
||||
{{ t('setupWizard.testFailedHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 旋转动画 */
|
||||
.rotating {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
261
src/views/setup/DownloaderSettingsStep.vue
Normal file
261
src/views/setup/DownloaderSettingsStep.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.downloader.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.downloader.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.downloader.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.downloader.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 下载器选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.downloader.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'qbittorrent' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'qbittorrent' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectDownloader('qbittorrent')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/qbittorrent.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">qBittorrent</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'transmission' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'transmission' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectDownloader('transmission')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/transmission.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Transmission</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 下载器配置 -->
|
||||
<VCol v-if="wizardData.downloader.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow v-if="wizardData.downloader.type === 'qbittorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
:error="validationErrors.downloader.name"
|
||||
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
:error="validationErrors.downloader.host"
|
||||
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:error="validationErrors.downloader.username"
|
||||
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:error="validationErrors.downloader.password"
|
||||
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.sequentail"
|
||||
:label="t('downloader.sequentail')"
|
||||
:hint="t('downloader.sequentail')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.force_resume"
|
||||
:label="t('downloader.force_resume')"
|
||||
:hint="t('downloader.force_resume')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.first_last_piece"
|
||||
:label="t('downloader.first_last_piece')"
|
||||
:hint="t('downloader.first_last_piece')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.downloader.type === 'transmission'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
:error="validationErrors.downloader.name"
|
||||
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
:error="validationErrors.downloader.host"
|
||||
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:error="validationErrors.downloader.username"
|
||||
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:error="validationErrors.downloader.password"
|
||||
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.type"
|
||||
:label="t('downloader.type')"
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
496
src/views/setup/MediaServerSettingsStep.vue
Normal file
496
src/views/setup/MediaServerSettingsStep.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import api from '@/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectMediaServer, validationErrors } = useSetupWizard()
|
||||
|
||||
// 同步媒体库选项
|
||||
const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
||||
{
|
||||
title: t('common.all'),
|
||||
value: 'all',
|
||||
},
|
||||
])
|
||||
|
||||
// 调用API查询媒体库
|
||||
async function loadLibrary(server: string) {
|
||||
try {
|
||||
console.log('Loading library for server:', server)
|
||||
const result: any[] = await api.get('mediaserver/library', { params: { server } })
|
||||
if (result && result.length > 0) {
|
||||
librariesOptions.value = result.map(item => ({
|
||||
title: item.name,
|
||||
value: item.id?.toString(),
|
||||
}))
|
||||
console.log('Loaded libraries:', librariesOptions.value)
|
||||
} else {
|
||||
librariesOptions.value = []
|
||||
console.log('No libraries found')
|
||||
}
|
||||
librariesOptions.value.unshift({
|
||||
title: t('common.all'),
|
||||
value: 'all',
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('Error loading library:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择媒体服务器并自动加载媒体库
|
||||
async function selectMediaServerWithLibrary(type: string) {
|
||||
selectMediaServer(type)
|
||||
// 如果选择了媒体服务器类型,自动加载媒体库
|
||||
if (type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查是否需要加载媒体库
|
||||
onMounted(async () => {
|
||||
// 如果已经有媒体服务器配置,自动加载媒体库
|
||||
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听媒体服务器配置变化,自动加载媒体库
|
||||
watch(
|
||||
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
|
||||
async ([type, name]) => {
|
||||
console.log('Media server changed:', { type, name })
|
||||
if (type && name) {
|
||||
await loadLibrary(name)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.mediaServer.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.mediaServer.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.mediaServer.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.mediaServer.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 媒体服务器选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.mediaServer.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'emby' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'emby' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('emby')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/emby.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Emby</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'jellyfin' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'jellyfin' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('jellyfin')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/jellyfin.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Jellyfin</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'plex' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'plex' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('plex')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/plex.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Plex</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'trimemedia' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'trimemedia' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('trimemedia')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/trimemedia.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">飞牛影视</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 媒体服务器配置 -->
|
||||
<VCol v-if="wizardData.mediaServer.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow v-if="wizardData.mediaServer.type === 'emby'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.apikey"
|
||||
:label="t('mediaserver.apiKey')"
|
||||
:hint="t('mediaserver.embyApiKeyHint')"
|
||||
:error="validationErrors.mediaServer.apikey"
|
||||
:error-messages="validationErrors.mediaServer.apikey ? [t('mediaserver.apiKeyRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'jellyfin'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.apikey"
|
||||
:label="t('mediaserver.apiKey')"
|
||||
:hint="t('mediaserver.jellyfinApiKeyHint')"
|
||||
:error="validationErrors.mediaServer.apikey"
|
||||
:error-messages="validationErrors.mediaServer.apikey ? [t('mediaserver.apiKeyRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'trimemedia'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
:error="validationErrors.mediaServer.username"
|
||||
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="wizardData.mediaServer.config.password"
|
||||
:label="t('mediaserver.password')"
|
||||
:error="validationErrors.mediaServer.password"
|
||||
:error-messages="validationErrors.mediaServer.password ? [t('mediaserver.passwordRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'plex'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.token"
|
||||
:label="t('mediaserver.plexToken')"
|
||||
:hint="t('mediaserver.plexTokenHint')"
|
||||
:error="validationErrors.mediaServer.token"
|
||||
:error-messages="validationErrors.mediaServer.token ? [t('mediaserver.tokenRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.type"
|
||||
:label="t('mediaserver.type')"
|
||||
:hint="t('mediaserver.customTypeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:hint="t('mediaserver.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
552
src/views/setup/NotificationSettingsStep.vue
Normal file
552
src/views/setup/NotificationSettingsStep.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectNotification, validationErrors } = useSetupWizard()
|
||||
|
||||
// 消息类型下拉字典
|
||||
const notificationTypes = [
|
||||
{ value: '资源下载', title: t('notificationSwitch.resourceDownload') },
|
||||
{ value: '整理入库', title: t('notificationSwitch.organize') },
|
||||
{ value: '订阅', title: t('notificationSwitch.subscribe') },
|
||||
{ value: '站点', title: t('notificationSwitch.site') },
|
||||
{ value: '媒体服务器', title: t('notificationSwitch.mediaServer') },
|
||||
{ value: '手动处理', title: t('notificationSwitch.manual') },
|
||||
{ value: '插件', title: t('notificationSwitch.plugin') },
|
||||
{ value: '其它', title: t('notificationSwitch.other') },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.notification.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.notification.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.notification.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.notification.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 通知选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.notification.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'wechat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'wechat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('wechat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/wechat.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">微信</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'telegram' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'telegram' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('telegram')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/telegram.webp" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Telegram</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'slack' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'slack' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('slack')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/slack.webp" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Slack</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'synologychat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'synologychat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('synologychat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/synologychat.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Synology Chat</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'vocechat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'vocechat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('vocechat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg src="/src/assets/images/logos/vocechat.png" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">VoceChat</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'webpush' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'webpush' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('webpush')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VIcon icon="mdi-apple-safari" size="48" class="mb-2" />
|
||||
<div class="text-h6">WebPush</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 通知配置 -->
|
||||
<VCol v-if="wizardData.notification.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.notification.switchs"
|
||||
:items="notificationTypes"
|
||||
:label="t('notification.type')"
|
||||
:hint="t('notification.typeHint')"
|
||||
multiple
|
||||
clearable
|
||||
chips
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-bell-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="wizardData.notification.type === 'wechat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_CORPID"
|
||||
:label="t('notification.wechat.corpId')"
|
||||
:hint="t('notification.wechat.corpIdHint')"
|
||||
:error="validationErrors.notification.WECHAT_CORPID"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_CORPID ? [t('notification.wechat.corpIdRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_APP_ID"
|
||||
:label="t('notification.wechat.appId')"
|
||||
:hint="t('notification.wechat.appIdHint')"
|
||||
:error="validationErrors.notification.WECHAT_APP_ID"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_APP_ID ? [t('notification.wechat.appIdRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_APP_SECRET"
|
||||
:label="t('notification.wechat.appSecret')"
|
||||
:hint="t('notification.wechat.appSecretHint')"
|
||||
:error="validationErrors.notification.WECHAT_APP_SECRET"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_APP_SECRET
|
||||
? [t('notification.wechat.appSecretRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_PROXY"
|
||||
:label="t('notification.wechat.proxy')"
|
||||
:hint="t('notification.wechat.proxyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_TOKEN"
|
||||
:label="t('notification.wechat.token')"
|
||||
:hint="t('notification.wechat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_ENCODING_AESKEY"
|
||||
:label="t('notification.wechat.encodingAesKey')"
|
||||
:hint="t('notification.wechat.encodingAesKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_ADMINS"
|
||||
:label="t('notification.wechat.admins')"
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'telegram'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_TOKEN"
|
||||
:label="t('notification.telegram.token')"
|
||||
:hint="t('notification.telegram.tokenHint')"
|
||||
:error="validationErrors.notification.TELEGRAM_TOKEN"
|
||||
:error-messages="
|
||||
validationErrors.notification.TELEGRAM_TOKEN ? [t('notification.telegram.tokenRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_CHAT_ID"
|
||||
:label="t('notification.telegram.chatId')"
|
||||
:hint="t('notification.telegram.chatIdHint')"
|
||||
:error="validationErrors.notification.TELEGRAM_CHAT_ID"
|
||||
:error-messages="
|
||||
validationErrors.notification.TELEGRAM_CHAT_ID
|
||||
? [t('notification.telegram.chatIdRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-chat"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_USERS"
|
||||
:label="t('notification.telegram.users')"
|
||||
:placeholder="t('notification.telegram.usersPlaceholder')"
|
||||
:hint="t('notification.telegram.usersHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_ADMINS"
|
||||
:label="t('notification.telegram.admins')"
|
||||
:placeholder="t('notification.telegram.adminsPlaceholder')"
|
||||
:hint="t('notification.telegram.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.API_URL"
|
||||
:label="t('notification.telegram.apiUrl')"
|
||||
:placeholder="t('notification.telegram.apiUrlPlaceholder')"
|
||||
:hint="t('notification.telegram.apiUrlHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'slack'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_OAUTH_TOKEN"
|
||||
:label="t('notification.slack.oauthToken')"
|
||||
:placeholder="t('notification.slack.oauthTokenPlaceholder')"
|
||||
:hint="t('notification.slack.oauthTokenHint')"
|
||||
:error="validationErrors.notification.SLACK_OAUTH_TOKEN"
|
||||
:error-messages="
|
||||
validationErrors.notification.SLACK_OAUTH_TOKEN
|
||||
? [t('notification.slack.oauthTokenRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_APP_TOKEN"
|
||||
:label="t('notification.slack.appToken')"
|
||||
:placeholder="t('notification.slack.appTokenPlaceholder')"
|
||||
:hint="t('notification.slack.appTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_CHANNEL"
|
||||
:label="t('notification.slack.channel')"
|
||||
:placeholder="t('notification.slack.channelPlaceholder')"
|
||||
:hint="t('notification.slack.channelHint')"
|
||||
:error="validationErrors.notification.SLACK_CHANNEL"
|
||||
:error-messages="
|
||||
validationErrors.notification.SLACK_CHANNEL ? [t('notification.slack.channelRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'synologychat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SYNOLOGYCHAT_WEBHOOK"
|
||||
:label="t('notification.synologychat.webhook')"
|
||||
:hint="t('notification.synologychat.webhookHint')"
|
||||
:error="validationErrors.notification.SYNOLOGYCHAT_WEBHOOK"
|
||||
:error-messages="
|
||||
validationErrors.notification.SYNOLOGYCHAT_WEBHOOK
|
||||
? [t('notification.synologychat.webhookRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-webhook"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SYNOLOGYCHAT_TOKEN"
|
||||
:label="t('notification.synologychat.token')"
|
||||
:hint="t('notification.synologychat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'vocechat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_HOST"
|
||||
:label="t('notification.vocechat.host')"
|
||||
:hint="t('notification.vocechat.hostHint')"
|
||||
:error="validationErrors.notification.VOCECHAT_HOST"
|
||||
:error-messages="
|
||||
validationErrors.notification.VOCECHAT_HOST ? [t('notification.vocechat.hostRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_API_KEY"
|
||||
:label="t('notification.vocechat.apiKey')"
|
||||
:hint="t('notification.vocechat.apiKeyHint')"
|
||||
:error="validationErrors.notification.VOCECHAT_API_KEY"
|
||||
:error-messages="
|
||||
validationErrors.notification.VOCECHAT_API_KEY
|
||||
? [t('notification.vocechat.apiKeyRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_CHANNEL_ID"
|
||||
:label="t('notification.vocechat.channelId')"
|
||||
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
|
||||
:hint="t('notification.vocechat.channelIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'webpush'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WEBPUSH_USERNAME"
|
||||
:label="t('notification.webpush.username')"
|
||||
:hint="t('notification.webpush.usernameHint')"
|
||||
:error="validationErrors.notification.WEBPUSH_USERNAME"
|
||||
:error-messages="
|
||||
validationErrors.notification.WEBPUSH_USERNAME
|
||||
? [t('notification.webpush.usernameRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.type"
|
||||
:label="t('notification.type')"
|
||||
:hint="t('notification.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:hint="t('notification.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
279
src/views/setup/PreferencesSettingsStep.vue
Normal file
279
src/views/setup/PreferencesSettingsStep.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import api from '@/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { updatePreferences } = useSetupWizard()
|
||||
|
||||
// 个性化选项
|
||||
const personalizationOptions = ref({
|
||||
excludeDolbyVision: true, // 排除杜比视界
|
||||
excludeBluray: true, // 排除蓝光原盘
|
||||
})
|
||||
|
||||
// 预设配置 - 使用多语言
|
||||
const presetConfigs = computed(() => ({
|
||||
'4k-enthusiast': {
|
||||
name: t('setupWizard.preferences.presets.4k-enthusiast.name'),
|
||||
description: t('setupWizard.preferences.presets.4k-enthusiast.description'),
|
||||
icon: 'mdi-4k',
|
||||
color: 'primary',
|
||||
ruleString:
|
||||
' SPECSUB & 4K & 60FPS & UHD & !BLU & !DOLBY > CNSUB & 4K & 60FPS & UHD & !BLU & !DOLBY > 4K & 60FPS & UHD & !BLU & !DOLBY > SPECSUB & 4K & UHD & !BLU & !DOLBY > CNSUB & 4K & UHD & !BLU & !DOLBY > 4K & UHD & !BLU & !DOLBY > SPECSUB & 4K & !BLU & !DOLBY > CNSUB & 4K & !BLU & !DOLBY > 4K & !BLU & !DOLBY ',
|
||||
},
|
||||
'balanced': {
|
||||
name: t('setupWizard.preferences.presets.balanced.name'),
|
||||
description: t('setupWizard.preferences.presets.balanced.description'),
|
||||
icon: 'mdi-scale-unbalanced',
|
||||
color: 'success',
|
||||
ruleString:
|
||||
' SPECSUB & 4K & !BLU & !DOLBY & !UHD & !60FPS > CNSUB & 4K & !BLU & !DOLBY & !REMUX & !60FPS > SPECSUB & 1080P & !BLU & !DOLBY & !60FPS & !UHD > CNSUB & 1080P & !BLU & !DOLBY & !UHD & !60FPS > 4K & BLU & !DOLBY & !UHD & !60FPS > 1080P & !BLU & !DOLBY & !UHD & !60FPS ',
|
||||
},
|
||||
'space-saver': {
|
||||
name: t('setupWizard.preferences.presets.space-saver.name'),
|
||||
description: t('setupWizard.preferences.presets.space-saver.description'),
|
||||
icon: 'mdi-harddisk',
|
||||
color: 'warning',
|
||||
ruleString:
|
||||
' SPECSUB & 1080P & !BLU & !UHD & !60FPS & !DOLBY > CNSUB & 1080P & !BLU & !UHD & !60FPS & !DOLBY > 1080P & !BLU & !UHD & !60FPS & !DOLBY > !BLU & !UHD & !60FPS & !DOLBY ',
|
||||
},
|
||||
'free-priority': {
|
||||
name: t('setupWizard.preferences.presets.free-priority.name'),
|
||||
description: t('setupWizard.preferences.presets.free-priority.description'),
|
||||
icon: 'mdi-gift',
|
||||
color: 'info',
|
||||
ruleString:
|
||||
' SPECSUB & FREE & !BLU & !DOLBY > CNSUB & FREE & !BLU & !DOLBY > FREE & !BLU & !DOLBY > !BLU & !DOLBY ',
|
||||
},
|
||||
}))
|
||||
|
||||
// 当前选中的预设
|
||||
const selectedPreset = ref('')
|
||||
|
||||
// 加载用户当前的规则组设置
|
||||
async function loadUserFilterRuleGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||
if (result.success && result.data?.value && result.data.value.length > 0) {
|
||||
const userRuleGroups = result.data.value
|
||||
|
||||
// 查找匹配的预设
|
||||
for (const [presetKey, preset] of Object.entries(presetConfigs.value)) {
|
||||
const matchingRule = userRuleGroups.find((rule: any) => rule.name === preset.name)
|
||||
if (matchingRule) {
|
||||
selectedPreset.value = presetKey
|
||||
|
||||
// 分析规则字符串,判断个性化选项
|
||||
const ruleString = matchingRule.rule_string || ''
|
||||
personalizationOptions.value.excludeDolbyVision = ruleString.includes('!DOLBY')
|
||||
personalizationOptions.value.excludeBluray = ruleString.includes('!BLU')
|
||||
|
||||
// 更新向导数据
|
||||
updateWizardData()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Load user filter rule groups failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择预设
|
||||
function selectPreset(presetKey: string) {
|
||||
if (selectedPreset.value === presetKey) {
|
||||
// 如果再次点击同一个预设,则取消选择
|
||||
selectedPreset.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
selectedPreset.value = presetKey
|
||||
updateWizardData()
|
||||
}
|
||||
|
||||
// 生成规则序列的逻辑
|
||||
const generateRuleSequences = computed(() => {
|
||||
if (!selectedPreset.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const preset = presetConfigs.value[selectedPreset.value as keyof typeof presetConfigs.value]
|
||||
if (!preset) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ruleString = preset.ruleString
|
||||
|
||||
// 根据个性化选项调整规则
|
||||
if (!personalizationOptions.value.excludeDolbyVision) {
|
||||
// 移除所有 !DOLBY 条件
|
||||
ruleString = ruleString.replace(/ & !DOLBY/g, '').replace(/!DOLBY & /g, '')
|
||||
}
|
||||
|
||||
if (!personalizationOptions.value.excludeBluray) {
|
||||
// 移除所有 !BLU 条件
|
||||
ruleString = ruleString.replace(/ & !BLU/g, '').replace(/!BLU & /g, '')
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: preset.name,
|
||||
rule_string: ruleString,
|
||||
media_type: '',
|
||||
category: '',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 监听偏好变化,更新到wizardData
|
||||
function updateWizardData() {
|
||||
if (updatePreferences) {
|
||||
updatePreferences(personalizationOptions.value, generateRuleSequences.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载用户设置
|
||||
onMounted(() => {
|
||||
loadUserFilterRuleGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.preferences.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.preferences.description') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 快速预设 -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle class="text-h6 d-flex align-center">
|
||||
<VIcon icon="mdi-flash" class="me-2" />
|
||||
{{ t('setupWizard.preferences.quickPresets') }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">{{ t('setupWizard.preferences.quickPresetsDesc') }}</p>
|
||||
<VRow>
|
||||
<VCol v-for="(preset, key) in presetConfigs" :key="key" cols="12" sm="6" md="3">
|
||||
<VCard
|
||||
:color="selectedPreset === key ? preset.color : 'default'"
|
||||
:variant="selectedPreset === key ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer preset-card"
|
||||
@click="selectPreset(key)"
|
||||
>
|
||||
<VCardText class="text-center pa-4">
|
||||
<VIcon :icon="preset.icon" size="40" class="mb-3" />
|
||||
<div class="text-h6 mb-2">{{ preset.name }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">{{ preset.description }}</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 个性化选项 -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle class="text-h6 d-flex align-center">
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
{{ t('setupWizard.preferences.personalizationOptions') }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
{{ t('setupWizard.preferences.personalizationOptionsDesc') }}
|
||||
</p>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="personalizationOptions.excludeDolbyVision"
|
||||
:label="t('setupWizard.preferences.excludeDolbyVision')"
|
||||
color="primary"
|
||||
hide-details
|
||||
@change="updateWizardData"
|
||||
/>
|
||||
<p class="text-caption text-medium-emphasis mt-1">
|
||||
{{ t('setupWizard.preferences.excludeDolbyVisionHint') }}
|
||||
</p>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="personalizationOptions.excludeBluray"
|
||||
:label="t('setupWizard.preferences.excludeBluray')"
|
||||
color="primary"
|
||||
hide-details
|
||||
@change="updateWizardData"
|
||||
/>
|
||||
<p class="text-caption text-medium-emphasis mt-1">{{ t('setupWizard.preferences.excludeBlurayHint') }}</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.preset-card:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 预设卡片选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
|
||||
/* 规则代码样式 */
|
||||
.v-code {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 展开面板样式 */
|
||||
.v-expansion-panel-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.v-expansion-panel-text {
|
||||
padding-block-start: 16px;
|
||||
}
|
||||
|
||||
/* 开关组件样式优化 */
|
||||
.v-switch {
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
|
||||
/* 芯片组样式 */
|
||||
.v-chip-group {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.v-chip {
|
||||
margin-block: 4px;
|
||||
margin-inline: 0;
|
||||
}
|
||||
</style>
|
||||
94
src/views/setup/StorageSettingsStep.vue
Normal file
94
src/views/setup/StorageSettingsStep.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, validateCurrentStep } = useSetupWizard()
|
||||
|
||||
// 验证状态
|
||||
const validation = computed(() => validateCurrentStep())
|
||||
const hasErrors = computed(() => !validation.value.isValid)
|
||||
|
||||
// 整理方式选项
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
]
|
||||
|
||||
// 覆盖模式选项
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '按文件大小', value: 'size' },
|
||||
{ title: '仅保留最新', value: 'latest' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.storage.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.storage.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.storage.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.storage.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VPathField
|
||||
v-model="wizardData.storage.downloadPath"
|
||||
:label="t('setupWizard.storage.downloadPath')"
|
||||
:hint="t('setupWizard.storage.downloadPathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-download"
|
||||
placeholder="/downloads"
|
||||
:error="!wizardData.storage.downloadPath && hasErrors"
|
||||
:error-messages="
|
||||
!wizardData.storage.downloadPath && hasErrors ? [t('setupWizard.storage.downloadPathRequired')] : []
|
||||
"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VPathField
|
||||
v-model="wizardData.storage.libraryPath"
|
||||
:label="t('setupWizard.storage.libraryPath')"
|
||||
:hint="t('setupWizard.storage.libraryPathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-folder-multiple"
|
||||
placeholder="/media"
|
||||
:error="!wizardData.storage.libraryPath && hasErrors"
|
||||
:error-messages="
|
||||
!wizardData.storage.libraryPath && hasErrors ? [t('setupWizard.storage.libraryPathRequired')] : []
|
||||
"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="wizardData.storage.transferType"
|
||||
:label="t('directory.transferType')"
|
||||
:hint="t('directory.transferTypeHint')"
|
||||
persistent-hint
|
||||
:items="transferTypeItems"
|
||||
prepend-inner-icon="mdi-swap-horizontal"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="wizardData.storage.overwriteMode"
|
||||
:label="t('directory.overwriteMode')"
|
||||
:hint="t('directory.overwriteModeHint')"
|
||||
persistent-hint
|
||||
:items="overwriteModeItems"
|
||||
prepend-inner-icon="mdi-file-replace"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user