|
|
@@ -1,214 +1,205 @@
|
|
|
<template>
|
|
|
<div class="storage-manager" v-loading="pageLoading">
|
|
|
- <el-alert
|
|
|
- title="选择引擎查看配置,支持修改保存和连通测试。"
|
|
|
- type="info"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- />
|
|
|
+ <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 v-if="engines.length" class="grid">
|
|
|
+ <div v-for="item in engines" :key="item.name" class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="card-head__top">
|
|
|
+ <div class="title-block">
|
|
|
+ <div class="title">{{ formatProviderLabel(item.name) }}</div>
|
|
|
+ <div class="subtitle">{{ item.description || '支持对象存储配置与连通测试' }}</div>
|
|
|
</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 class="tags">
|
|
|
+ <el-tag :type="item.allowed ? 'success' : 'info'" effect="light">
|
|
|
+ {{ item.allowed ? '允许' : '不允许' }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag :type="item.available ? 'primary' : 'warning'" effect="light">
|
|
|
+ {{ item.available ? '可用' : '不可用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-footer">
|
|
|
+ <el-button link type="primary" :loading="providerLoading === item.name" @click="openEditProvider(item.name)">
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <el-empty v-else description="暂无引擎" />
|
|
|
+
|
|
|
+ <el-drawer v-model="drawerVisible"
|
|
|
+ :title="selectedProvider ? `${formatProviderLabel(selectedProvider)} 配置` : '编辑存储引擎'" direction="rtl" size="760px">
|
|
|
+ <template v-if="selectedProvider">
|
|
|
+ <div class="drawer-intro">
|
|
|
+ <div class="drawer-intro__title">{{ formatProviderLabel(selectedProvider) }}</div>
|
|
|
+ <div class="drawer-intro__desc">修改当前存储引擎配置后保存,可直接在此执行重新加载与连通测试。</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>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <el-button @click="drawerVisible = false">取消</el-button>
|
|
|
+ <el-button @click="reloadSelectedProvider" :loading="providerLoading === selectedProvider">
|
|
|
+ 重新加载
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" plain :disabled="!selectedProvider"
|
|
|
+ :loading="selectedProvider ? testingProvider === selectedProvider : false"
|
|
|
+ @click="selectedProvider && testProvider(selectedProvider)">
|
|
|
+ 测试连接
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-drawer>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { onMounted, reactive, ref } from 'vue'
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { storageProvider } from '@repo/api-service'
|
|
|
|
|
|
@@ -280,6 +271,7 @@ const pageLoading = ref(false)
|
|
|
const engineLoading = ref(false)
|
|
|
const initLoading = ref(false)
|
|
|
const saving = ref(false)
|
|
|
+const drawerVisible = ref(false)
|
|
|
const selectedProvider = ref<StorageProviderName | ''>('')
|
|
|
const providerLoading = ref<StorageProviderName | ''>('')
|
|
|
const testingProvider = ref<StorageProviderName | ''>('')
|
|
|
@@ -448,9 +440,6 @@ async function loadEngines() {
|
|
|
description: item.description || ''
|
|
|
}))
|
|
|
}
|
|
|
- if (!selectedProvider.value && engines.value[0]) {
|
|
|
- await selectProvider(engines.value[0]!.name)
|
|
|
- }
|
|
|
} catch {
|
|
|
ElMessage.error('加载引擎失败')
|
|
|
} finally {
|
|
|
@@ -499,6 +488,11 @@ async function selectProvider(provider: StorageProviderName) {
|
|
|
await loadProviderConfig(provider)
|
|
|
}
|
|
|
|
|
|
+async function openEditProvider(provider: StorageProviderName) {
|
|
|
+ await selectProvider(provider)
|
|
|
+ drawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
function buildCheckPayload(provider: StorageProviderName) {
|
|
|
switch (provider) {
|
|
|
case 'local':
|
|
|
@@ -535,6 +529,10 @@ async function testProvider(provider: StorageProviderName) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+async function getDefaultEngine() {
|
|
|
+ return storageProvider.()
|
|
|
+}
|
|
|
+
|
|
|
function buildUpdatePayload() {
|
|
|
return {
|
|
|
default_provider: form.default_provider,
|
|
|
@@ -547,6 +545,13 @@ function buildUpdatePayload() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+async function ensureAllConfigsLoaded() {
|
|
|
+ for (const provider of providerNames) {
|
|
|
+ if (loadedProviders.value[provider]) continue
|
|
|
+ await loadProviderConfig(provider)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function saveConfig() {
|
|
|
if (!selectedProvider.value) {
|
|
|
ElMessage.warning('请先选择一个引擎')
|
|
|
@@ -554,10 +559,12 @@ async function saveConfig() {
|
|
|
}
|
|
|
saving.value = true
|
|
|
try {
|
|
|
+ await ensureAllConfigsLoaded()
|
|
|
const res = await storageProvider.postStorageProviderUpdate(buildUpdatePayload() as any)
|
|
|
if (res?.isSuccess) {
|
|
|
ElMessage.success('配置已保存')
|
|
|
await loadEngines()
|
|
|
+ drawerVisible.value = false
|
|
|
return
|
|
|
}
|
|
|
ElMessage.error('保存失败')
|
|
|
@@ -588,7 +595,6 @@ onMounted(async () => {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 16px;
|
|
|
- min-height: calc(100vh - 210px);
|
|
|
}
|
|
|
|
|
|
.storage-actions {
|
|
|
@@ -598,119 +604,175 @@ onMounted(async () => {
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
-.storage-layout {
|
|
|
+.toolbar-meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.pill {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 999px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ background: #f8fafc;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.grid {
|
|
|
display: grid;
|
|
|
- grid-template-columns: 280px minmax(0, 1fr);
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
gap: 16px;
|
|
|
- flex: 1;
|
|
|
- min-height: 0;
|
|
|
+ min-height: 240px;
|
|
|
}
|
|
|
|
|
|
-.engine-list {
|
|
|
+.card {
|
|
|
+ overflow: hidden;
|
|
|
+ padding: 16px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
- border-radius: 12px;
|
|
|
+ border-radius: 20px;
|
|
|
background: #fff;
|
|
|
- padding: 12px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
|
|
+ transition:
|
|
|
+ transform 0.22s ease,
|
|
|
+ box-shadow 0.22s ease,
|
|
|
+ border-color 0.22s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ border-color: #d8dee9;
|
|
|
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.card-head {
|
|
|
+ padding: 14px 16px;
|
|
|
+ border-radius: 18px;
|
|
|
+ background:
|
|
|
+ radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
|
|
|
+ linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
|
|
|
+}
|
|
|
+
|
|
|
+.card-head__top {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
- min-height: 0;
|
|
|
- overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.engine-list__header {
|
|
|
- font-size: 14px;
|
|
|
+.title-block {
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ font-size: 18px;
|
|
|
font-weight: 700;
|
|
|
- color: #111827;
|
|
|
+ line-height: 1.25;
|
|
|
+ color: #0f172a;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.subtitle {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #64748b;
|
|
|
+ line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
-.engine-list__body {
|
|
|
+.badge-row {
|
|
|
+ margin-top: 10px;
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 10px;
|
|
|
- flex: 1;
|
|
|
- min-height: 0;
|
|
|
- overflow-y: auto;
|
|
|
- padding-right: 4px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.engine-card {
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
- border-radius: 12px;
|
|
|
- background: #f8fafc;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s ease;
|
|
|
+.badge {
|
|
|
+ padding: 5px 10px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: rgba(255, 255, 255, 0.92);
|
|
|
+ font-size: 12px;
|
|
|
+ color: #334155;
|
|
|
+ border: 1px solid rgba(203, 213, 225, 0.9);
|
|
|
}
|
|
|
|
|
|
-.engine-card:hover,
|
|
|
-.engine-card--active {
|
|
|
- border-color: #3b82f6;
|
|
|
- background: #eff6ff;
|
|
|
+.badge.subtle {
|
|
|
+ background: rgba(15, 23, 42, 0.82);
|
|
|
+ color: #f8fafc;
|
|
|
+ border-color: transparent;
|
|
|
}
|
|
|
|
|
|
-.engine-card__top {
|
|
|
+.tags {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
+ flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
|
- margin-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
-.engine-card__name {
|
|
|
- font-weight: 700;
|
|
|
- color: #111827;
|
|
|
+.meta-list {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
|
|
|
-.engine-card__meta {
|
|
|
+.meta-item {
|
|
|
+ padding: 10px 12px;
|
|
|
+ border-radius: 14px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 6px;
|
|
|
- margin-bottom: 8px;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.engine-card__desc {
|
|
|
+.meta-label {
|
|
|
font-size: 12px;
|
|
|
- line-height: 1.5;
|
|
|
- color: #6b7280;
|
|
|
+ color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
-.config-panel {
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
- border-radius: 12px;
|
|
|
- background: #fff;
|
|
|
- padding: 16px;
|
|
|
+.meta-value {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #334155;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 16px;
|
|
|
- min-height: 0;
|
|
|
- overflow: auto;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: auto;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-intro {
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 18px;
|
|
|
+ background:
|
|
|
+ radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
|
|
|
+ linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
-.config-panel__title {
|
|
|
+.drawer-intro__title {
|
|
|
font-size: 16px;
|
|
|
font-weight: 700;
|
|
|
color: #111827;
|
|
|
}
|
|
|
|
|
|
-.config-panel__desc {
|
|
|
+.drawer-intro__desc {
|
|
|
margin-top: 6px;
|
|
|
font-size: 13px;
|
|
|
color: #6b7280;
|
|
|
}
|
|
|
|
|
|
-.config-panel__footer {
|
|
|
+.drawer-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;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.form-grid {
|
|
|
@@ -720,7 +782,8 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
@media (max-width: 960px) {
|
|
|
- .storage-layout,
|
|
|
+
|
|
|
+ .meta-list,
|
|
|
.form-grid {
|
|
|
grid-template-columns: minmax(0, 1fr);
|
|
|
}
|