|
|
@@ -0,0 +1,473 @@
|
|
|
+<template>
|
|
|
+ <div class="management-page">
|
|
|
+ <div class="page-head">
|
|
|
+ <div>
|
|
|
+ <h1>{{ t('pages.vectorStore.title') }}</h1>
|
|
|
+ <p>{{ t('pages.vectorStore.description') }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="vector-manager" v-loading="pageLoading">
|
|
|
+ <!-- 搜索和操作栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <el-form :model="searchForm" inline class="search-form">
|
|
|
+ <el-form-item>
|
|
|
+ <el-input
|
|
|
+ v-model="searchForm.keyword"
|
|
|
+ :placeholder="t('pages.vectorStore.searchPlaceholder')"
|
|
|
+ clearable
|
|
|
+ @clear="handleSearch"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ {{ t('common.search') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="handleReset">
|
|
|
+ <el-icon><RefreshRight /></el-icon>
|
|
|
+ {{ t('common.reset') }}
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="toolbar-actions">
|
|
|
+ <el-button type="primary" @click="openCreate">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ {{ t('pages.vectorStore.create') }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 卡片列表 -->
|
|
|
+ <div class="card-grid">
|
|
|
+ <div v-for="item in list" :key="item.id" class="card">
|
|
|
+ <div class="card-head">
|
|
|
+ <div class="card-head__top">
|
|
|
+ <div class="title-block">
|
|
|
+ <div class="title">{{ item.name }}</div>
|
|
|
+ <div class="subtitle">{{ formatEngineType(item.engine_type) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="actions">
|
|
|
+ <el-dropdown>
|
|
|
+ <span class="actions-trigger">
|
|
|
+ <el-icon><MoreFilled /></el-icon>
|
|
|
+ </span>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item @click="handleTestConnection(item)">
|
|
|
+ {{ t('pages.vectorStore.testConnection') }}
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="openEdit(item)">
|
|
|
+ {{ t('common.edit') }}
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="openDetail(item)">
|
|
|
+ {{ t('common.details') }}
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item divided @click="handleDeleteConfirm(item)">
|
|
|
+ {{ t('common.delete') }}
|
|
|
+ </el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-info">
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">{{ t('pages.vectorStore.connectionAddr') }}</span>
|
|
|
+ <span class="info-value">{{ item.connection_config?.addr || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">创建时间</span>
|
|
|
+ <span class="info-value">{{ item.creationTime || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- <div class="tags">
|
|
|
+ <el-tag :type="item.source === 'system' ? 'warning' : 'info'" effect="light">
|
|
|
+ {{
|
|
|
+ item.source === 'system'
|
|
|
+ ? t('pages.vectorStore.sourceSystem')
|
|
|
+ : t('pages.vectorStore.sourceUser')
|
|
|
+ }}
|
|
|
+ </el-tag>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+ <el-empty v-if="!list.length" class="empty" :description="t('pages.vectorStore.empty')" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination" v-if="pagination.totalCount">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.pageIndex"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ background
|
|
|
+ layout="total, prev, pager, next"
|
|
|
+ :total="pagination.totalCount"
|
|
|
+ @current-change="loadList"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创建/编辑抽屉 -->
|
|
|
+ <VectorEditDrawer
|
|
|
+ v-model:visible="editDrawerVisible"
|
|
|
+ :is-edit="isEdit"
|
|
|
+ :edit-id="editId"
|
|
|
+ :engine-types="engineTypes"
|
|
|
+ :initial-data="editData"
|
|
|
+ @saved="loadList"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 详情抽屉 -->
|
|
|
+ <VectorDetailDrawer
|
|
|
+ v-model:visible="detailDrawerVisible"
|
|
|
+ :target-id="detailTargetId"
|
|
|
+ :engine-types="engineTypes"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, reactive, ref } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { MoreFilled, Plus, Refresh, RefreshRight, Search } from '@element-plus/icons-vue'
|
|
|
+import { vector } from '@repo/api-service'
|
|
|
+import { useI18n } from '@/composables/useI18n'
|
|
|
+import VectorEditDrawer from './VectorEditDrawer.vue'
|
|
|
+import VectorDetailDrawer from './VectorDetailDrawer.vue'
|
|
|
+import type { EngineTypeConfig, VectorStoreItem } from './types'
|
|
|
+
|
|
|
+const { t } = useI18n()
|
|
|
+
|
|
|
+// --- 状态 ---
|
|
|
+const pageLoading = ref(false)
|
|
|
+const editDrawerVisible = ref(false)
|
|
|
+const detailDrawerVisible = ref(false)
|
|
|
+const isEdit = ref(false)
|
|
|
+const editId = ref('')
|
|
|
+const editData = ref<VectorStoreItem | null>(null)
|
|
|
+const detailTargetId = ref('')
|
|
|
+const list = ref<VectorStoreItem[]>([])
|
|
|
+const engineTypes = ref<EngineTypeConfig[]>([])
|
|
|
+
|
|
|
+const searchForm = reactive({ keyword: '' })
|
|
|
+const pagination = reactive({ pageIndex: 1, pageSize: 20, totalCount: 0 })
|
|
|
+
|
|
|
+// --- 方法 ---
|
|
|
+function formatEngineType(type: string) {
|
|
|
+ const config = engineTypes.value.find((item) => item.type === type)
|
|
|
+ return config?.display_name || type.toUpperCase()
|
|
|
+}
|
|
|
+
|
|
|
+async function loadTypes() {
|
|
|
+ try {
|
|
|
+ const res = await vector.postTypes({})
|
|
|
+ if (res?.isSuccess && res.result) {
|
|
|
+ engineTypes.value = res.result as EngineTypeConfig[]
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error(t('pages.vectorStore.loadTypesFailed'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadList() {
|
|
|
+ pageLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await vector.postPageList({
|
|
|
+ keyword: searchForm.keyword,
|
|
|
+ pageIndex: pagination.pageIndex,
|
|
|
+ pageSize: pagination.pageSize
|
|
|
+ })
|
|
|
+ if (res?.isSuccess && res.result) {
|
|
|
+ const data = res.result?.model as any
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ list.value = data
|
|
|
+ pagination.totalCount = data.length
|
|
|
+ } else {
|
|
|
+ list.value = data.list || data.items || data.data || []
|
|
|
+ pagination.totalCount = data.totalCount ?? data.total ?? list.value.length
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error(t('pages.vectorStore.loadListFailed'))
|
|
|
+ } finally {
|
|
|
+ pageLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ pagination.pageIndex = 1
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleReset() {
|
|
|
+ searchForm.keyword = ''
|
|
|
+ pagination.pageIndex = 1
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+
|
|
|
+function openCreate() {
|
|
|
+ isEdit.value = false
|
|
|
+ editId.value = ''
|
|
|
+ editData.value = null
|
|
|
+ editDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function openEdit(row: VectorStoreItem) {
|
|
|
+ isEdit.value = true
|
|
|
+ editId.value = row.id
|
|
|
+ editData.value = row
|
|
|
+ editDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function openDetail(row: VectorStoreItem) {
|
|
|
+ detailTargetId.value = row.id
|
|
|
+ detailDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDeleteConfirm(row: VectorStoreItem) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ t('pages.vectorStore.deleteConfirm'),
|
|
|
+ t('pages.vectorStore.deleteConfirmTitle'),
|
|
|
+ { type: 'warning' }
|
|
|
+ )
|
|
|
+ await handleDelete(row)
|
|
|
+ } catch {
|
|
|
+ // cancelled
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(row: VectorStoreItem) {
|
|
|
+ try {
|
|
|
+ const res = await vector.postOpenApiDelete({ id: row.id })
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ ElMessage.success(t('pages.vectorStore.deleteSuccess'))
|
|
|
+ await loadList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(t('pages.vectorStore.deleteFailed'))
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error(t('pages.vectorStore.deleteFailed'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleTestConnection(row: VectorStoreItem) {
|
|
|
+ try {
|
|
|
+ const res = await vector.postConnectTestId({ id: row.id })
|
|
|
+ if (res?.isSuccess) {
|
|
|
+ ElMessage.success(`${row.name} ${t('pages.vectorStore.testSuccess')}`)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(`${row.name} ${t('pages.vectorStore.testFailed')}`)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error(`${row.name} ${t('pages.vectorStore.testFailed')}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ pageLoading.value = true
|
|
|
+ try {
|
|
|
+ await Promise.all([loadTypes(), loadList()])
|
|
|
+ } finally {
|
|
|
+ pageLoading.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.management-page {
|
|
|
+ padding: 24px;
|
|
|
+ min-height: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.page-head {
|
|
|
+ margin-bottom: 18px;
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 6px 0 0;
|
|
|
+ color: var(--text-tertiary);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.vector-manager {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.search-form {
|
|
|
+ :deep(.el-form-item) {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-form-item + .el-form-item) {
|
|
|
+ margin-left: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .el-button + .el-button {
|
|
|
+ margin-left: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ min-height: 200px;
|
|
|
+
|
|
|
+ .empty {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card {
|
|
|
+ overflow: hidden;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid var(--border-light);
|
|
|
+ border-radius: 20px;
|
|
|
+ background: var(--bg-base);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ transition:
|
|
|
+ transform 0.22s ease,
|
|
|
+ box-shadow 0.22s ease,
|
|
|
+ border-color 0.22s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ border-color: var(--border-base);
|
|
|
+ box-shadow: var(--shadow-md);
|
|
|
+}
|
|
|
+
|
|
|
+.card-head {
|
|
|
+ padding: 14px 16px;
|
|
|
+ border-radius: 18px;
|
|
|
+ background: var(--card-bg-unselected);
|
|
|
+}
|
|
|
+
|
|
|
+.card-head__top {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.title-block {
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.25;
|
|
|
+ color: var(--text-strong);
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.subtitle {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--text-secondary);
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.card-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 0 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-label {
|
|
|
+ color: var(--text-tertiary);
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 70px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-value {
|
|
|
+ color: var(--text-primary);
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.actions-trigger {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ border-radius: 999px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ color: var(--text-secondary);
|
|
|
+ background: var(--bg-overlay);
|
|
|
+ transition:
|
|
|
+ background 0.2s ease,
|
|
|
+ color 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.actions-trigger:hover {
|
|
|
+ background: var(--bg-container);
|
|
|
+ color: var(--text-strong);
|
|
|
+}
|
|
|
+
|
|
|
+.pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 960px) {
|
|
|
+ .toolbar {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|