更新用户配置

This commit is contained in:
jxxghp
2025-09-10 08:30:19 +08:00
parent 5ecbf626c8
commit 7c8e94d1df
4 changed files with 398 additions and 210 deletions
+5
View File
@@ -381,8 +381,11 @@ export default {
addUser: 'Add User', addUser: 'Add User',
editUser: 'Edit User', editUser: 'Edit User',
username: 'Username', username: 'Username',
usernameHint: 'Username for system login',
password: 'Password', password: 'Password',
passwordHint: 'Password for system login',
confirmPassword: 'Confirm Password', confirmPassword: 'Confirm Password',
confirmPasswordHint: 'Please enter the password again to confirm',
role: 'Role', role: 'Role',
email: 'Email', email: 'Email',
enabled: 'Enabled', enabled: 'Enabled',
@@ -2839,7 +2842,9 @@ export default {
libraryStorage: 'Library Storage', libraryStorage: 'Library Storage',
libraryDirectory: 'Library Directory', libraryDirectory: 'Library Directory',
transferType: 'Transfer Type', transferType: 'Transfer Type',
transferTypeHint: 'File operation organization method, hard link saves space, copy is safer',
overwriteMode: 'Overwrite Mode', overwriteMode: 'Overwrite Mode',
overwriteModeHint: 'How to handle when target file already exists',
smartRename: 'Smart Rename', smartRename: 'Smart Rename',
scrapingMetadata: 'Scrape Metadata', scrapingMetadata: 'Scrape Metadata',
sendNotification: 'Send Notification', sendNotification: 'Send Notification',
+5
View File
@@ -379,8 +379,11 @@ export default {
addUser: '添加用户', addUser: '添加用户',
editUser: '编辑用户', editUser: '编辑用户',
username: '用户名', username: '用户名',
usernameHint: '用于登录系统的用户名',
password: '密码', password: '密码',
passwordHint: '用于登录系统的密码',
confirmPassword: '确认密码', confirmPassword: '确认密码',
confirmPasswordHint: '请再次输入密码以确认',
role: '角色', role: '角色',
email: '邮箱', email: '邮箱',
enabled: '启用', enabled: '启用',
@@ -2807,7 +2810,9 @@ export default {
libraryStorage: '媒体库存储', libraryStorage: '媒体库存储',
libraryDirectory: '媒体库目录', libraryDirectory: '媒体库目录',
transferType: '整理方式', transferType: '整理方式',
transferTypeHint: '文件操作整理方式,硬链接节省空间,复制更安全',
overwriteMode: '覆盖模式', overwriteMode: '覆盖模式',
overwriteModeHint: '当目标文件已存在时的处理方式',
smartRename: '智能重命名', smartRename: '智能重命名',
scrapingMetadata: '刮削元数据', scrapingMetadata: '刮削元数据',
sendNotification: '发送通知', sendNotification: '发送通知',
+5
View File
@@ -380,8 +380,11 @@ export default {
addUser: '添加用戶', addUser: '添加用戶',
editUser: '編輯用戶', editUser: '編輯用戶',
username: '用戶名', username: '用戶名',
usernameHint: '用於登入系統的用戶名',
password: '密碼', password: '密碼',
passwordHint: '用於登入系統的密碼',
confirmPassword: '確認密碼', confirmPassword: '確認密碼',
confirmPasswordHint: '請再次輸入密碼以確認',
role: '角色', role: '角色',
email: '郵箱', email: '郵箱',
enabled: '啟用', enabled: '啟用',
@@ -2806,7 +2809,9 @@ export default {
libraryStorage: '媒體庫存儲', libraryStorage: '媒體庫存儲',
libraryDirectory: '媒體庫目錄', libraryDirectory: '媒體庫目錄',
transferType: '轉移方式', transferType: '轉移方式',
transferTypeHint: '文件操作整理方式,硬連結節省空間,複製更安全',
overwriteMode: '覆蓋模式', overwriteMode: '覆蓋模式',
overwriteModeHint: '當目標文件已存在時的處理方式',
smartRename: '智能重命名', smartRename: '智能重命名',
scrapingMetadata: '刮削元數據', scrapingMetadata: '刮削元數據',
sendNotification: '發送通知', sendNotification: '發送通知',
+383 -210
View File
@@ -21,14 +21,19 @@ const wizardData = ref({
// 步骤1:基础参数 // 步骤1:基础参数
basic: { basic: {
appDomain: '', appDomain: '',
wallpaper: 'tmdb',
recognizeSource: 'themoviedb',
apiToken: '', apiToken: '',
username: '',
password: '',
confirmPassword: '',
proxyHost: '',
githubToken: '',
}, },
// 步骤2:存储目录 // 步骤2:存储目录
storage: { storage: {
downloadPath: '', downloadPath: '',
libraryPath: '', libraryPath: '',
transferType: 'link',
overwriteMode: 'never',
}, },
// 步骤3:下载器 // 步骤3:下载器
downloader: { downloader: {
@@ -79,18 +84,24 @@ const stepDescriptions = [
t('setupWizard.step6.description'), t('setupWizard.step6.description'),
] ]
// 壁纸选项 // 密码可见性控制
const wallpaperItems = [ const isPasswordVisible = ref(false)
{ title: t('setting.system.wallpaperItems.tmdb'), value: 'tmdb' }, const isConfirmPasswordVisible = ref(false)
{ title: t('setting.system.wallpaperItems.bing'), value: 'bing' },
{ title: t('setting.system.wallpaperItems.mediaserver'), value: 'mediaserver' }, // 整理方式选项
{ title: t('setting.system.wallpaperItems.none'), value: '' }, const transferTypeItems = [
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: '复制', value: 'copy' },
{ title: '移动', value: 'move' },
] ]
// 识别源选项 // 覆盖模式选项
const recognizeSourceItems = [ const overwriteModeItems = [
{ title: 'TheMovieDb', value: 'themoviedb' }, { title: '从不覆盖', value: 'never' },
{ title: '豆瓣', value: 'douban' }, { title: '总是覆盖', value: 'always' },
{ title: '按文件大小', value: 'size' },
{ title: '仅保留最新', value: 'latest' },
] ]
// 质量偏好选项 // 质量偏好选项
@@ -182,8 +193,11 @@ function selectPreset(preset: string) {
} }
// 下一步 // 下一步
function nextStep() { async function nextStep() {
if (currentStep.value < totalSteps) { if (currentStep.value < totalSteps) {
// 保存当前步骤的设置
await saveCurrentStepSettings()
currentStep.value++ currentStep.value++
} }
} }
@@ -195,25 +209,48 @@ function prevStep() {
} }
} }
// 保存当前步骤的设置
async function saveCurrentStepSettings() {
try {
switch (currentStep.value) {
case 1:
await saveBasicSettings()
break
case 2:
await saveStorageSettings()
break
case 3:
await saveDownloaderSettings()
break
case 4:
await saveMediaServerSettings()
break
case 5:
await saveNotificationSettings()
break
case 6:
await savePreferenceSettings()
break
}
} catch (error) {
console.error('Save current step settings failed:', error)
$toast.error(t('setupWizard.saveStepFailed'))
}
}
// 完成向导 // 完成向导
async function completeWizard() { async function completeWizard() {
try { try {
// 保存基础设置 // 验证密码一致性
await saveBasicSettings() if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) {
$toast.error(t('user.passwordMismatch'))
return
}
// 保存存储配置 // 创建用户(如果还没有创建)
await saveStorageSettings() await createUser()
// 保存下载器配置 // 保存最后一步的设置(资源偏好)
await saveDownloaderSettings()
// 保存媒体服务器配置
await saveMediaServerSettings()
// 保存通知配置
await saveNotificationSettings()
// 保存资源偏好
await savePreferenceSettings() await savePreferenceSettings()
$toast.success(t('setupWizard.completed')) $toast.success(t('setupWizard.completed'))
@@ -224,104 +261,292 @@ async function completeWizard() {
} }
} }
// 创建用户
async function createUser() {
if (wizardData.value.basic.username && wizardData.value.basic.password) {
try {
// 检查用户是否已存在
const response = await api.get('user/')
const existingUsers = response.data || []
const userExists = existingUsers.some((user: any) => user.name === wizardData.value.basic.username)
if (!userExists) {
const userData = {
name: wizardData.value.basic.username,
password: wizardData.value.basic.password,
is_active: true,
is_superuser: true,
}
await api.post('user/', userData)
}
} catch (error) {
console.log('Create user failed or user already exists:', error)
}
}
}
// 保存基础设置 // 保存基础设置
async function saveBasicSettings() { async function saveBasicSettings() {
const basicSettings = { try {
APP_DOMAIN: wizardData.value.basic.appDomain, const basicSettings = {
WALLPAPER: wizardData.value.basic.wallpaper, APP_DOMAIN: wizardData.value.basic.appDomain,
RECOGNIZE_SOURCE: wizardData.value.basic.recognizeSource, API_TOKEN: wizardData.value.basic.apiToken,
API_TOKEN: wizardData.value.basic.apiToken, PROXY_HOST: wizardData.value.basic.proxyHost,
} GITHUB_TOKEN: wizardData.value.basic.githubToken,
}
await api.post('system/env', basicSettings) const response = await api.post('system/env', basicSettings)
if (response.data?.success) {
$toast.success(t('setupWizard.basicSettingsSaved'))
}
} catch (error) {
console.error('Save basic settings failed:', error)
$toast.error(t('setupWizard.saveBasicSettingsFailed'))
}
} }
// 保存存储配置 // 保存存储配置
async function saveStorageSettings() { async function saveStorageSettings() {
// 创建本地存储 try {
const storage = { // 创建本地存储
name: '本地存储', const storage = {
type: 'local', name: '本地存储',
config: {}, type: 'local',
config: {},
}
await api.post('system/setting/Storages', [storage])
// 创建目录配置
const directory = {
name: '默认目录',
storage: 'local',
download_path: wizardData.value.storage.downloadPath,
library_path: wizardData.value.storage.libraryPath,
priority: 0,
monitor_type: 'compatibility',
media_type: 'movie,tv',
media_category: 'default',
transfer_type: wizardData.value.storage.transferType,
overwrite_mode: wizardData.value.storage.overwriteMode,
}
const response = await api.post('system/setting/Directories', [directory])
if (response.data?.success) {
$toast.success(t('setupWizard.storageSettingsSaved'))
}
} catch (error) {
console.error('Save storage settings failed:', error)
$toast.error(t('setupWizard.saveStorageSettingsFailed'))
} }
await api.post('system/setting/Storages', [storage])
// 创建目录配置
const directory = {
name: '默认目录',
storage: 'local',
download_path: wizardData.value.storage.downloadPath,
library_path: wizardData.value.storage.libraryPath,
priority: 0,
monitor_type: 'compatibility',
media_type: 'movie,tv',
media_category: 'default',
transfer_type: 'link',
}
await api.post('system/setting/Directories', [directory])
} }
// 保存下载器配置 // 保存下载器配置
async function saveDownloaderSettings() { async function saveDownloaderSettings() {
if (wizardData.value.downloader.type) { if (wizardData.value.downloader.type) {
const downloader = { try {
name: wizardData.value.downloader.name, const downloader = {
type: wizardData.value.downloader.type, name: wizardData.value.downloader.name,
default: true, type: wizardData.value.downloader.type,
enabled: true, default: true,
config: wizardData.value.downloader.config, enabled: true,
} config: wizardData.value.downloader.config,
}
await api.post('system/setting/Downloaders', [downloader]) const response = await api.post('system/setting/Downloaders', [downloader])
if (response.data?.success) {
$toast.success(t('setupWizard.downloaderSettingsSaved'))
}
} catch (error) {
console.error('Save downloader settings failed:', error)
$toast.error(t('setupWizard.saveDownloaderSettingsFailed'))
}
} }
} }
// 保存媒体服务器配置 // 保存媒体服务器配置
async function saveMediaServerSettings() { async function saveMediaServerSettings() {
if (wizardData.value.mediaServer.type) { if (wizardData.value.mediaServer.type) {
const mediaServer = { try {
name: wizardData.value.mediaServer.name, const mediaServer = {
type: wizardData.value.mediaServer.type, name: wizardData.value.mediaServer.name,
enabled: true, type: wizardData.value.mediaServer.type,
config: wizardData.value.mediaServer.config, enabled: true,
} config: wizardData.value.mediaServer.config,
}
await api.post('system/setting/MediaServers', [mediaServer]) const response = await api.post('system/setting/MediaServers', [mediaServer])
if (response.data?.success) {
$toast.success(t('setupWizard.mediaServerSettingsSaved'))
}
} catch (error) {
console.error('Save media server settings failed:', error)
$toast.error(t('setupWizard.saveMediaServerSettingsFailed'))
}
} }
} }
// 保存通知配置 // 保存通知配置
async function saveNotificationSettings() { async function saveNotificationSettings() {
if (wizardData.value.notification.type) { if (wizardData.value.notification.type) {
const notification = { try {
name: wizardData.value.notification.name, const notification = {
type: wizardData.value.notification.type, name: wizardData.value.notification.name,
enabled: true, type: wizardData.value.notification.type,
config: wizardData.value.notification.config, enabled: true,
} config: wizardData.value.notification.config,
}
await api.post('system/setting/Notifications', [notification]) const response = await api.post('system/setting/Notifications', [notification])
if (response.data?.success) {
$toast.success(t('setupWizard.notificationSettingsSaved'))
}
} catch (error) {
console.error('Save notification settings failed:', error)
$toast.error(t('setupWizard.saveNotificationSettingsFailed'))
}
} }
} }
// 保存资源偏好设置 // 保存资源偏好设置
async function savePreferenceSettings() { async function savePreferenceSettings() {
// 这里可以根据偏好设置创建相应的过滤规则 try {
// 暂时保存到系统设置中 // 这里可以根据偏好设置创建相应的过滤规则
const preferenceSettings = { // 暂时保存到系统设置中
QUALITY_PREFERENCE: wizardData.value.preferences.quality, const preferenceSettings = {
SUBTITLE_PREFERENCE: wizardData.value.preferences.subtitle, QUALITY_PREFERENCE: wizardData.value.preferences.quality,
RESOLUTION_PREFERENCE: wizardData.value.preferences.resolution, SUBTITLE_PREFERENCE: wizardData.value.preferences.subtitle,
} RESOLUTION_PREFERENCE: wizardData.value.preferences.resolution,
}
await api.post('system/env', preferenceSettings) const response = await api.post('system/env', preferenceSettings)
if (response.data?.success) {
$toast.success(t('setupWizard.preferenceSettingsSaved'))
}
} catch (error) {
console.error('Save preference settings failed:', error)
$toast.error(t('setupWizard.savePreferenceSettingsFailed'))
}
}
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
// 加载基础设置
if (result.data.APP_DOMAIN) {
wizardData.value.basic.appDomain = result.data.APP_DOMAIN
}
if (result.data.API_TOKEN) {
wizardData.value.basic.apiToken = result.data.API_TOKEN
}
if (result.data.PROXY_HOST) {
wizardData.value.basic.proxyHost = result.data.PROXY_HOST
}
if (result.data.GITHUB_TOKEN) {
wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN
}
}
} catch (error) {
console.log('Load system settings failed:', error)
}
}
// 加载存储设置
async function loadStorageSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Directories')
if (result.success && result.data?.value && result.data.value.length > 0) {
const directory = result.data.value[0]
wizardData.value.storage.downloadPath = directory.download_path || ''
wizardData.value.storage.libraryPath = directory.library_path || ''
wizardData.value.storage.transferType = directory.transfer_type || 'link'
wizardData.value.storage.overwriteMode = directory.overwrite_mode || 'never'
}
} catch (error) {
console.log('Load storage settings failed:', error)
}
}
// 加载下载器设置
async function loadDownloaderSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
if (result.success && result.data?.value && result.data.value.length > 0) {
const downloader = result.data.value[0]
wizardData.value.downloader.type = downloader.type
wizardData.value.downloader.name = downloader.name
wizardData.value.downloader.config = downloader.config || {}
}
} catch (error) {
console.log('Load downloader settings failed:', error)
}
}
// 加载媒体服务器设置
async function loadMediaServerSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MediaServers')
if (result.success && result.data?.value && result.data.value.length > 0) {
const mediaServer = result.data.value[0]
wizardData.value.mediaServer.type = mediaServer.type
wizardData.value.mediaServer.name = mediaServer.name
wizardData.value.mediaServer.config = mediaServer.config || {}
wizardData.value.mediaServer.sync_libraries = mediaServer.sync_libraries || []
}
} catch (error) {
console.log('Load media server settings failed:', error)
}
}
// 加载通知设置
async function loadNotificationSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
if (result.success && result.data?.value && result.data.value.length > 0) {
const notification = result.data.value[0]
wizardData.value.notification.type = notification.type
wizardData.value.notification.name = notification.name
wizardData.value.notification.enabled = notification.enabled
wizardData.value.notification.config = notification.config || {}
wizardData.value.notification.switchs = notification.switchs || []
}
} catch (error) {
console.log('Load notification settings failed:', error)
}
}
// 加载资源偏好设置
async function loadPreferenceSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
if (result.data.QUALITY_PREFERENCE) {
wizardData.value.preferences.quality = result.data.QUALITY_PREFERENCE
}
if (result.data.SUBTITLE_PREFERENCE) {
wizardData.value.preferences.subtitle = result.data.SUBTITLE_PREFERENCE
}
if (result.data.RESOLUTION_PREFERENCE) {
wizardData.value.preferences.resolution = result.data.RESOLUTION_PREFERENCE
}
}
} catch (error) {
console.log('Load preference settings failed:', error)
}
} }
// 初始化 // 初始化
onMounted(() => { onMounted(async () => {
createRandomString() createRandomString()
await loadSystemSettings()
await loadStorageSettings()
await loadDownloaderSettings()
await loadMediaServerSettings()
await loadNotificationSettings()
await loadPreferenceSettings()
}) })
</script> </script>
@@ -382,23 +607,62 @@ onMounted(() => {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSelect <VTextField
v-model="wizardData.basic.wallpaper" v-model="wizardData.basic.username"
:label="t('setupWizard.basic.wallpaper')" :label="t('user.username')"
:hint="t('setupWizard.basic.wallpaperHint')" :hint="t('user.usernameHint')"
persistent-hint persistent-hint
:items="wallpaperItems" prepend-inner-icon="mdi-account"
prepend-inner-icon="mdi-image" :rules="[(v: string) => !!v || t('user.usernameRequired')]"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSelect <VTextField
v-model="wizardData.basic.recognizeSource" v-model="wizardData.basic.password"
:label="t('setupWizard.basic.recognizeSource')" :type="isPasswordVisible ? 'text' : 'password'"
:hint="t('setupWizard.basic.recognizeSourceHint')" :label="t('user.password')"
:hint="t('user.passwordHint')"
persistent-hint persistent-hint
:items="recognizeSourceItems" prepend-inner-icon="mdi-lock"
prepend-inner-icon="mdi-database" :append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
:rules="[(v: string) => !!v || t('user.passwordRequired'), (v: string) => v.length >= 6 || t('user.passwordMinLength')]"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.basic.confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:label="t('user.confirmPassword')"
:hint="t('user.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"
:rules="[
(v: string) => !!v || t('user.confirmPasswordRequired'),
(v: string) => v === wizardData.basic.password || t('user.passwordMismatch')
]"
/>
</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>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -455,6 +719,26 @@ onMounted(() => {
placeholder="/media" placeholder="/media"
/> />
</VCol> </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> </VRow>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -1621,117 +1905,6 @@ onMounted(() => {
</VCardText> </VCardText>
</VCard> </VCard>
</VStepperWindowItem> </VStepperWindowItem>
<!-- 步骤6资源偏好 -->
<VStepperWindowItem :value="6">
<VCard variant="outlined">
<VCardText>
<div class="text-center mb-6">
<h3 class="text-h4 mb-2">{{ stepTitles[5] }}</h3>
<p class="text-body-1 text-medium-emphasis">{{ stepDescriptions[5] }}</p>
</div>
<VRow>
<VCol cols="12">
<VAlert type="info" variant="tonal" class="mb-4">
<VAlertTitle>{{ t('setupWizard.preferences.info') }}</VAlertTitle>
{{ t('setupWizard.preferences.infoDesc') }}
</VAlert>
</VCol>
<!-- 预设规则选择 -->
<VCol cols="12">
<VCard>
<VCardTitle class="text-h6">{{ t('setupWizard.preferences.presetRules') }}</VCardTitle>
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VCard
:color="selectedPreset === '4k' ? 'primary' : 'default'"
:variant="selectedPreset === '4k' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectPreset('4k')"
>
<VCardText class="text-center">
<VIcon icon="mdi-4k" size="48" class="mb-2" />
<div class="text-h6">4K 优先</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="4">
<VCard
:color="selectedPreset === 'balanced' ? 'primary' : 'default'"
:variant="selectedPreset === 'balanced' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectPreset('balanced')"
>
<VCardText class="text-center">
<VIcon icon="mdi-balance-scale" size="48" class="mb-2" />
<div class="text-h6">平衡模式</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="4">
<VCard
:color="selectedPreset === 'chinese' ? 'primary' : 'default'"
:variant="selectedPreset === 'chinese' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectPreset('chinese')"
>
<VCardText class="text-center">
<VIcon icon="mdi-translate" size="48" class="mb-2" />
<div class="text-h6">中文字幕</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<!-- 详细配置 -->
<VCol cols="12">
<VCard>
<VCardTitle class="text-h6">{{ t('setupWizard.preferences.detailedConfig') }}</VCardTitle>
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VSelect
v-model="wizardData.preferences.quality"
:label="t('setupWizard.preferences.quality')"
:hint="t('setupWizard.preferences.qualityHint')"
persistent-hint
:items="qualityItems"
prepend-inner-icon="mdi-star"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="wizardData.preferences.subtitle"
:label="t('setupWizard.preferences.subtitle')"
:hint="t('setupWizard.preferences.subtitleHint')"
persistent-hint
:items="subtitleItems"
prepend-inner-icon="mdi-subtitles"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="wizardData.preferences.resolution"
:label="t('setupWizard.preferences.resolution')"
:hint="t('setupWizard.preferences.resolutionHint')"
persistent-hint
:items="resolutionItems"
prepend-inner-icon="mdi-monitor"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
</VRow>
</VCardText>
</VCard>
</VStepperWindowItem>
</VStepperWindow> </VStepperWindow>
<!-- 操作按钮 --> <!-- 操作按钮 -->