更新用户配置

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

View File

@@ -381,8 +381,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',
@@ -2839,7 +2842,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',

View File

@@ -379,8 +379,11 @@ export default {
addUser: '添加用户',
editUser: '编辑用户',
username: '用户名',
usernameHint: '用于登录系统的用户名',
password: '密码',
passwordHint: '用于登录系统的密码',
confirmPassword: '确认密码',
confirmPasswordHint: '请再次输入密码以确认',
role: '角色',
email: '邮箱',
enabled: '启用',
@@ -2807,7 +2810,9 @@ export default {
libraryStorage: '媒体库存储',
libraryDirectory: '媒体库目录',
transferType: '整理方式',
transferTypeHint: '文件操作整理方式,硬链接节省空间,复制更安全',
overwriteMode: '覆盖模式',
overwriteModeHint: '当目标文件已存在时的处理方式',
smartRename: '智能重命名',
scrapingMetadata: '刮削元数据',
sendNotification: '发送通知',

View File

@@ -380,8 +380,11 @@ export default {
addUser: '添加用戶',
editUser: '編輯用戶',
username: '用戶名',
usernameHint: '用於登入系統的用戶名',
password: '密碼',
passwordHint: '用於登入系統的密碼',
confirmPassword: '確認密碼',
confirmPasswordHint: '請再次輸入密碼以確認',
role: '角色',
email: '郵箱',
enabled: '啟用',
@@ -2806,7 +2809,9 @@ export default {
libraryStorage: '媒體庫存儲',
libraryDirectory: '媒體庫目錄',
transferType: '轉移方式',
transferTypeHint: '文件操作整理方式,硬連結節省空間,複製更安全',
overwriteMode: '覆蓋模式',
overwriteModeHint: '當目標文件已存在時的處理方式',
smartRename: '智能重命名',
scrapingMetadata: '刮削元數據',
sendNotification: '發送通知',

View File

@@ -21,14 +21,19 @@ const wizardData = ref({
// 步骤1基础参数
basic: {
appDomain: '',
wallpaper: 'tmdb',
recognizeSource: 'themoviedb',
apiToken: '',
username: '',
password: '',
confirmPassword: '',
proxyHost: '',
githubToken: '',
},
// 步骤2存储目录
storage: {
downloadPath: '',
libraryPath: '',
transferType: 'link',
overwriteMode: 'never',
},
// 步骤3下载器
downloader: {
@@ -79,18 +84,24 @@ const stepDescriptions = [
t('setupWizard.step6.description'),
]
// 壁纸选项
const wallpaperItems = [
{ title: t('setting.system.wallpaperItems.tmdb'), value: 'tmdb' },
{ title: t('setting.system.wallpaperItems.bing'), value: 'bing' },
{ title: t('setting.system.wallpaperItems.mediaserver'), value: 'mediaserver' },
{ title: t('setting.system.wallpaperItems.none'), value: '' },
// 密码可见性控制
const isPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
// 整理方式选项
const transferTypeItems = [
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: '复制', value: 'copy' },
{ title: '移动', value: 'move' },
]
// 识别源选项
const recognizeSourceItems = [
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
// 覆盖模式选项
const overwriteModeItems = [
{ title: '从不覆盖', value: 'never' },
{ 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) {
// 保存当前步骤的设置
await saveCurrentStepSettings()
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() {
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()
$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() {
const basicSettings = {
APP_DOMAIN: wizardData.value.basic.appDomain,
WALLPAPER: wizardData.value.basic.wallpaper,
RECOGNIZE_SOURCE: wizardData.value.basic.recognizeSource,
API_TOKEN: wizardData.value.basic.apiToken,
}
try {
const basicSettings = {
APP_DOMAIN: wizardData.value.basic.appDomain,
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() {
// 创建本地存储
const storage = {
name: '本地存储',
type: 'local',
config: {},
try {
// 创建本地存储
const storage = {
name: '本地存储',
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() {
if (wizardData.value.downloader.type) {
const downloader = {
name: wizardData.value.downloader.name,
type: wizardData.value.downloader.type,
default: true,
enabled: true,
config: wizardData.value.downloader.config,
}
try {
const downloader = {
name: wizardData.value.downloader.name,
type: wizardData.value.downloader.type,
default: true,
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() {
if (wizardData.value.mediaServer.type) {
const mediaServer = {
name: wizardData.value.mediaServer.name,
type: wizardData.value.mediaServer.type,
enabled: true,
config: wizardData.value.mediaServer.config,
}
try {
const mediaServer = {
name: wizardData.value.mediaServer.name,
type: wizardData.value.mediaServer.type,
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() {
if (wizardData.value.notification.type) {
const notification = {
name: wizardData.value.notification.name,
type: wizardData.value.notification.type,
enabled: true,
config: wizardData.value.notification.config,
}
try {
const notification = {
name: wizardData.value.notification.name,
type: wizardData.value.notification.type,
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() {
// 这里可以根据偏好设置创建相应的过滤规则
// 暂时保存到系统设置中
const preferenceSettings = {
QUALITY_PREFERENCE: wizardData.value.preferences.quality,
SUBTITLE_PREFERENCE: wizardData.value.preferences.subtitle,
RESOLUTION_PREFERENCE: wizardData.value.preferences.resolution,
}
try {
// 这里可以根据偏好设置创建相应的过滤规则
// 暂时保存到系统设置中
const preferenceSettings = {
QUALITY_PREFERENCE: wizardData.value.preferences.quality,
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()
await loadSystemSettings()
await loadStorageSettings()
await loadDownloaderSettings()
await loadMediaServerSettings()
await loadNotificationSettings()
await loadPreferenceSettings()
})
</script>
@@ -382,23 +607,62 @@ onMounted(() => {
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.basic.wallpaper"
:label="t('setupWizard.basic.wallpaper')"
:hint="t('setupWizard.basic.wallpaperHint')"
<VTextField
v-model="wizardData.basic.username"
:label="t('user.username')"
:hint="t('user.usernameHint')"
persistent-hint
:items="wallpaperItems"
prepend-inner-icon="mdi-image"
prepend-inner-icon="mdi-account"
:rules="[(v: string) => !!v || t('user.usernameRequired')]"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.basic.recognizeSource"
:label="t('setupWizard.basic.recognizeSource')"
:hint="t('setupWizard.basic.recognizeSourceHint')"
<VTextField
v-model="wizardData.basic.password"
:type="isPasswordVisible ? 'text' : 'password'"
:label="t('user.password')"
:hint="t('user.passwordHint')"
persistent-hint
:items="recognizeSourceItems"
prepend-inner-icon="mdi-database"
prepend-inner-icon="mdi-lock"
: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 cols="12" md="6">
@@ -455,6 +719,26 @@ onMounted(() => {
placeholder="/media"
/>
</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>
@@ -1621,117 +1905,6 @@ onMounted(() => {
</VCardText>
</VCard>
</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>
<!-- 操作按钮 -->