Files
WeKnora/frontend/src/views/settings/StorageEngineSettings.vue

1259 lines
40 KiB
Vue

<template>
<div class="storage-engine-settings">
<div class="section-header">
<h2>{{ $t('settings.storage.title') }}</h2>
<p class="section-description">
{{ $t('settings.storage.description') }}
</p>
</div>
<div v-if="loading" class="loading-state">
<t-loading size="small" />
<span>{{ $t('settings.storage.loading') }}</span>
</div>
<div v-else-if="error" class="error-inline">
<t-alert theme="error" :message="error">
<template #operation>
<t-button size="small" @click="loadAll">{{ $t('settings.storage.retry') }}</t-button>
</template>
</t-alert>
</div>
<template v-else>
<div class="settings-group">
<div class="setting-row">
<div class="setting-info">
<label>{{ $t('settings.storage.defaultEngine') }}</label>
<p class="desc">{{ $t('settings.storage.defaultEngineDesc') }}</p>
</div>
<div class="setting-control">
<t-select
v-model="config.default_provider"
style="width: 280px;"
:placeholder="$t('settings.storage.defaultEngine')"
@change="onSaveDefaultEngine"
>
<t-option value="local" :label="$t('settings.storage.engineLocal')" />
<t-option value="minio" label="MinIO" />
<t-option value="cos" :label="$t('settings.storage.engineCos')" />
<t-option value="tos" :label="$t('settings.storage.engineTos')" />
<t-option value="s3" label="AWS S3" />
<t-option value="oss" :label="$t('settings.storage.engineOss')" />
</t-select>
<span v-if="saveMessage && !drawerVisible" :class="['save-msg', saveSuccess ? 'success' : 'error']" style="margin-left: 12px;">
{{ saveMessage }}
</span>
</div>
</div>
</div>
<div class="engine-cards">
<!-- Local -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'local' }]" @click="openDrawer('local')">
<div class="engine-card-header">
<h3>{{ $t('settings.storage.localTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.available') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.localDesc') }}</p>
</div>
<!-- MinIO -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'minio' }]" @click="openDrawer('minio')">
<div class="engine-card-header">
<h3>MinIO</h3>
<t-tag v-if="minioAvailable" theme="success" variant="light" size="small">{{ $t('settings.storage.available') }}</t-tag>
<t-tag v-else theme="default" variant="light" size="small">{{ $t('settings.storage.needsConfig') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.minioDesc') }}</p>
</div>
<!-- COS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'cos' }]" @click="openDrawer('cos')">
<div class="engine-card-header">
<h3>{{ $t('settings.storage.cosTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.cosDesc') }}</p>
</div>
<!-- TOS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'tos' }]" @click="openDrawer('tos')">
<div class="engine-card-header">
<h3>{{ $t('settings.storage.tosTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.tosDesc') }}</p>
</div>
<!-- S3 -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 's3' }]" @click="openDrawer('s3')">
<div class="engine-card-header">
<h3>{{ $t('settings.storage.s3Title') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.s3Desc') }}</p>
</div>
<!-- OSS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'oss' }]" @click="openDrawer('oss')">
<div class="engine-card-header">
<h3>{{ $t('settings.storage.ossTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
</div>
<p class="engine-card-desc">{{ $t('settings.storage.ossDesc') }}</p>
</div>
</div>
</template>
<!-- 配置抽屉 -->
<t-drawer
v-model:visible="drawerVisible"
:header="drawerTitle"
size="500px"
:footer="true"
@confirm="onSave"
>
<div class="drawer-content">
<!-- Local -->
<template v-if="currentEngine === 'local'">
<div class="engine-info-block">
<p class="engine-desc">{{ $t('settings.storage.localDesc') }}</p>
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.local.path_prefix"
:placeholder="$t('settings.storage.pathPrefixPlaceholder')"
clearable
/>
</div>
</div>
</template>
<!-- MinIO -->
<template v-else-if="currentEngine === 'minio'">
<div class="engine-info-block">
<p class="engine-desc">{{ $t('settings.storage.minioDesc') }}</p>
</div>
<div class="mode-selector">
<div
:class="['mode-option', { active: config.minio.mode !== 'remote' }]"
@click="config.minio.mode = 'docker'"
>
<span class="mode-label">{{ $t('settings.storage.minioDocker') }}</span>
<t-tag v-if="minioEnvAvailable" theme="success" variant="light" size="small">{{ $t('settings.storage.detected') }}</t-tag>
<t-tag v-else theme="default" variant="light" size="small">{{ $t('settings.storage.notDetected') }}</t-tag>
</div>
<div
:class="['mode-option', { active: config.minio.mode === 'remote' }]"
@click="config.minio.mode = 'remote'"
>
<span class="mode-label">{{ $t('settings.storage.minioRemote') }}</span>
</div>
</div>
<!-- Docker mode -->
<div v-if="config.minio.mode !== 'remote'">
<div v-if="minioEnvAvailable" class="engine-hint success">
{{ $t('settings.storage.minioDockerDetected') }}
</div>
<div v-else class="engine-hint warning">
{{ $t('settings.storage.minioDockerNotDetected') }}
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.minio.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
:disabled="!minioEnvAvailable"
clearable
/>
</div>
<div class="form-item form-item--inline">
<label class="form-label">Use SSL</label>
<t-switch v-model="config.minio.use_ssl" size="small" />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.minio.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</div>
<!-- Remote mode -->
<div v-else>
<div class="engine-hint">{{ $t('settings.storage.minioRemoteHint') }}</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.minio.endpoint"
placeholder="e.g. minio.example.com:9000"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Access Key ID</label>
<t-input
v-model="config.minio.access_key_id"
placeholder="MinIO Access Key"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Secret Access Key</label>
<t-input
v-model="config.minio.secret_access_key"
type="password"
placeholder="MinIO Secret Key"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.minio.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
</div>
<div class="form-item form-item--inline">
<label class="form-label">Use SSL</label>
<t-switch v-model="config.minio.use_ssl" size="small" />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.minio.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</div>
</template>
<!-- COS -->
<template v-else-if="currentEngine === 'cos'">
<div class="engine-info-block">
<p class="engine-desc">
{{ $t('settings.storage.cosDesc') }}
<a class="engine-link" href="https://console.cloud.tencent.com/cos" target="_blank" rel="noopener">{{ $t('settings.storage.console') }} </a>
<a class="engine-link" href="https://cloud.tencent.com/document/product/436" target="_blank" rel="noopener">{{ $t('settings.storage.docs') }} </a>
</p>
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Secret ID</label>
<t-input
v-model="config.cos.secret_id"
:placeholder="$t('settings.storage.cosSecretIdPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.cos.secret_key"
type="password"
:placeholder="$t('settings.storage.cosSecretKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.cos.region"
placeholder="e.g. ap-guangzhou"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.cos.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">App ID</label>
<t-input
v-model="config.cos.app_id"
:placeholder="$t('settings.storage.cosAppIdPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.cos.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</template>
<!-- TOS -->
<template v-else-if="currentEngine === 'tos'">
<div class="engine-info-block">
<p class="engine-desc">
{{ $t('settings.storage.tosDesc') }}
<a class="engine-link" href="https://console.volcengine.com/tos" target="_blank" rel="noopener">{{ $t('settings.storage.console') }} </a>
<a class="engine-link" href="https://www.volcengine.com/docs/6349" target="_blank" rel="noopener">{{ $t('settings.storage.docs') }} </a>
</p>
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.tos.endpoint"
placeholder="e.g. https://tos-cn-beijing.volces.com"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.tos.region"
placeholder="e.g. cn-beijing"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.tos.access_key"
:placeholder="$t('settings.storage.tosAccessKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.tos.secret_key"
type="password"
:placeholder="$t('settings.storage.tosSecretKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.tos.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.tos.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</template>
<!-- S3 -->
<template v-else-if="currentEngine === 's3'">
<div class="engine-info-block">
<p class="engine-desc">
{{ $t('settings.storage.s3Desc') }}
<a class="engine-link" href="https://aws.amazon.com/s3/" target="_blank" rel="noopener">{{ $t('settings.storage.console') }} </a>
<a class="engine-link" href="https://docs.aws.amazon.com/s3/" target="_blank" rel="noopener">{{ $t('settings.storage.docs') }} </a>
</p>
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.s3.endpoint"
placeholder="e.g. https://s3.amazonaws.com"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.s3.region"
placeholder="e.g. us-east-1"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.s3.access_key"
:placeholder="$t('settings.storage.s3AccessKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.s3.secret_key"
type="password"
:placeholder="$t('settings.storage.s3SecretKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.s3.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.s3.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</template>
<!-- OSS -->
<template v-else-if="currentEngine === 'oss'">
<div class="engine-info-block">
<p class="engine-desc">
{{ $t('settings.storage.ossDesc') }}
<a class="engine-link" href="https://oss.console.aliyun.com/" target="_blank" rel="noopener">{{ $t('settings.storage.console') }} </a>
<a class="engine-link" href="https://help.aliyun.com/zh/oss/" target="_blank" rel="noopener">{{ $t('settings.storage.docs') }} </a>
</p>
</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.oss.endpoint"
placeholder="e.g. https://oss-cn-hangzhou.aliyuncs.com"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.oss.region"
placeholder="e.g. cn-hangzhou"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.oss.access_key"
:placeholder="$t('settings.storage.ossAccessKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.oss.secret_key"
type="password"
:placeholder="$t('settings.storage.ossSecretKeyPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.oss.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.oss.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
</div>
</div>
</template>
<div class="form-item" v-if="currentEngine && currentEngine !== 'local'">
<label class="form-label">{{ $t('settings.storage.testConnection') }}</label>
<div class="api-test-section">
<t-button variant="outline" :loading="currentCheckState.loading" @click="currentCheckState.onCheck">
{{ $t('settings.storage.testConnection') }}
</t-button>
<span v-if="currentCheckState.result" :class="['test-message', currentCheckState.result.ok ? (currentCheckState.result.bucket_created ? 'created' : 'success') : 'error']">
{{ currentCheckState.result.message }}
</span>
</div>
</div>
</div>
<template #footer>
<div class="drawer-footer-actions">
<t-button theme="default" variant="outline" @click="drawerVisible = false">{{ $t('common.cancel') }}</t-button>
<t-button theme="primary" :loading="saving" @click="onSave">{{ $t('common.save') }}</t-button>
</div>
</template>
</t-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import {
getStorageEngineConfig,
updateStorageEngineConfig,
getStorageEngineStatus,
checkStorageEngine,
type StorageEngineConfig,
} from '@/api/system'
const { t } = useI18n()
const defaultConfig = (): StorageEngineConfig => ({
default_provider: 'local',
local: { path_prefix: '' },
minio: { mode: 'docker', endpoint: '', access_key_id: '', secret_access_key: '', bucket_name: '', use_ssl: false, path_prefix: '' },
cos: {
secret_id: '',
secret_key: '',
region: '',
bucket_name: '',
app_id: '',
path_prefix: '',
},
tos: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
s3: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
oss: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
use_temp_bucket: false,
temp_bucket_name: '',
temp_region: '',
},
})
const loading = ref(true)
const error = ref('')
const config = ref<StorageEngineConfig>(defaultConfig())
const engineStatus = ref<{ local: boolean; minio: boolean; cos: boolean }>({
local: true,
minio: false,
cos: true,
})
const minioEnvAvailable = ref(false)
const saving = ref(false)
const saveMessage = ref('')
const saveSuccess = ref(false)
const checkingMinio = ref(false)
const minioCheckResult = ref<{ ok: boolean; message: string; bucket_created?: boolean } | null>(null)
const checkingCos = ref(false)
const cosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingTos = ref(false)
const tosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingS3 = ref(false)
const s3CheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingOss = ref(false)
const ossCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const drawerVisible = ref(false)
const currentEngine = ref<string | null>(null)
const currentCheckState = computed(() => {
switch (currentEngine.value) {
case 'minio': return { loading: checkingMinio.value, result: minioCheckResult.value, onCheck: onCheckMinio }
case 'cos': return { loading: checkingCos.value, result: cosCheckResult.value, onCheck: onCheckCos }
case 'tos': return { loading: checkingTos.value, result: tosCheckResult.value, onCheck: onCheckTos }
case 's3': return { loading: checkingS3.value, result: s3CheckResult.value, onCheck: onCheckS3 }
case 'oss': return { loading: checkingOss.value, result: ossCheckResult.value, onCheck: onCheckOss }
default: return { loading: false, result: null, onCheck: () => {} }
}
})
const drawerTitle = computed(() => {
if (!currentEngine.value) return ''
const titles: Record<string, string> = {
local: t('settings.storage.localTitle'),
minio: 'MinIO',
cos: t('settings.storage.cosTitle'),
tos: t('settings.storage.tosTitle'),
s3: t('settings.storage.s3Title'),
oss: t('settings.storage.ossTitle'),
}
return titles[currentEngine.value] || currentEngine.value
})
const minioAvailable = computed(() => {
if (config.value.minio?.mode === 'remote') {
return !!(config.value.minio.endpoint && config.value.minio.access_key_id && config.value.minio.secret_access_key)
}
return minioEnvAvailable.value
})
function openDrawer(engine: string) {
currentEngine.value = engine
drawerVisible.value = true
saveMessage.value = ''
// Reset check results
minioCheckResult.value = null
cosCheckResult.value = null
tosCheckResult.value = null
s3CheckResult.value = null
ossCheckResult.value = null
}
async function loadConfig() {
try {
const res = await getStorageEngineConfig()
const d = res?.data
if (d) {
config.value = {
default_provider: d.default_provider || 'local',
local: d.local ? { path_prefix: d.local.path_prefix || '' } : { path_prefix: '' },
minio: d.minio
? {
mode: d.minio.mode || 'docker',
endpoint: d.minio.endpoint || '',
access_key_id: d.minio.access_key_id || '',
secret_access_key: d.minio.secret_access_key || '',
bucket_name: d.minio.bucket_name || '',
use_ssl: d.minio.use_ssl ?? false,
path_prefix: d.minio.path_prefix || '',
}
: defaultConfig().minio!,
cos: d.cos
? {
secret_id: d.cos.secret_id || '',
secret_key: d.cos.secret_key || '',
region: d.cos.region || '',
bucket_name: d.cos.bucket_name || '',
app_id: d.cos.app_id || '',
path_prefix: d.cos.path_prefix || '',
}
: defaultConfig().cos!,
tos: d.tos
? {
endpoint: d.tos.endpoint || '',
region: d.tos.region || '',
access_key: d.tos.access_key || '',
secret_key: d.tos.secret_key || '',
bucket_name: d.tos.bucket_name || '',
path_prefix: d.tos.path_prefix || '',
}
: defaultConfig().tos!,
s3: d.s3
? {
endpoint: d.s3.endpoint || '',
region: d.s3.region || '',
access_key: d.s3.access_key || '',
secret_key: d.s3.secret_key || '',
bucket_name: d.s3.bucket_name || '',
path_prefix: d.s3.path_prefix || '',
}
: defaultConfig().s3!,
oss: d.oss
? {
endpoint: d.oss.endpoint || '',
region: d.oss.region || '',
access_key: d.oss.access_key || '',
secret_key: d.oss.secret_key || '',
bucket_name: d.oss.bucket_name || '',
path_prefix: d.oss.path_prefix || '',
use_temp_bucket: d.oss.use_temp_bucket ?? false,
temp_bucket_name: d.oss.temp_bucket_name || '',
temp_region: d.oss.temp_region || '',
}
: defaultConfig().oss!,
}
}
} catch {
config.value = defaultConfig()
}
}
async function loadStatus() {
try {
const res = await getStorageEngineStatus()
const engines = res?.data?.engines ?? []
const status = { local: true, minio: false, cos: true }
for (const e of engines) {
if (e.name === 'local') status.local = e.available
if (e.name === 'minio') status.minio = e.available
if (e.name === 'cos') status.cos = e.available
}
engineStatus.value = status
minioEnvAvailable.value = res?.data?.minio_env_available ?? false
} catch {
engineStatus.value = { local: true, minio: false, cos: true }
minioEnvAvailable.value = false
}
}
async function loadAll() {
loading.value = true
error.value = ''
try {
await Promise.all([loadConfig(), loadStatus()])
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : t('settings.storage.loadFailed')
} finally {
loading.value = false
}
}
function buildPayload(): StorageEngineConfig {
const mode = config.value.minio?.mode || 'docker'
return {
default_provider: config.value.default_provider || 'local',
local: { path_prefix: (config.value.local?.path_prefix || '').trim() },
minio: {
mode,
endpoint: mode === 'remote' ? (config.value.minio?.endpoint || '').trim() : '',
access_key_id: mode === 'remote' ? (config.value.minio?.access_key_id || '').trim() : '',
secret_access_key: mode === 'remote' ? (config.value.minio?.secret_access_key || '').trim() : '',
bucket_name: (config.value.minio?.bucket_name || '').trim(),
use_ssl: config.value.minio?.use_ssl ?? false,
path_prefix: (config.value.minio?.path_prefix || '').trim(),
},
cos: {
secret_id: (config.value.cos?.secret_id || '').trim(),
secret_key: (config.value.cos?.secret_key || '').trim(),
region: (config.value.cos?.region || '').trim(),
bucket_name: (config.value.cos?.bucket_name || '').trim(),
app_id: (config.value.cos?.app_id || '').trim(),
path_prefix: (config.value.cos?.path_prefix || '').trim(),
},
tos: {
endpoint: (config.value.tos?.endpoint || '').trim(),
region: (config.value.tos?.region || '').trim(),
access_key: (config.value.tos?.access_key || '').trim(),
secret_key: (config.value.tos?.secret_key || '').trim(),
bucket_name: (config.value.tos?.bucket_name || '').trim(),
path_prefix: (config.value.tos?.path_prefix || '').trim(),
},
s3: {
endpoint: (config.value.s3?.endpoint || '').trim(),
region: (config.value.s3?.region || '').trim(),
access_key: (config.value.s3?.access_key || '').trim(),
secret_key: (config.value.s3?.secret_key || '').trim(),
bucket_name: (config.value.s3?.bucket_name || '').trim(),
path_prefix: (config.value.s3?.path_prefix || '').trim(),
},
oss: {
endpoint: (config.value.oss?.endpoint || '').trim(),
region: (config.value.oss?.region || '').trim(),
access_key: (config.value.oss?.access_key || '').trim(),
secret_key: (config.value.oss?.secret_key || '').trim(),
bucket_name: (config.value.oss?.bucket_name || '').trim(),
path_prefix: (config.value.oss?.path_prefix || '').trim(),
// Temp bucket fields: not exposed in UI; server manages these independently
use_temp_bucket: config.value.oss?.use_temp_bucket ?? false,
temp_bucket_name: (config.value.oss?.temp_bucket_name || '').trim(),
temp_region: (config.value.oss?.temp_region || '').trim(),
},
}
}
async function onSave() {
saving.value = true
saveMessage.value = ''
try {
await updateStorageEngineConfig(buildPayload())
await loadStatus()
saveSuccess.value = true
saveMessage.value = t('settings.storage.saveSuccess')
drawerVisible.value = false
} catch (e: unknown) {
saveSuccess.value = false
saveMessage.value = e instanceof Error ? e.message : t('settings.storage.saveFailed')
} finally {
saving.value = false
}
}
async function onSaveDefaultEngine() {
await onSave()
}
async function onCheckMinio() {
checkingMinio.value = true
minioCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'minio', minio: payload.minio })
minioCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
minioCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingMinio.value = false
}
}
async function onCheckCos() {
checkingCos.value = true
cosCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'cos', cos: payload.cos })
cosCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
cosCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingCos.value = false
}
}
async function onCheckTos() {
checkingTos.value = true
tosCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'tos', tos: payload.tos })
tosCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
tosCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingTos.value = false
}
}
async function onCheckS3() {
checkingS3.value = true
s3CheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 's3', s3: payload.s3 })
s3CheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
s3CheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingS3.value = false
}
}
async function onCheckOss() {
checkingOss.value = true
ossCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'oss', oss: payload.oss })
ossCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
ossCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingOss.value = false
}
}
onMounted(loadAll)
</script>
<style lang="less" scoped>
.storage-engine-settings {
width: 100%;
}
.section-header {
margin-bottom: 32px;
h2 {
font-size: 20px;
font-weight: 600;
color: var(--td-text-color-primary);
margin: 0 0 8px 0;
}
.section-description {
font-size: 14px;
color: var(--td-text-color-secondary);
margin: 0;
line-height: 1.5;
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 48px 0;
color: var(--td-text-color-placeholder);
font-size: 14px;
}
.error-inline {
padding: 16px 0;
}
.settings-group {
display: flex;
flex-direction: column;
gap: 0;
}
.setting-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px 0;
border-bottom: 1px solid var(--td-component-stroke);
&:last-child {
border-bottom: none;
}
}
.setting-info {
flex: 1;
max-width: 65%;
padding-right: 24px;
label {
font-size: 15px;
font-weight: 500;
color: var(--td-text-color-primary);
display: block;
margin-bottom: 4px;
}
.desc {
font-size: 13px;
color: var(--td-text-color-secondary);
margin: 0;
line-height: 1.5;
}
}
.setting-control {
flex-shrink: 0;
min-width: 280px;
display: flex;
justify-content: flex-end;
align-items: center;
}
// ---- 引擎卡片布局 ----
.engine-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
margin-top: 24px;
}
.engine-card {
border: 1px solid var(--td-component-stroke);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--td-bg-color-container);
display: flex;
flex-direction: column;
&:hover {
border-color: var(--td-brand-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&.active {
border-color: var(--td-brand-color);
background: rgba(var(--td-brand-color-5-rgba), 0.05);
}
}
.engine-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
h3 {
font-size: 15px;
font-weight: 600;
color: var(--td-text-color-primary);
margin: 0;
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.engine-card-desc {
font-size: 13px;
color: var(--td-text-color-secondary);
margin: 0;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
// ---- 抽屉内容 ----
.drawer-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.engine-info-block {
.engine-desc {
font-size: 13px;
color: var(--td-text-color-secondary);
margin: 0 0 8px 0;
line-height: 1.5;
}
}
// 输入框样式
:deep(.t-input),
:deep(.t-select) {
width: 100%;
font-size: 13px;
.t-input__inner,
.t-input__wrap,
input {
font-size: 13px;
border-radius: 6px;
border-color: var(--td-component-stroke);
transition: all 0.15s ease;
}
&:hover .t-input__inner,
&:hover .t-input__wrap,
&:hover input {
border-color: var(--td-component-stroke);
}
&.t-is-focused .t-input__inner,
&.t-is-focused .t-input__wrap,
&.t-is-focused input {
border-color: var(--td-brand-color);
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
}
}
.engine-link {
color: var(--td-brand-color);
text-decoration: none;
margin-left: 4px;
font-size: 13px;
&:hover {
text-decoration: underline;
}
}
.engine-form {
display: flex;
flex-direction: column;
gap: 0;
}
.form-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--td-text-color-primary);
&.required::after {
content: '*';
color: var(--td-error-color);
margin-left: 4px;
font-weight: 600;
}
}
.form-item--inline {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
.form-label {
margin-bottom: 0;
flex-shrink: 0;
}
}
.mode-selector {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.mode-option {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--td-component-stroke);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
background: var(--td-bg-color-secondarycontainer);
&:hover {
border-color: var(--td-text-color-disabled);
}
&.active {
border-color: var(--td-brand-color);
background: rgba(7, 192, 95, 0.06);
}
.mode-label {
font-size: 13px;
font-weight: 500;
color: var(--td-text-color-primary);
}
}
.engine-hint {
font-size: 13px;
color: var(--td-text-color-secondary);
line-height: 1.6;
padding: 10px 14px;
margin-bottom: 16px;
border-radius: 6px;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid var(--td-component-stroke);
&.success {
color: var(--td-text-color-primary);
background: var(--td-success-color-light);
border-color: var(--td-success-color-focus);
}
&.warning {
color: var(--td-text-color-primary);
background: var(--td-warning-color-light);
border-color: var(--td-warning-color-focus);
}
}
.drawer-actions {
display: flex;
align-items: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed var(--td-component-stroke);
}
.api-test-section {
display: flex;
align-items: center;
gap: 12px;
.test-message {
font-size: 13px;
line-height: 1.5;
flex: 1;
&.success {
color: var(--td-brand-color-active);
}
&.error {
color: var(--td-error-color);
}
&.created {
color: var(--td-warning-color);
}
}
:deep(.t-button) {
min-width: 88px;
height: 32px;
font-size: 13px;
border-radius: 6px;
flex-shrink: 0;
}
}
.drawer-footer-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
width: 100%;
:deep(.t-button) {
min-width: 80px;
height: 36px;
font-weight: 500;
font-size: 14px;
border-radius: 6px;
transition: all 0.15s ease;
&.t-button--variant-outline {
color: var(--td-text-color-secondary);
border-color: var(--td-component-stroke);
&:hover {
border-color: var(--td-brand-color);
color: var(--td-brand-color);
background: rgba(7, 192, 95, 0.04);
}
}
}
}
.save-msg {
font-size: 13px;
&.success {
color: var(--td-success-color);
}
&.error {
color: var(--td-error-color);
}
}
</style>