Files
FileCodeBoxFronted/src/composables/useSystemConfig.ts
T
2026-06-03 08:01:55 +08:00

372 lines
11 KiB
TypeScript

import { ref, computed } from 'vue'
import { ConfigService } from '@/services'
import { useAlertStore } from '@/stores/alertStore'
import { useConfigStore } from '@/stores/configStore'
import type {
ConfigDiagnosticItem,
ConfigDiagnosticsResponse,
ConfigDiagnosticSeverity,
ConfigDiagnosticSummary,
ConfigState
} from '@/types'
import { DEFAULT_CONFIG_STATE, readStoredConfig } from '@/utils/config-storage'
import { getErrorMessage } from '@/utils/common'
import {
buildConfigSubmitPayload,
bytesToFileSizeForm,
secondsToSaveTimeForm,
type FileSizeUnit,
type SaveTimeUnit
} from '@/utils/config-form'
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() {
const alertStore = useAlertStore()
const configStore = useConfigStore()
// 状态管理
const config = ref<ConfigState>({ ...DEFAULT_CONFIG_STATE })
const isRefreshing = ref(false)
const isSaving = ref(false)
const isDiagnosticsRefreshing = ref(false)
const savedPayloadSnapshot = ref('')
const configDiagnostics = ref<NormalizedConfigDiagnostics>(emptyConfigDiagnostics())
const fileSize = ref(1)
const sizeUnit = ref<FileSizeUnit>('MB')
const saveTime = ref(1)
const saveTimeUnit = ref<SaveTimeUnit>('天')
const isLoading = computed(() => isRefreshing.value || isSaving.value)
const buildSubmitPayload = (): Partial<ConfigState> => {
const payload: Partial<ConfigState> = buildConfigSubmitPayload(
config.value,
{ value: fileSize.value, unit: sizeUnit.value },
{ value: saveTime.value, unit: saveTimeUnit.value }
)
if (!payload.admin_token) {
delete payload.admin_token
}
return payload
}
const snapshotPayload = (payload: Partial<ConfigState>) => JSON.stringify(payload)
const normalizeEditableConfig = (nextConfig: Partial<ConfigState>): ConfigState => {
const editableConfig = stripDiagnosticFields(nextConfig)
return {
...DEFAULT_CONFIG_STATE,
...editableConfig,
admin_token: ''
}
}
const markConfigSaved = () => {
savedPayloadSnapshot.value = snapshotPayload(buildSubmitPayload())
}
const isDirty = computed(() => {
if (!savedPayloadSnapshot.value) {
return false
}
return snapshotPayload(buildSubmitPayload()) !== savedPayloadSnapshot.value
})
// 从本地存储获取配置
const getStoredConfig = (): ConfigState | null => {
const storedConfig = readStoredConfig<Partial<ConfigState>>()
return storedConfig ? normalizeEditableConfig(storedConfig) : null
}
// 保存配置到本地存储
const saveConfigToStorage = (configData: ConfigState) => {
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> => {
try {
isRefreshing.value = true
const response = await ConfigService.getConfig()
if (response.code === 200 && response.detail) {
configDiagnostics.value = normalizeConfigDiagnostics(response.detail)
config.value = normalizeEditableConfig(response.detail)
const notifyMessage = configStore.applyRemoteConfig(config.value)
if (notifyMessage) {
alertStore.showAlert(notifyMessage, 'success')
}
return config.value
} else {
throw new Error(response.message || '获取配置失败')
}
} catch (error) {
// 如果网络请求失败,尝试使用本地存储的配置
const storedConfig = getStoredConfig()
if (storedConfig) {
config.value = storedConfig
configDiagnostics.value = emptyConfigDiagnostics()
return config.value
}
alertStore.showAlert(getErrorMessage(error, '获取配置失败'), 'error')
return null
} finally {
isRefreshing.value = false
}
}
// 更新系统配置
const updateConfig = async (newConfig: Partial<ConfigState>): Promise<boolean> => {
try {
isSaving.value = true
const response = await ConfigService.updateConfig(newConfig)
if (response.code === 200) {
config.value = normalizeEditableConfig({ ...config.value, ...newConfig })
syncConfigForm(config.value)
markConfigSaved()
saveConfigToStorage(config.value)
await refreshDiagnostics(false)
alertStore.showAlert('配置更新成功!', 'success')
return true
} else {
throw new Error(response.message || '更新配置失败')
}
} catch (error) {
alertStore.showAlert(getErrorMessage(error, '更新配置失败'), 'error')
return false
} finally {
isSaving.value = false
}
}
const toggleConfigFlag = (key: ConfigFlagKey) => {
config.value[key] = config.value[key] === 1 ? 0 : 1
}
const syncConfigForm = (nextConfig: ConfigState) => {
const sizeForm = bytesToFileSizeForm(nextConfig.uploadSize)
fileSize.value = sizeForm.value
sizeUnit.value = sizeForm.unit
const saveTimeForm = secondsToSaveTimeForm(nextConfig.max_save_seconds)
saveTime.value = saveTimeForm.value
saveTimeUnit.value = saveTimeForm.unit
}
const refreshConfig = async () => {
const latestConfig = await fetchConfig()
if (latestConfig) {
syncConfigForm(latestConfig)
markConfigSaved()
}
}
const submitConfig = () => {
if (!isDirty.value || isSaving.value) {
return Promise.resolve(false)
}
return updateConfig(buildSubmitPayload())
}
// 初始化配置
const initConfig = async () => {
// 先尝试从本地存储加载
const storedConfig = getStoredConfig()
if (storedConfig) {
config.value = storedConfig
syncConfigForm(storedConfig)
markConfigSaved()
}
// 然后从服务器获取最新配置
await refreshConfig()
}
// 计算属性
const maxFileSizeMB = computed(() => {
return Math.round(config.value.uploadSize / 1024 / 1024)
})
const configDiagnosticItems = computed(() => configDiagnostics.value.items)
const configDiagnosticSummary = computed(() => configDiagnostics.value.summary)
const isConfigLoaded = computed(() => {
return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value
})
return {
// 状态
config,
isLoading,
isRefreshing,
isSaving,
isDiagnosticsRefreshing,
isDirty,
configDiagnosticItems,
configDiagnosticSummary,
fileSize,
sizeUnit,
saveTime,
saveTimeUnit,
// 计算属性
maxFileSizeMB,
isConfigLoaded,
// 方法
fetchConfig,
updateConfig,
refreshConfig,
refreshDiagnostics,
submitConfig,
toggleConfigFlag,
initConfig,
getStoredConfig,
saveConfigToStorage
}
}