feat: add admin config diagnostics
This commit is contained in:
@@ -2,7 +2,13 @@ import { ref, computed } from 'vue'
|
|||||||
import { ConfigService } from '@/services'
|
import { ConfigService } from '@/services'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
import { useConfigStore } from '@/stores/configStore'
|
import { useConfigStore } from '@/stores/configStore'
|
||||||
import type { ConfigState } from '@/types'
|
import type {
|
||||||
|
ConfigDiagnosticItem,
|
||||||
|
ConfigDiagnosticsResponse,
|
||||||
|
ConfigDiagnosticSeverity,
|
||||||
|
ConfigDiagnosticSummary,
|
||||||
|
ConfigState
|
||||||
|
} from '@/types'
|
||||||
import { DEFAULT_CONFIG_STATE, readStoredConfig } from '@/utils/config-storage'
|
import { DEFAULT_CONFIG_STATE, readStoredConfig } from '@/utils/config-storage'
|
||||||
import { getErrorMessage } from '@/utils/common'
|
import { getErrorMessage } from '@/utils/common'
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +21,122 @@ import {
|
|||||||
|
|
||||||
type ConfigFlagKey = 'enableChunk' | 's3_proxy' | 'openUpload'
|
type ConfigFlagKey = 'enableChunk' | 's3_proxy' | 'openUpload'
|
||||||
|
|
||||||
|
type NormalizedConfigDiagnostics = {
|
||||||
|
items: ConfigDiagnosticItem[]
|
||||||
|
diagnosticItems: ConfigDiagnosticItem[]
|
||||||
|
diagnostic_items: ConfigDiagnosticItem[]
|
||||||
|
summary: ConfigDiagnosticSummary
|
||||||
|
diagnosticSummary: ConfigDiagnosticSummary
|
||||||
|
diagnostic_summary: ConfigDiagnosticSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigDiagnosticsSource =
|
||||||
|
| (Partial<ConfigState> &
|
||||||
|
ConfigDiagnosticsResponse & {
|
||||||
|
diagnostics?: ConfigDiagnosticsResponse
|
||||||
|
})
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
const diagnosticSeverities: ConfigDiagnosticSeverity[] = ['danger', 'warning', 'success', 'neutral']
|
||||||
|
|
||||||
|
const emptyDiagnosticSummary = (): ConfigDiagnosticSummary => ({
|
||||||
|
total: 0,
|
||||||
|
dangerCount: 0,
|
||||||
|
warningCount: 0,
|
||||||
|
successCount: 0,
|
||||||
|
neutralCount: 0,
|
||||||
|
strongestSeverity: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyConfigDiagnostics = (): NormalizedConfigDiagnostics => {
|
||||||
|
const items: ConfigDiagnosticItem[] = []
|
||||||
|
const summary = emptyDiagnosticSummary()
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
diagnosticItems: items,
|
||||||
|
diagnostic_items: items,
|
||||||
|
summary,
|
||||||
|
diagnosticSummary: summary,
|
||||||
|
diagnostic_summary: summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeDiagnosticSeverity = (severity: string | undefined): ConfigDiagnosticSeverity =>
|
||||||
|
diagnosticSeverities.includes(severity as ConfigDiagnosticSeverity)
|
||||||
|
? (severity as ConfigDiagnosticSeverity)
|
||||||
|
: 'neutral'
|
||||||
|
|
||||||
|
const normalizeConfigDiagnostics = (
|
||||||
|
value: ConfigDiagnosticsSource
|
||||||
|
): NormalizedConfigDiagnostics => {
|
||||||
|
const diagnostics = value?.diagnostics
|
||||||
|
const items = (
|
||||||
|
diagnostics?.items ??
|
||||||
|
diagnostics?.diagnosticItems ??
|
||||||
|
diagnostics?.diagnostic_items ??
|
||||||
|
value?.items ??
|
||||||
|
value?.diagnosticItems ??
|
||||||
|
value?.diagnostic_items ??
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
.filter((item) => item && item.key)
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
count: Number(item.count || 0),
|
||||||
|
priority: Number(item.priority || 0),
|
||||||
|
severity: normalizeDiagnosticSeverity(item.severity),
|
||||||
|
fields: item.fields || (item.field ? [String(item.field)] : [])
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rawSummary =
|
||||||
|
diagnostics?.summary ??
|
||||||
|
diagnostics?.diagnosticSummary ??
|
||||||
|
diagnostics?.diagnostic_summary ??
|
||||||
|
value?.summary ??
|
||||||
|
value?.diagnosticSummary ??
|
||||||
|
value?.diagnostic_summary ??
|
||||||
|
{}
|
||||||
|
const summary: ConfigDiagnosticSummary = {
|
||||||
|
total: Number(rawSummary.total ?? items.length),
|
||||||
|
dangerCount: Number(rawSummary.dangerCount ?? rawSummary.danger_count ?? 0),
|
||||||
|
danger_count: Number(rawSummary.dangerCount ?? rawSummary.danger_count ?? 0),
|
||||||
|
warningCount: Number(rawSummary.warningCount ?? rawSummary.warning_count ?? 0),
|
||||||
|
warning_count: Number(rawSummary.warningCount ?? rawSummary.warning_count ?? 0),
|
||||||
|
successCount: Number(rawSummary.successCount ?? rawSummary.success_count ?? 0),
|
||||||
|
success_count: Number(rawSummary.successCount ?? rawSummary.success_count ?? 0),
|
||||||
|
neutralCount: Number(rawSummary.neutralCount ?? rawSummary.neutral_count ?? 0),
|
||||||
|
neutral_count: Number(rawSummary.neutralCount ?? rawSummary.neutral_count ?? 0),
|
||||||
|
strongestSeverity: normalizeDiagnosticSeverity(
|
||||||
|
rawSummary.strongestSeverity ?? rawSummary.strongest_severity
|
||||||
|
),
|
||||||
|
strongest_severity: normalizeDiagnosticSeverity(
|
||||||
|
rawSummary.strongestSeverity ?? rawSummary.strongest_severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
diagnosticItems: items,
|
||||||
|
diagnostic_items: items,
|
||||||
|
summary,
|
||||||
|
diagnosticSummary: summary,
|
||||||
|
diagnostic_summary: summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripDiagnosticFields = (nextConfig: Partial<ConfigState>): Partial<ConfigState> => {
|
||||||
|
const {
|
||||||
|
diagnostics: _diagnostics,
|
||||||
|
diagnosticItems: _diagnosticItems,
|
||||||
|
diagnostic_items: _diagnosticItemsSnake,
|
||||||
|
diagnosticSummary: _diagnosticSummary,
|
||||||
|
diagnostic_summary: _diagnosticSummarySnake,
|
||||||
|
...editableConfig
|
||||||
|
} = nextConfig
|
||||||
|
return editableConfig
|
||||||
|
}
|
||||||
|
|
||||||
export function useSystemConfig() {
|
export function useSystemConfig() {
|
||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
@@ -23,7 +145,9 @@ export function useSystemConfig() {
|
|||||||
const config = ref<ConfigState>({ ...DEFAULT_CONFIG_STATE })
|
const config = ref<ConfigState>({ ...DEFAULT_CONFIG_STATE })
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
const isDiagnosticsRefreshing = ref(false)
|
||||||
const savedPayloadSnapshot = ref('')
|
const savedPayloadSnapshot = ref('')
|
||||||
|
const configDiagnostics = ref<NormalizedConfigDiagnostics>(emptyConfigDiagnostics())
|
||||||
const fileSize = ref(1)
|
const fileSize = ref(1)
|
||||||
const sizeUnit = ref<FileSizeUnit>('MB')
|
const sizeUnit = ref<FileSizeUnit>('MB')
|
||||||
const saveTime = ref(1)
|
const saveTime = ref(1)
|
||||||
@@ -47,11 +171,14 @@ export function useSystemConfig() {
|
|||||||
|
|
||||||
const snapshotPayload = (payload: Partial<ConfigState>) => JSON.stringify(payload)
|
const snapshotPayload = (payload: Partial<ConfigState>) => JSON.stringify(payload)
|
||||||
|
|
||||||
const normalizeEditableConfig = (nextConfig: Partial<ConfigState>): ConfigState => ({
|
const normalizeEditableConfig = (nextConfig: Partial<ConfigState>): ConfigState => {
|
||||||
|
const editableConfig = stripDiagnosticFields(nextConfig)
|
||||||
|
return {
|
||||||
...DEFAULT_CONFIG_STATE,
|
...DEFAULT_CONFIG_STATE,
|
||||||
...nextConfig,
|
...editableConfig,
|
||||||
admin_token: ''
|
admin_token: ''
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const markConfigSaved = () => {
|
const markConfigSaved = () => {
|
||||||
savedPayloadSnapshot.value = snapshotPayload(buildSubmitPayload())
|
savedPayloadSnapshot.value = snapshotPayload(buildSubmitPayload())
|
||||||
@@ -76,6 +203,24 @@ export function useSystemConfig() {
|
|||||||
configStore.updateConfig(configData)
|
configStore.updateConfig(configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshDiagnostics = async (showError = true) => {
|
||||||
|
try {
|
||||||
|
isDiagnosticsRefreshing.value = true
|
||||||
|
const response = await ConfigService.getDiagnostics()
|
||||||
|
if (response.code === 200 && response.detail) {
|
||||||
|
configDiagnostics.value = normalizeConfigDiagnostics(response.detail)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || '获取配置诊断失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (showError) {
|
||||||
|
alertStore.showAlert(getErrorMessage(error, '获取配置诊断失败'), 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isDiagnosticsRefreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取系统配置
|
// 获取系统配置
|
||||||
const fetchConfig = async (): Promise<ConfigState | null> => {
|
const fetchConfig = async (): Promise<ConfigState | null> => {
|
||||||
try {
|
try {
|
||||||
@@ -84,6 +229,7 @@ export function useSystemConfig() {
|
|||||||
const response = await ConfigService.getConfig()
|
const response = await ConfigService.getConfig()
|
||||||
|
|
||||||
if (response.code === 200 && response.detail) {
|
if (response.code === 200 && response.detail) {
|
||||||
|
configDiagnostics.value = normalizeConfigDiagnostics(response.detail)
|
||||||
config.value = normalizeEditableConfig(response.detail)
|
config.value = normalizeEditableConfig(response.detail)
|
||||||
const notifyMessage = configStore.applyRemoteConfig(config.value)
|
const notifyMessage = configStore.applyRemoteConfig(config.value)
|
||||||
if (notifyMessage) {
|
if (notifyMessage) {
|
||||||
@@ -99,6 +245,7 @@ export function useSystemConfig() {
|
|||||||
const storedConfig = getStoredConfig()
|
const storedConfig = getStoredConfig()
|
||||||
if (storedConfig) {
|
if (storedConfig) {
|
||||||
config.value = storedConfig
|
config.value = storedConfig
|
||||||
|
configDiagnostics.value = emptyConfigDiagnostics()
|
||||||
return config.value
|
return config.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +268,7 @@ export function useSystemConfig() {
|
|||||||
syncConfigForm(config.value)
|
syncConfigForm(config.value)
|
||||||
markConfigSaved()
|
markConfigSaved()
|
||||||
saveConfigToStorage(config.value)
|
saveConfigToStorage(config.value)
|
||||||
|
await refreshDiagnostics(false)
|
||||||
alertStore.showAlert('配置更新成功!', 'success')
|
alertStore.showAlert('配置更新成功!', 'success')
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@@ -183,6 +331,9 @@ export function useSystemConfig() {
|
|||||||
return Math.round(config.value.uploadSize / 1024 / 1024)
|
return Math.round(config.value.uploadSize / 1024 / 1024)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const configDiagnosticItems = computed(() => configDiagnostics.value.items)
|
||||||
|
const configDiagnosticSummary = computed(() => configDiagnostics.value.summary)
|
||||||
|
|
||||||
const isConfigLoaded = computed(() => {
|
const isConfigLoaded = computed(() => {
|
||||||
return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value
|
return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value
|
||||||
})
|
})
|
||||||
@@ -193,7 +344,10 @@ export function useSystemConfig() {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isDiagnosticsRefreshing,
|
||||||
isDirty,
|
isDirty,
|
||||||
|
configDiagnosticItems,
|
||||||
|
configDiagnosticSummary,
|
||||||
fileSize,
|
fileSize,
|
||||||
sizeUnit,
|
sizeUnit,
|
||||||
saveTime,
|
saveTime,
|
||||||
@@ -207,6 +361,7 @@ export function useSystemConfig() {
|
|||||||
fetchConfig,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
refreshConfig,
|
refreshConfig,
|
||||||
|
refreshDiagnostics,
|
||||||
submitConfig,
|
submitConfig,
|
||||||
toggleConfigFlag,
|
toggleConfigFlag,
|
||||||
initConfig,
|
initConfig,
|
||||||
|
|||||||
@@ -822,7 +822,66 @@ export default {
|
|||||||
saving: 'Saving',
|
saving: 'Saving',
|
||||||
unsavedChanges: 'Unsaved configuration changes',
|
unsavedChanges: 'Unsaved configuration changes',
|
||||||
allChangesSaved: 'All configuration changes are saved',
|
allChangesSaved: 'All configuration changes are saved',
|
||||||
refreshBlocked: 'Save current changes before refreshing'
|
refreshBlocked: 'Save current changes before refreshing',
|
||||||
|
diagnosticsTitle: 'Configuration Checkup',
|
||||||
|
diagnosticsDesc:
|
||||||
|
'Automatically checks security, storage, upload, and retention policies. Click a suggestion to jump to the related field.',
|
||||||
|
diagnosticsRefresh: 'Refresh checkup',
|
||||||
|
diagnosticsTotal: 'Suggestions',
|
||||||
|
diagnosticsDanger: 'High risk',
|
||||||
|
diagnosticsWarning: 'Action needed',
|
||||||
|
diagnosticsFocusAction: 'Locate setting',
|
||||||
|
diagnosticSeverity: {
|
||||||
|
danger: 'High risk',
|
||||||
|
warning: 'Action needed',
|
||||||
|
success: 'Stable',
|
||||||
|
neutral: 'Suggestion'
|
||||||
|
},
|
||||||
|
diagnosticsItems: {
|
||||||
|
default_admin_password: {
|
||||||
|
title: 'Default admin password is still in use',
|
||||||
|
description:
|
||||||
|
'The default admin password is a security risk. Set a strong password as soon as possible.'
|
||||||
|
},
|
||||||
|
s3_incomplete: {
|
||||||
|
title: 'S3 configuration is incomplete',
|
||||||
|
description: 'S3 is missing {count} fields. Complete them before enabling object storage.'
|
||||||
|
},
|
||||||
|
webdav_incomplete: {
|
||||||
|
title: 'WebDAV configuration is incomplete',
|
||||||
|
description:
|
||||||
|
'WebDAV is missing {count} fields. Complete them before enabling remote storage.'
|
||||||
|
},
|
||||||
|
guest_upload_retention: {
|
||||||
|
title: 'Guest uploads have no retention limit',
|
||||||
|
description:
|
||||||
|
'Guest upload is enabled with unlimited retention. Set a default retention window to reduce storage pressure.'
|
||||||
|
},
|
||||||
|
chunking_recommended: {
|
||||||
|
title: 'Chunked upload is recommended',
|
||||||
|
description:
|
||||||
|
'Enable chunked upload for large files to improve upload stability and recovery after failures.'
|
||||||
|
},
|
||||||
|
upload_guard_disabled: {
|
||||||
|
title: 'Upload rate limiting is disabled',
|
||||||
|
description:
|
||||||
|
'Upload count limits are not active. Set an upload window and maximum upload count.'
|
||||||
|
},
|
||||||
|
access_guard_disabled: {
|
||||||
|
title: 'Access protection is disabled',
|
||||||
|
description:
|
||||||
|
'Error access protection is not active. Set a detection window and maximum error count.'
|
||||||
|
},
|
||||||
|
expiration_style_empty: {
|
||||||
|
title: 'Expiration method is not configured',
|
||||||
|
description:
|
||||||
|
'Choose a default expiration method so uploaded files are retained or expired as expected.'
|
||||||
|
},
|
||||||
|
healthy: {
|
||||||
|
title: 'Configuration looks stable',
|
||||||
|
description: 'No configuration risks need priority attention right now.'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
systemSettings: {
|
systemSettings: {
|
||||||
title: 'System Settings',
|
title: 'System Settings',
|
||||||
|
|||||||
@@ -842,7 +842,58 @@ export default {
|
|||||||
saving: '保存中',
|
saving: '保存中',
|
||||||
unsavedChanges: '有未保存的配置变更',
|
unsavedChanges: '有未保存的配置变更',
|
||||||
allChangesSaved: '所有配置已保存',
|
allChangesSaved: '所有配置已保存',
|
||||||
refreshBlocked: '请先保存当前变更再刷新'
|
refreshBlocked: '请先保存当前变更再刷新',
|
||||||
|
diagnosticsTitle: '配置体检',
|
||||||
|
diagnosticsDesc: '自动检查安全、存储、上传和保留策略,点击建议可定位到相关配置。',
|
||||||
|
diagnosticsRefresh: '刷新体检',
|
||||||
|
diagnosticsTotal: '建议项',
|
||||||
|
diagnosticsDanger: '高风险',
|
||||||
|
diagnosticsWarning: '需处理',
|
||||||
|
diagnosticsFocusAction: '定位配置',
|
||||||
|
diagnosticSeverity: {
|
||||||
|
danger: '高风险',
|
||||||
|
warning: '需处理',
|
||||||
|
success: '稳定',
|
||||||
|
neutral: '建议'
|
||||||
|
},
|
||||||
|
diagnosticsItems: {
|
||||||
|
default_admin_password: {
|
||||||
|
title: '管理员密码仍为默认值',
|
||||||
|
description: '默认管理员密码存在安全风险,建议立即设置一个强密码。'
|
||||||
|
},
|
||||||
|
s3_incomplete: {
|
||||||
|
title: 'S3 配置不完整',
|
||||||
|
description: '当前 S3 存储缺少 {count} 项必要配置,补齐后再启用对象存储。'
|
||||||
|
},
|
||||||
|
webdav_incomplete: {
|
||||||
|
title: 'WebDAV 配置不完整',
|
||||||
|
description: '当前 WebDAV 存储缺少 {count} 项必要配置,补齐后再启用远程存储。'
|
||||||
|
},
|
||||||
|
guest_upload_retention: {
|
||||||
|
title: '游客上传缺少保留上限',
|
||||||
|
description: '游客上传已开启且保存时间不受限,建议设置默认保留期降低存储压力。'
|
||||||
|
},
|
||||||
|
chunking_recommended: {
|
||||||
|
title: '建议开启分片上传',
|
||||||
|
description: '大文件上传场景建议启用分片上传,提升上传稳定性和失败恢复能力。'
|
||||||
|
},
|
||||||
|
upload_guard_disabled: {
|
||||||
|
title: '上传限流未启用',
|
||||||
|
description: '当前上传次数限制未生效,建议设置上传窗口和次数上限。'
|
||||||
|
},
|
||||||
|
access_guard_disabled: {
|
||||||
|
title: '访问保护未启用',
|
||||||
|
description: '当前错误访问保护未生效,建议设置检测窗口和错误次数上限。'
|
||||||
|
},
|
||||||
|
expiration_style_empty: {
|
||||||
|
title: '未配置过期方式',
|
||||||
|
description: '建议选择默认过期方式,确保上传文件按预期保留或失效。'
|
||||||
|
},
|
||||||
|
healthy: {
|
||||||
|
title: '配置状态稳定',
|
||||||
|
description: '当前未发现需要优先处理的配置风险。'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: '仪表盘',
|
title: '仪表盘',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import api from './client'
|
import api from './client'
|
||||||
import type { ApiResponse, ConfigState } from '@/types'
|
import type { ApiResponse, ConfigDiagnosticsResponse, ConfigState } from '@/types'
|
||||||
|
|
||||||
type PublicConfigEnvelope = {
|
type PublicConfigEnvelope = {
|
||||||
config?: Partial<ConfigState>
|
config?: Partial<ConfigState>
|
||||||
@@ -17,6 +17,10 @@ export class ConfigService {
|
|||||||
return api.get('/admin/config/get')
|
return api.get('/admin/config/get')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getDiagnostics(): Promise<ApiResponse<ConfigDiagnosticsResponse>> {
|
||||||
|
return api.get('/admin/config/diagnostics')
|
||||||
|
}
|
||||||
|
|
||||||
static async getUserConfig(): Promise<ApiResponse<ConfigState>> {
|
static async getUserConfig(): Promise<ApiResponse<ConfigState>> {
|
||||||
try {
|
try {
|
||||||
const response = (await api.get('/api/v1/config')) as ApiResponse<
|
const response = (await api.get('/api/v1/config')) as ApiResponse<
|
||||||
|
|||||||
@@ -15,6 +15,53 @@ export interface ThemeChoice {
|
|||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConfigDiagnosticSeverity = 'danger' | 'warning' | 'success' | 'neutral'
|
||||||
|
|
||||||
|
export interface ConfigDiagnosticAction {
|
||||||
|
type?: string
|
||||||
|
field?: keyof ConfigState | string | null
|
||||||
|
fields?: string[]
|
||||||
|
category?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigDiagnosticItem {
|
||||||
|
key: string
|
||||||
|
severity: ConfigDiagnosticSeverity | string
|
||||||
|
category: string
|
||||||
|
priority?: number
|
||||||
|
count?: number
|
||||||
|
field?: keyof ConfigState | string | null
|
||||||
|
fields?: string[]
|
||||||
|
action?: ConfigDiagnosticAction
|
||||||
|
actionType?: string
|
||||||
|
action_type?: string
|
||||||
|
targetField?: keyof ConfigState | string | null
|
||||||
|
target_field?: keyof ConfigState | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigDiagnosticSummary {
|
||||||
|
total: number
|
||||||
|
dangerCount: number
|
||||||
|
danger_count?: number
|
||||||
|
warningCount: number
|
||||||
|
warning_count?: number
|
||||||
|
successCount: number
|
||||||
|
success_count?: number
|
||||||
|
neutralCount: number
|
||||||
|
neutral_count?: number
|
||||||
|
strongestSeverity: ConfigDiagnosticSeverity
|
||||||
|
strongest_severity?: ConfigDiagnosticSeverity
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigDiagnosticsResponse {
|
||||||
|
items?: ConfigDiagnosticItem[]
|
||||||
|
diagnosticItems?: ConfigDiagnosticItem[]
|
||||||
|
diagnostic_items?: ConfigDiagnosticItem[]
|
||||||
|
summary?: Partial<ConfigDiagnosticSummary>
|
||||||
|
diagnosticSummary?: Partial<ConfigDiagnosticSummary>
|
||||||
|
diagnostic_summary?: Partial<ConfigDiagnosticSummary>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigState {
|
export interface ConfigState {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -52,4 +99,9 @@ export interface ConfigState {
|
|||||||
webdav_url: string
|
webdav_url: string
|
||||||
webdav_username: string
|
webdav_username: string
|
||||||
webdav_password: string
|
webdav_password: string
|
||||||
|
diagnostics?: ConfigDiagnosticsResponse
|
||||||
|
diagnosticItems?: ConfigDiagnosticItem[]
|
||||||
|
diagnostic_items?: ConfigDiagnosticItem[]
|
||||||
|
diagnosticSummary?: Partial<ConfigDiagnosticSummary>
|
||||||
|
diagnostic_summary?: Partial<ConfigDiagnosticSummary>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, onMounted } from 'vue'
|
import { inject, nextTick, onMounted, unref, type Component } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { RefreshCwIcon, SaveIcon } from 'lucide-vue-next'
|
import {
|
||||||
|
AlertTriangleIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
InfoIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
SaveIcon,
|
||||||
|
ShieldAlertIcon
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import BaseButton from '@/components/common/BaseButton.vue'
|
import BaseButton from '@/components/common/BaseButton.vue'
|
||||||
import SettingNumberInput from '@/components/common/SettingNumberInput.vue'
|
import SettingNumberInput from '@/components/common/SettingNumberInput.vue'
|
||||||
import SettingSwitch from '@/components/common/SettingSwitch.vue'
|
import SettingSwitch from '@/components/common/SettingSwitch.vue'
|
||||||
import { useSystemConfig } from '@/composables'
|
import { useSystemConfig } from '@/composables'
|
||||||
|
import type { ConfigDiagnosticItem, ConfigDiagnosticSeverity } from '@/types'
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
const isDarkMode = inject('isDarkMode')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -13,16 +22,134 @@ const {
|
|||||||
config,
|
config,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isDiagnosticsRefreshing,
|
||||||
isDirty,
|
isDirty,
|
||||||
|
configDiagnosticItems,
|
||||||
|
configDiagnosticSummary,
|
||||||
fileSize,
|
fileSize,
|
||||||
sizeUnit,
|
sizeUnit,
|
||||||
saveTime,
|
saveTime,
|
||||||
saveTimeUnit,
|
saveTimeUnit,
|
||||||
refreshConfig,
|
refreshConfig,
|
||||||
|
refreshDiagnostics,
|
||||||
submitConfig,
|
submitConfig,
|
||||||
toggleConfigFlag
|
toggleConfigFlag
|
||||||
} = useSystemConfig()
|
} = useSystemConfig()
|
||||||
|
|
||||||
|
const diagnosticSeverities: ConfigDiagnosticSeverity[] = ['danger', 'warning', 'success', 'neutral']
|
||||||
|
|
||||||
|
const normalizeDiagnosticSeverity = (severity: string): ConfigDiagnosticSeverity =>
|
||||||
|
diagnosticSeverities.includes(severity as ConfigDiagnosticSeverity)
|
||||||
|
? (severity as ConfigDiagnosticSeverity)
|
||||||
|
: 'neutral'
|
||||||
|
|
||||||
|
const isDark = () => Boolean(unref(isDarkMode))
|
||||||
|
|
||||||
|
const getDiagnosticTitle = (item: ConfigDiagnosticItem) =>
|
||||||
|
t(`manage.settings.diagnosticsItems.${item.key}.title`, {
|
||||||
|
count: item.count ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDiagnosticDescription = (item: ConfigDiagnosticItem) =>
|
||||||
|
t(`manage.settings.diagnosticsItems.${item.key}.description`, {
|
||||||
|
count: item.count ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDiagnosticSeverityLabel = (severity: string) =>
|
||||||
|
t(`manage.settings.diagnosticSeverity.${normalizeDiagnosticSeverity(severity)}`)
|
||||||
|
|
||||||
|
const getDiagnosticIcon = (severity: string): Component => {
|
||||||
|
const normalizedSeverity = normalizeDiagnosticSeverity(severity)
|
||||||
|
if (normalizedSeverity === 'danger') {
|
||||||
|
return ShieldAlertIcon
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'warning') {
|
||||||
|
return AlertTriangleIcon
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'success') {
|
||||||
|
return CheckCircleIcon
|
||||||
|
}
|
||||||
|
return InfoIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDiagnosticClass = (severity: string) => {
|
||||||
|
const normalizedSeverity = normalizeDiagnosticSeverity(severity)
|
||||||
|
const darkMode = isDark()
|
||||||
|
|
||||||
|
if (normalizedSeverity === 'danger') {
|
||||||
|
return darkMode
|
||||||
|
? 'border-red-500/40 bg-red-500/10 hover:border-red-400'
|
||||||
|
: 'border-red-200 bg-red-50 hover:border-red-300'
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'warning') {
|
||||||
|
return darkMode
|
||||||
|
? 'border-amber-500/40 bg-amber-500/10 hover:border-amber-400'
|
||||||
|
: 'border-amber-200 bg-amber-50 hover:border-amber-300'
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'success') {
|
||||||
|
return darkMode
|
||||||
|
? 'border-emerald-500/40 bg-emerald-500/10 hover:border-emerald-400'
|
||||||
|
: 'border-emerald-200 bg-emerald-50 hover:border-emerald-300'
|
||||||
|
}
|
||||||
|
return darkMode
|
||||||
|
? 'border-gray-600 bg-gray-700/50 hover:border-gray-500'
|
||||||
|
: 'border-gray-200 bg-gray-50 hover:border-gray-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDiagnosticIconClass = (severity: string) => {
|
||||||
|
const normalizedSeverity = normalizeDiagnosticSeverity(severity)
|
||||||
|
if (normalizedSeverity === 'danger') {
|
||||||
|
return 'bg-red-100 text-red-600'
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'warning') {
|
||||||
|
return 'bg-amber-100 text-amber-600'
|
||||||
|
}
|
||||||
|
if (normalizedSeverity === 'success') {
|
||||||
|
return 'bg-emerald-100 text-emerald-600'
|
||||||
|
}
|
||||||
|
return 'bg-gray-100 text-gray-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDiagnosticTargetField = (item: ConfigDiagnosticItem) => {
|
||||||
|
const field =
|
||||||
|
item.targetField ??
|
||||||
|
item.target_field ??
|
||||||
|
item.field ??
|
||||||
|
item.fields?.[0] ??
|
||||||
|
item.action?.field ??
|
||||||
|
item.action?.fields?.[0]
|
||||||
|
return field ? String(field) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const findConfigFieldElement = (field: string) =>
|
||||||
|
document.querySelector<HTMLElement>(`[data-config-field="${field}"]`)
|
||||||
|
|
||||||
|
const focusDiagnosticField = async (item: ConfigDiagnosticItem) => {
|
||||||
|
const targetField = getDiagnosticTargetField(item)
|
||||||
|
if (!targetField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const fieldElement =
|
||||||
|
findConfigFieldElement(targetField) ??
|
||||||
|
(targetField === 'enableChunk' ? findConfigFieldElement('file_storage') : null)
|
||||||
|
|
||||||
|
if (!fieldElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
const focusable = fieldElement.matches('input, select, textarea, button')
|
||||||
|
? fieldElement
|
||||||
|
: fieldElement.querySelector<HTMLElement>('input, select, textarea, button')
|
||||||
|
focusable?.focus({ preventScroll: true })
|
||||||
|
}, 260)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void refreshConfig()
|
void refreshConfig()
|
||||||
})
|
})
|
||||||
@@ -81,6 +208,143 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="mb-6 space-y-4 border-b pb-6"
|
||||||
|
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div class="max-w-3xl">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-md"
|
||||||
|
:class="[
|
||||||
|
isDarkMode ? 'bg-indigo-500/15 text-indigo-300' : 'bg-indigo-50 text-indigo-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<ShieldAlertIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
||||||
|
>
|
||||||
|
{{ t('manage.settings.diagnosticsTitle') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
|
||||||
|
{{ t('manage.settings.diagnosticsDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="min-w-24 rounded-md border px-3 py-2"
|
||||||
|
:class="[isDarkMode ? 'border-gray-700 bg-gray-800/70' : 'border-gray-200 bg-white']"
|
||||||
|
>
|
||||||
|
<p class="text-xs" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||||
|
{{ t('manage.settings.diagnosticsTotal') }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-1 text-xl font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
||||||
|
>
|
||||||
|
{{ configDiagnosticSummary.total }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="min-w-24 rounded-md border px-3 py-2"
|
||||||
|
:class="[isDarkMode ? 'border-red-500/30 bg-red-500/10' : 'border-red-200 bg-red-50']"
|
||||||
|
>
|
||||||
|
<p class="text-xs" :class="[isDarkMode ? 'text-red-200' : 'text-red-600']">
|
||||||
|
{{ t('manage.settings.diagnosticsDanger') }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-1 text-xl font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-red-100' : 'text-red-700']"
|
||||||
|
>
|
||||||
|
{{ configDiagnosticSummary.dangerCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="min-w-24 rounded-md border px-3 py-2"
|
||||||
|
:class="[
|
||||||
|
isDarkMode ? 'border-amber-500/30 bg-amber-500/10' : 'border-amber-200 bg-amber-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<p class="text-xs" :class="[isDarkMode ? 'text-amber-200' : 'text-amber-600']">
|
||||||
|
{{ t('manage.settings.diagnosticsWarning') }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-1 text-xl font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-amber-100' : 'text-amber-700']"
|
||||||
|
>
|
||||||
|
{{ configDiagnosticSummary.warningCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseButton
|
||||||
|
variant="outline"
|
||||||
|
:loading="isDiagnosticsRefreshing"
|
||||||
|
:disabled="isRefreshing || isSaving"
|
||||||
|
@click="() => void refreshDiagnostics()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<RefreshCwIcon class="mr-2 h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ t('manage.settings.diagnosticsRefresh') }}
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||||
|
<button
|
||||||
|
v-for="item in configDiagnosticItems"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="group flex h-full items-start gap-3 rounded-md border p-4 text-left transition"
|
||||||
|
:class="getDiagnosticClass(item.severity)"
|
||||||
|
@click="focusDiagnosticField(item)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mt-0.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md"
|
||||||
|
:class="getDiagnosticIconClass(item.severity)"
|
||||||
|
>
|
||||||
|
<component :is="getDiagnosticIcon(item.severity)" class="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-semibold"
|
||||||
|
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
||||||
|
>
|
||||||
|
{{ getDiagnosticTitle(item) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-900/70 text-gray-300' : 'bg-white/80 text-gray-600']"
|
||||||
|
>
|
||||||
|
{{ getDiagnosticSeverityLabel(item.severity) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mt-1 block text-sm leading-6"
|
||||||
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']"
|
||||||
|
>
|
||||||
|
{{ getDiagnosticDescription(item) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mt-1 inline-flex items-center gap-1 text-xs font-medium"
|
||||||
|
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-600']"
|
||||||
|
>
|
||||||
|
{{ t('manage.settings.diagnosticsFocusAction') }}
|
||||||
|
<ArrowRightIcon class="h-3.5 w-3.5 transition group-hover:translate-x-0.5" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="space-y-6 rounded-lg shadow-md p-6"
|
class="space-y-6 rounded-lg shadow-md p-6"
|
||||||
:class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']"
|
:class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']"
|
||||||
@@ -143,6 +407,7 @@ onMounted(() => {
|
|||||||
type="password"
|
type="password"
|
||||||
minlength="6"
|
minlength="6"
|
||||||
v-model="config.admin_token"
|
v-model="config.admin_token"
|
||||||
|
data-config-field="admin_token"
|
||||||
:placeholder="t('admin.settings.passwordPlaceholder')"
|
:placeholder="t('admin.settings.passwordPlaceholder')"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -304,6 +569,7 @@ onMounted(() => {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
v-model="config.file_storage"
|
v-model="config.file_storage"
|
||||||
|
data-config-field="file_storage"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border appearance-none bg-no-repeat bg-right focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none cursor-pointer"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border appearance-none bg-no-repeat bg-right focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none cursor-pointer"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -321,6 +587,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
v-if="config.file_storage === 'local'"
|
v-if="config.file_storage === 'local'"
|
||||||
|
data-config-field="enableChunk"
|
||||||
:label="t('manage.settings.chunkUploadNote')"
|
:label="t('manage.settings.chunkUploadNote')"
|
||||||
:model-value="config.enableChunk"
|
:model-value="config.enableChunk"
|
||||||
:enabled-text="t('common.enabled')"
|
:enabled-text="t('common.enabled')"
|
||||||
@@ -340,6 +607,7 @@ onMounted(() => {
|
|||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('manage.settings.webdavUrlPlaceholder')"
|
:placeholder="t('manage.settings.webdavUrlPlaceholder')"
|
||||||
v-model="config.webdav_url"
|
v-model="config.webdav_url"
|
||||||
|
data-config-field="webdav_url"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -360,6 +628,7 @@ onMounted(() => {
|
|||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('manage.settings.webdavUsernamePlaceholder')"
|
:placeholder="t('manage.settings.webdavUsernamePlaceholder')"
|
||||||
v-model="config.webdav_username"
|
v-model="config.webdav_username"
|
||||||
|
data-config-field="webdav_username"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -380,6 +649,7 @@ onMounted(() => {
|
|||||||
type="password"
|
type="password"
|
||||||
:placeholder="t('manage.settings.webdavPasswordPlaceholder')"
|
:placeholder="t('manage.settings.webdavPasswordPlaceholder')"
|
||||||
v-model="config.webdav_password"
|
v-model="config.webdav_password"
|
||||||
|
data-config-field="webdav_password"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -403,6 +673,7 @@ onMounted(() => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="config.s3_access_key_id"
|
v-model="config.s3_access_key_id"
|
||||||
|
data-config-field="s3_access_key_id"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -422,6 +693,7 @@ onMounted(() => {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
v-model="config.s3_secret_access_key"
|
v-model="config.s3_secret_access_key"
|
||||||
|
data-config-field="s3_secret_access_key"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -441,6 +713,7 @@ onMounted(() => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="config.s3_bucket_name"
|
v-model="config.s3_bucket_name"
|
||||||
|
data-config-field="s3_bucket_name"
|
||||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -538,6 +811,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
|
data-config-field="enableChunk"
|
||||||
:label="t('manage.settings.chunkUploadNote')"
|
:label="t('manage.settings.chunkUploadNote')"
|
||||||
:model-value="config.enableChunk"
|
:model-value="config.enableChunk"
|
||||||
:enabled-text="t('common.enabled')"
|
:enabled-text="t('common.enabled')"
|
||||||
@@ -561,12 +835,14 @@ onMounted(() => {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<SettingNumberInput
|
<SettingNumberInput
|
||||||
v-model="config.uploadMinute"
|
v-model="config.uploadMinute"
|
||||||
|
data-config-field="uploadMinute"
|
||||||
:label="t('manage.settings.uploadPerMinute')"
|
:label="t('manage.settings.uploadPerMinute')"
|
||||||
:suffix="t('common.minute')"
|
:suffix="t('common.minute')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingNumberInput
|
<SettingNumberInput
|
||||||
v-model="config.uploadCount"
|
v-model="config.uploadCount"
|
||||||
|
data-config-field="uploadCount"
|
||||||
:label="t('manage.settings.uploadCountLimit')"
|
:label="t('manage.settings.uploadCountLimit')"
|
||||||
:suffix="t('common.files')"
|
:suffix="t('common.files')"
|
||||||
/>
|
/>
|
||||||
@@ -605,7 +881,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2" data-config-field="expireStyle">
|
||||||
<label
|
<label
|
||||||
class="block text-sm font-medium mb-2"
|
class="block text-sm font-medium mb-2"
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"
|
||||||
@@ -642,7 +918,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2" data-config-field="max_save_seconds">
|
||||||
<label
|
<label
|
||||||
class="block text-sm font-medium"
|
class="block text-sm font-medium"
|
||||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']"
|
||||||
@@ -699,12 +975,14 @@ onMounted(() => {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<SettingNumberInput
|
<SettingNumberInput
|
||||||
v-model="config.errorMinute"
|
v-model="config.errorMinute"
|
||||||
|
data-config-field="errorMinute"
|
||||||
:label="t('manage.settings.errorPerMinute')"
|
:label="t('manage.settings.errorPerMinute')"
|
||||||
:suffix="t('common.minute')"
|
:suffix="t('common.minute')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingNumberInput
|
<SettingNumberInput
|
||||||
v-model="config.errorCount"
|
v-model="config.errorCount"
|
||||||
|
data-config-field="errorCount"
|
||||||
:label="t('manage.settings.errorCountLimit')"
|
:label="t('manage.settings.errorCountLimit')"
|
||||||
:suffix="t('common.times')"
|
:suffix="t('common.times')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user