|
|
@@ -0,0 +1,728 @@
|
|
|
+<template>
|
|
|
+ <div class="storage-manager" v-loading="pageLoading">
|
|
|
+ <el-alert
|
|
|
+ title="选择引擎查看配置,支持修改保存和连通测试。"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="storage-actions">
|
|
|
+ <el-button type="primary" :loading="initLoading" @click="handleInit">初始化存储</el-button>
|
|
|
+ <el-button :loading="engineLoading" @click="loadEngines">刷新列表</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="storage-layout">
|
|
|
+ <aside class="engine-list">
|
|
|
+ <div class="engine-list__header">引擎列表</div>
|
|
|
+ <div v-if="engines.length" class="engine-list__body">
|
|
|
+ <div
|
|
|
+ v-for="item in engines"
|
|
|
+ :key="item.name"
|
|
|
+ class="engine-card"
|
|
|
+ :class="{ 'engine-card--active': selectedProvider === item.name }"
|
|
|
+ @click="selectProvider(item.name)"
|
|
|
+ >
|
|
|
+ <div class="engine-card__top">
|
|
|
+ <div class="engine-card__name">{{ formatProviderLabel(item.name) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="engine-card__meta">
|
|
|
+ <el-tag size="small" :type="item.allowed ? 'success' : 'info'" effect="plain">
|
|
|
+ {{ item.allowed ? '允许' : '不允许' }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag size="small" :type="item.available ? 'primary' : 'warning'" effect="plain">
|
|
|
+ {{ item.available ? '可用' : '不可用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="engine-card__desc">
|
|
|
+ {{ item.description || '点击加载配置信息' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-empty v-else description="暂无引擎" />
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <section class="config-panel">
|
|
|
+ <template v-if="selectedProvider">
|
|
|
+ <div class="config-panel__header">
|
|
|
+ <div>
|
|
|
+ <div class="config-panel__title">{{ formatProviderLabel(selectedProvider) }} 配置</div>
|
|
|
+ <div class="config-panel__desc">点击左侧引擎后加载配置信息,修改后保存。</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-form :model="form" label-position="top">
|
|
|
+ <template v-if="selectedProvider === 'local'">
|
|
|
+ <el-form-item label="存储前缀">
|
|
|
+ <el-input v-model="form.local.path_prefix" placeholder="例如 knowledge/files" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="selectedProvider === 'minio'">
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="Endpoint">
|
|
|
+ <el-input v-model="form.minio.endpoint" placeholder="127.0.0.1:9000" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Mode">
|
|
|
+ <el-input v-model="form.minio.mode" placeholder="docker" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Access Key ID">
|
|
|
+ <el-input v-model="form.minio.access_key_id" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret Access Key">
|
|
|
+ <el-input v-model="form.minio.secret_access_key" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Bucket">
|
|
|
+ <el-input v-model="form.minio.bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Path Prefix">
|
|
|
+ <el-input v-model="form.minio.path_prefix" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ <el-form-item label="Use SSL">
|
|
|
+ <el-switch v-model="form.minio.use_ssl" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="selectedProvider === 'cos'">
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="App ID">
|
|
|
+ <el-input v-model="form.cos.app_id" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Region">
|
|
|
+ <el-input v-model="form.cos.region" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret ID">
|
|
|
+ <el-input v-model="form.cos.secret_id" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret Key">
|
|
|
+ <el-input v-model="form.cos.secret_key" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Bucket">
|
|
|
+ <el-input v-model="form.cos.bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Path Prefix">
|
|
|
+ <el-input v-model="form.cos.path_prefix" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="selectedProvider === 'tos'">
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="Endpoint">
|
|
|
+ <el-input v-model="form.tos.endpoint" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Region">
|
|
|
+ <el-input v-model="form.tos.region" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Access Key">
|
|
|
+ <el-input v-model="form.tos.access_key" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret Key">
|
|
|
+ <el-input v-model="form.tos.secret_key" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Bucket">
|
|
|
+ <el-input v-model="form.tos.bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Path Prefix">
|
|
|
+ <el-input v-model="form.tos.path_prefix" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="selectedProvider === 's3'">
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="Endpoint">
|
|
|
+ <el-input v-model="form.s3.endpoint" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Region">
|
|
|
+ <el-input v-model="form.s3.region" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Access Key">
|
|
|
+ <el-input v-model="form.s3.access_key" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret Key">
|
|
|
+ <el-input v-model="form.s3.secret_key" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Bucket">
|
|
|
+ <el-input v-model="form.s3.bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Path Prefix">
|
|
|
+ <el-input v-model="form.s3.path_prefix" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="selectedProvider === 'oss'">
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="Endpoint">
|
|
|
+ <el-input v-model="form.oss.endpoint" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Region">
|
|
|
+ <el-input v-model="form.oss.region" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Access Key">
|
|
|
+ <el-input v-model="form.oss.access_key" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Secret Key">
|
|
|
+ <el-input v-model="form.oss.secret_key" show-password />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Bucket">
|
|
|
+ <el-input v-model="form.oss.bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Path Prefix">
|
|
|
+ <el-input v-model="form.oss.path_prefix" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Temp Bucket">
|
|
|
+ <el-input v-model="form.oss.temp_bucket_name" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Temp Region">
|
|
|
+ <el-input v-model="form.oss.temp_region" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ <el-form-item label="Use Temp Bucket">
|
|
|
+ <el-switch v-model="form.oss.use_temp_bucket" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="config-panel__footer">
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ :loading="testingProvider === selectedProvider"
|
|
|
+ @click="testProvider(selectedProvider)"
|
|
|
+ >
|
|
|
+ 连通测试
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="reloadSelectedProvider" :loading="providerLoading === selectedProvider">
|
|
|
+ 重新加载
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div v-else class="config-panel__empty">请先从左侧选择一个引擎。</div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, reactive, ref } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { storageProvider } from '@repo/api-service'
|
|
|
+
|
|
|
+type StorageProviderName = 'local' | 'minio' | 'cos' | 'tos' | 's3' | 'oss'
|
|
|
+
|
|
|
+interface EngineItem {
|
|
|
+ name: StorageProviderName
|
|
|
+ allowed: boolean
|
|
|
+ available: boolean
|
|
|
+ description: string
|
|
|
+}
|
|
|
+
|
|
|
+interface StorageFormState {
|
|
|
+ default_provider: StorageProviderName
|
|
|
+ local: { provider: 'local'; path_prefix: string }
|
|
|
+ minio: {
|
|
|
+ provider: 'minio'
|
|
|
+ access_key_id: string
|
|
|
+ bucket_name: string
|
|
|
+ endpoint: string
|
|
|
+ mode: string
|
|
|
+ path_prefix: string
|
|
|
+ secret_access_key: string
|
|
|
+ use_ssl: boolean
|
|
|
+ }
|
|
|
+ cos: {
|
|
|
+ provider: 'cos'
|
|
|
+ app_id: string
|
|
|
+ bucket_name: string
|
|
|
+ path_prefix: string
|
|
|
+ region: string
|
|
|
+ secret_id: string
|
|
|
+ secret_key: string
|
|
|
+ }
|
|
|
+ tos: {
|
|
|
+ provider: 'tos'
|
|
|
+ access_key: string
|
|
|
+ bucket_name: string
|
|
|
+ endpoint: string
|
|
|
+ path_prefix: string
|
|
|
+ region: string
|
|
|
+ secret_key: string
|
|
|
+ }
|
|
|
+ s3: {
|
|
|
+ provider: 's3'
|
|
|
+ access_key: string
|
|
|
+ bucket_name: string
|
|
|
+ endpoint: string
|
|
|
+ path_prefix: string
|
|
|
+ region: string
|
|
|
+ secret_key: string
|
|
|
+ }
|
|
|
+ oss: {
|
|
|
+ provider: 'oss'
|
|
|
+ access_key: string
|
|
|
+ bucket_name: string
|
|
|
+ endpoint: string
|
|
|
+ path_prefix: string
|
|
|
+ region: string
|
|
|
+ secret_key: string
|
|
|
+ temp_bucket_name: string
|
|
|
+ temp_region: string
|
|
|
+ use_temp_bucket: boolean
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const providerNames: StorageProviderName[] = ['local', 'minio', 'cos', 'tos', 's3', 'oss']
|
|
|
+const pageLoading = ref(false)
|
|
|
+const engineLoading = ref(false)
|
|
|
+const initLoading = ref(false)
|
|
|
+const saving = ref(false)
|
|
|
+const selectedProvider = ref<StorageProviderName | ''>('')
|
|
|
+const providerLoading = ref<StorageProviderName | ''>('')
|
|
|
+const testingProvider = ref<StorageProviderName | ''>('')
|
|
|
+const engines = ref<EngineItem[]>([])
|
|
|
+const loadedProviders = ref<Record<StorageProviderName, boolean>>({
|
|
|
+ local: false,
|
|
|
+ minio: false,
|
|
|
+ cos: false,
|
|
|
+ tos: false,
|
|
|
+ s3: false,
|
|
|
+ oss: false
|
|
|
+})
|
|
|
+
|
|
|
+function createDefaultForm(): StorageFormState {
|
|
|
+ return {
|
|
|
+ default_provider: 'local',
|
|
|
+ local: { provider: 'local', path_prefix: '' },
|
|
|
+ minio: {
|
|
|
+ provider: 'minio',
|
|
|
+ access_key_id: '',
|
|
|
+ bucket_name: '',
|
|
|
+ endpoint: '',
|
|
|
+ mode: 'docker',
|
|
|
+ path_prefix: '',
|
|
|
+ secret_access_key: '',
|
|
|
+ use_ssl: false
|
|
|
+ },
|
|
|
+ cos: {
|
|
|
+ provider: 'cos',
|
|
|
+ app_id: '',
|
|
|
+ bucket_name: '',
|
|
|
+ path_prefix: '',
|
|
|
+ region: '',
|
|
|
+ secret_id: '',
|
|
|
+ secret_key: ''
|
|
|
+ },
|
|
|
+ tos: {
|
|
|
+ provider: 'tos',
|
|
|
+ access_key: '',
|
|
|
+ bucket_name: '',
|
|
|
+ endpoint: '',
|
|
|
+ path_prefix: '',
|
|
|
+ region: '',
|
|
|
+ secret_key: ''
|
|
|
+ },
|
|
|
+ s3: {
|
|
|
+ provider: 's3',
|
|
|
+ access_key: '',
|
|
|
+ bucket_name: '',
|
|
|
+ endpoint: '',
|
|
|
+ path_prefix: '',
|
|
|
+ region: '',
|
|
|
+ secret_key: ''
|
|
|
+ },
|
|
|
+ oss: {
|
|
|
+ provider: 'oss',
|
|
|
+ access_key: '',
|
|
|
+ bucket_name: '',
|
|
|
+ endpoint: '',
|
|
|
+ path_prefix: '',
|
|
|
+ region: '',
|
|
|
+ secret_key: '',
|
|
|
+ temp_bucket_name: '',
|
|
|
+ temp_region: '',
|
|
|
+ use_temp_bucket: false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const form = reactive<StorageFormState>(createDefaultForm())
|
|
|
+
|
|
|
+function formatProviderLabel(name: StorageProviderName) {
|
|
|
+ return name.toUpperCase()
|
|
|
+}
|
|
|
+
|
|
|
+function setLoaded(provider: StorageProviderName) {
|
|
|
+ loadedProviders.value[provider] = true
|
|
|
+}
|
|
|
+
|
|
|
+function assignConfig(provider: StorageProviderName, config: Record<string, any>) {
|
|
|
+ switch (provider) {
|
|
|
+ case 'local':
|
|
|
+ Object.assign(form.local, {
|
|
|
+ provider: 'local',
|
|
|
+ path_prefix: config.path_prefix ?? ''
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case 'minio':
|
|
|
+ Object.assign(form.minio, {
|
|
|
+ provider: 'minio',
|
|
|
+ access_key_id: config.access_key_id ?? '',
|
|
|
+ bucket_name: config.bucket_name ?? '',
|
|
|
+ endpoint: config.endpoint ?? '',
|
|
|
+ mode: config.mode ?? 'docker',
|
|
|
+ path_prefix: config.path_prefix ?? '',
|
|
|
+ secret_access_key: config.secret_access_key ?? '',
|
|
|
+ use_ssl: Boolean(config.use_ssl)
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case 'cos':
|
|
|
+ Object.assign(form.cos, {
|
|
|
+ provider: 'cos',
|
|
|
+ app_id: config.app_id ?? '',
|
|
|
+ bucket_name: config.bucket_name ?? '',
|
|
|
+ path_prefix: config.path_prefix ?? '',
|
|
|
+ region: config.region ?? '',
|
|
|
+ secret_id: config.secret_id ?? '',
|
|
|
+ secret_key: config.secret_key ?? ''
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case 'tos':
|
|
|
+ Object.assign(form.tos, {
|
|
|
+ provider: 'tos',
|
|
|
+ access_key: config.access_key ?? '',
|
|
|
+ bucket_name: config.bucket_name ?? '',
|
|
|
+ endpoint: config.endpoint ?? '',
|
|
|
+ path_prefix: config.path_prefix ?? '',
|
|
|
+ region: config.region ?? '',
|
|
|
+ secret_key: config.secret_key ?? ''
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case 's3':
|
|
|
+ Object.assign(form.s3, {
|
|
|
+ provider: 's3',
|
|
|
+ access_key: config.access_key ?? '',
|
|
|
+ bucket_name: config.bucket_name ?? '',
|
|
|
+ endpoint: config.endpoint ?? '',
|
|
|
+ path_prefix: config.path_prefix ?? '',
|
|
|
+ region: config.region ?? '',
|
|
|
+ secret_key: config.secret_key ?? ''
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case 'oss':
|
|
|
+ Object.assign(form.oss, {
|
|
|
+ provider: 'oss',
|
|
|
+ access_key: config.access_key ?? '',
|
|
|
+ bucket_name: config.bucket_name ?? '',
|
|
|
+ endpoint: config.endpoint ?? '',
|
|
|
+ path_prefix: config.path_prefix ?? '',
|
|
|
+ region: config.region ?? '',
|
|
|
+ secret_key: config.secret_key ?? '',
|
|
|
+ temp_bucket_name: config.temp_bucket_name ?? '',
|
|
|
+ temp_region: config.temp_region ?? '',
|
|
|
+ use_temp_bucket: Boolean(config.use_temp_bucket)
|
|
|
+ })
|
|
|
+ break
|
|
|
+ }
|
|
|
+ setLoaded(provider)
|
|
|
+}
|
|
|
+
|
|
|
+function extractConfig(result: Record<string, any>) {
|
|
|
+ return result?.config && typeof result.config === 'object' ? result.config : result
|
|
|
+}
|
|
|
+
|
|
|
+async function loadEngines() {
|
|
|
+ engineLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await storageProvider.postStorageProviderEngines({})
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ engines.value = (res.result || [])
|
|
|
+ .filter((item): item is EngineItem => providerNames.includes(item.name as StorageProviderName))
|
|
|
+ .map((item) => ({
|
|
|
+ name: item.name as StorageProviderName,
|
|
|
+ allowed: Boolean(item.allowed),
|
|
|
+ available: Boolean(item.available),
|
|
|
+ description: item.description || ''
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ if (!selectedProvider.value && engines.value[0]) {
|
|
|
+ await selectProvider(engines.value[0]!.name)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('加载引擎失败')
|
|
|
+ } finally {
|
|
|
+ engineLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleInit() {
|
|
|
+ initLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await storageProvider.postStorageProviderInitStorageProvider({})
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ ElMessage.success('初始化成功')
|
|
|
+ await loadEngines()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.error('初始化失败')
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('初始化失败')
|
|
|
+ } finally {
|
|
|
+ initLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadProviderConfig(provider: StorageProviderName) {
|
|
|
+ providerLoading.value = provider
|
|
|
+ try {
|
|
|
+ const res = await storageProvider.postStorageProviderConfig({ name: provider })
|
|
|
+ if (res?.isSuccess && res.result) {
|
|
|
+ const raw = res.result as Record<string, any>
|
|
|
+ assignConfig(provider, extractConfig(raw))
|
|
|
+ if (raw.is_default === true || raw.isDefault === true) {
|
|
|
+ form.default_provider = provider
|
|
|
+ }
|
|
|
+ selectedProvider.value = provider
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('加载配置失败')
|
|
|
+ } finally {
|
|
|
+ providerLoading.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function selectProvider(provider: StorageProviderName) {
|
|
|
+ selectedProvider.value = provider
|
|
|
+ await loadProviderConfig(provider)
|
|
|
+}
|
|
|
+
|
|
|
+function buildCheckPayload(provider: StorageProviderName) {
|
|
|
+ switch (provider) {
|
|
|
+ case 'local':
|
|
|
+ return { name: provider, config: { ...form.local } }
|
|
|
+ case 'minio':
|
|
|
+ return { name: provider, config: { ...form.minio } }
|
|
|
+ case 'cos':
|
|
|
+ return { name: provider, config: { ...form.cos } }
|
|
|
+ case 'tos':
|
|
|
+ return { name: provider, config: { ...form.tos } }
|
|
|
+ case 's3':
|
|
|
+ return { name: provider, config: { ...form.s3 } }
|
|
|
+ case 'oss':
|
|
|
+ return { name: provider, config: { ...form.oss } }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function testProvider(provider: StorageProviderName) {
|
|
|
+ testingProvider.value = provider
|
|
|
+ try {
|
|
|
+ if (selectedProvider.value !== provider || !loadedProviders.value[provider]) {
|
|
|
+ await loadProviderConfig(provider)
|
|
|
+ }
|
|
|
+ const res = await storageProvider.postStorageProviderCheck(buildCheckPayload(provider) as any)
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ ElMessage.success(`${formatProviderLabel(provider)} 连通成功`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.error(res?.error || `${formatProviderLabel(provider)} 连通失败`)
|
|
|
+ } catch {
|
|
|
+ ElMessage.error(`${formatProviderLabel(provider)} 连通失败`)
|
|
|
+ } finally {
|
|
|
+ testingProvider.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildUpdatePayload() {
|
|
|
+ return {
|
|
|
+ default_provider: form.default_provider,
|
|
|
+ cos: { ...form.cos },
|
|
|
+ local: { ...form.local },
|
|
|
+ minio: { ...form.minio },
|
|
|
+ oss: { ...form.oss },
|
|
|
+ s3: { ...form.s3 },
|
|
|
+ tos: { ...form.tos }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveConfig() {
|
|
|
+ if (!selectedProvider.value) {
|
|
|
+ ElMessage.warning('请先选择一个引擎')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ saving.value = true
|
|
|
+ try {
|
|
|
+ const res = await storageProvider.postStorageProviderUpdate(buildUpdatePayload() as any)
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ ElMessage.success('配置已保存')
|
|
|
+ await loadEngines()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function reloadSelectedProvider() {
|
|
|
+ if (!selectedProvider.value) return
|
|
|
+ await loadProviderConfig(selectedProvider.value)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ pageLoading.value = true
|
|
|
+ try {
|
|
|
+ await loadEngines()
|
|
|
+ } finally {
|
|
|
+ pageLoading.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.storage-manager {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ min-height: calc(100vh - 210px);
|
|
|
+}
|
|
|
+
|
|
|
+.storage-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.storage-layout {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 280px minmax(0, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-list {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fff;
|
|
|
+ padding: 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-list__header {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #111827;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-list__body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card {
|
|
|
+ padding: 12px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card:hover,
|
|
|
+.engine-card--active {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ background: #eff6ff;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card__top {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card__name {
|
|
|
+ font-weight: 700;
|
|
|
+ color: #111827;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card__meta {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.engine-card__desc {
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fff;
|
|
|
+ padding: 16px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel__title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #111827;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel__desc {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel__footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel__empty {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 320px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.form-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 0 12px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 960px) {
|
|
|
+ .storage-layout,
|
|
|
+ .form-grid {
|
|
|
+ grid-template-columns: minmax(0, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|