注册表新增数据库存储同步

This commit is contained in:
2025-10-11 11:56:19 +08:00
parent 4e34063373
commit 47c68cadf3
23 changed files with 4396 additions and 160 deletions
@@ -0,0 +1,831 @@
<template>
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
<div class="database-config-modal" @click.stop>
<div class="modal-header">
<h3>数据库配置</h3>
<button @click="$emit('close')" class="close-btn" title="关闭">
<SvgIcon name="close" class="close-icon" />
</button>
</div>
<div class="modal-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<LoadingSpinner text="处理中..." />
</div>
<!-- 错误信息 -->
<div v-else-if="error" class="error">
<ErrorMessage :error="error" @dismiss="clearError" />
</div>
<!-- 成功提示 -->
<div v-if="successMessage" class="success-message">
<div class="success-content">
<SvgIcon name="success" class="success-icon" />
<span>{{ successMessage }}</span>
</div>
</div>
<!-- 配置表单 -->
<div class="config-form">
<div class="form-section">
<h4>连接设置</h4>
<div class="form-group">
<label for="db-host">主机地址</label>
<input
id="db-host"
v-model="config.host"
type="text"
class="form-input"
placeholder="localhost"
:disabled="loading"
/>
<small class="form-help">MySQL服务器的主机地址</small>
</div>
<div class="form-group">
<label for="db-port">端口</label>
<input
id="db-port"
v-model.number="config.port"
type="number"
class="form-input"
placeholder="3306"
min="1"
max="65535"
:disabled="loading"
/>
<small class="form-help">MySQL服务器端口默认为3306</small>
</div>
<div class="form-group">
<label for="db-user">用户名</label>
<input
id="db-user"
v-model="config.user"
type="text"
class="form-input"
placeholder="root"
:disabled="loading"
/>
<small class="form-help">数据库用户名</small>
</div>
<div class="form-group">
<label for="db-password">密码</label>
<div class="password-input-group">
<input
id="db-password"
v-model="config.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入密码"
:disabled="loading"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="password-toggle"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
<SvgIcon :name="showPassword ? 'eye-off' : 'eye'" class="toggle-icon" />
</button>
</div>
<small class="form-help">数据库密码</small>
</div>
<div class="form-group">
<label for="db-database">数据库名</label>
<input
id="db-database"
v-model="config.database"
type="text"
class="form-input"
placeholder="pixiv_d"
:disabled="loading"
/>
<small class="form-help">要使用的数据库名称如果不存在将自动创建</small>
</div>
</div>
<div class="form-section">
<h4>连接选项</h4>
<div class="form-group">
<label for="db-connection-limit">连接池大小</label>
<input
id="db-connection-limit"
v-model.number="config.connectionLimit"
type="number"
class="form-input"
placeholder="10"
min="1"
max="100"
:disabled="loading"
/>
<small class="form-help">连接池最大连接数建议5-20之间</small>
</div>
<div class="form-group">
<label for="db-timeout">连接超时 ()</label>
<input
id="db-timeout"
v-model.number="config.acquireTimeout"
type="number"
class="form-input"
placeholder="60"
min="5"
max="300"
:disabled="loading"
/>
<small class="form-help">获取连接的超时时间</small>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input
v-model="config.ssl"
type="checkbox"
class="form-checkbox"
:disabled="loading"
/>
启用SSL连接
</label>
<small class="form-help">是否使用SSL加密连接到数据库</small>
</div>
</div>
</div>
<!-- 连接测试结果 -->
<div v-if="testResult" class="test-result" :class="testResult.success ? 'success' : 'error'">
<div class="test-result-header">
<SvgIcon :name="testResult.success ? 'success' : 'error'" class="result-icon" />
<span class="result-title">{{ testResult.success ? '连接成功' : '连接失败' }}</span>
</div>
<div v-if="testResult.message" class="result-message">{{ testResult.message }}</div>
<div v-if="testResult.details" class="result-details">
<div v-for="(value, key) in testResult.details" :key="key" class="detail-item">
<span class="detail-key">{{ key }}:</span>
<span class="detail-value">{{ value }}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
@click="testConnection"
class="btn btn-secondary"
:disabled="loading || !isConfigValid"
>
<SvgIcon name="test-connect" class="btn-icon" />
{{ loading ? '测试中...' : '测试连接' }}
</button>
<button
@click="saveConfig"
class="btn btn-primary"
:disabled="loading || !isConfigValid"
>
<SvgIcon name="save" class="btn-icon" />
{{ loading ? '保存中...' : '保存配置' }}
</button>
<button
@click="$emit('close')"
class="btn btn-outline"
:disabled="loading"
>
取消
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import SvgIcon from './SvgIcon.vue';
import LoadingSpinner from './LoadingSpinner.vue';
import ErrorMessage from './ErrorMessage.vue';
import databaseService from '@/services/database';
interface DatabaseConfig {
host: string;
port: number;
user: string;
password: string;
database: string;
connectionLimit: number;
acquireTimeout: number;
ssl: boolean;
}
interface TestResult {
success: boolean;
message?: string;
details?: Record<string, any>;
}
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
close: [];
saved: [config: DatabaseConfig];
}>();
// 响应式数据
const loading = ref(false);
const error = ref<string | null>(null);
const successMessage = ref<string | null>(null);
const showPassword = ref(false);
const testResult = ref<TestResult | null>(null);
// 默认配置
const defaultConfig: DatabaseConfig = {
host: 'localhost',
port: 3306,
user: 'root',
password: '',
database: 'pixiv_d',
connectionLimit: 10,
acquireTimeout: 60,
ssl: false
};
const config = ref<DatabaseConfig>({ ...defaultConfig });
// 计算属性
const isConfigValid = computed(() => {
return config.value.host.trim() !== '' &&
config.value.port > 0 &&
config.value.user.trim() !== '' &&
config.value.database.trim() !== '';
});
// 方法
const clearError = () => {
error.value = null;
};
const clearSuccess = () => {
successMessage.value = null;
};
const showSuccess = (message: string) => {
successMessage.value = message;
setTimeout(() => {
successMessage.value = null;
}, 3000);
};
const handleOverlayClick = (event: MouseEvent) => {
// 移除点击外部自动关闭功能,用户需要手动点击关闭按钮
// if (event.target === event.currentTarget) {
// emit('close');
// }
};
// 测试数据库连接
const testConnection = async () => {
if (!isConfigValid.value) {
error.value = '请填写完整的数据库配置信息';
return;
}
loading.value = true;
error.value = null;
testResult.value = null;
try {
const result = await databaseService.testConnection(config.value);
if (result.success) {
testResult.value = {
success: true,
message: '数据库连接测试成功',
details: result.data
};
showSuccess('数据库连接测试成功');
} else {
testResult.value = {
success: false,
message: result.error || '连接测试失败'
};
error.value = result.error || '连接测试失败';
}
} catch (err: any) {
const errorMessage = err.message || '连接测试失败';
testResult.value = {
success: false,
message: errorMessage
};
error.value = errorMessage;
} finally {
loading.value = false;
}
};
// 保存配置
const saveConfig = async () => {
if (!isConfigValid.value) {
error.value = '请填写完整的数据库配置信息';
return;
}
loading.value = true;
error.value = null;
try {
const result = await databaseService.saveConfig(config.value);
if (result.success) {
showSuccess('数据库配置保存成功');
emit('saved', config.value);
// 延迟关闭模态框
setTimeout(() => {
emit('close');
}, 1500);
} else {
error.value = result.error || '保存配置失败';
}
} catch (err: any) {
error.value = err.message || '保存配置失败';
} finally {
loading.value = false;
}
};
// 加载现有配置
const loadConfig = async () => {
loading.value = true;
error.value = null;
try {
const result = await databaseService.getConfig();
if (result.success && result.data) {
config.value = { ...defaultConfig, ...result.data };
}
} catch (err: any) {
console.warn('加载数据库配置失败:', err.message);
// 不显示错误,使用默认配置
} finally {
loading.value = false;
}
};
// 监听模态框显示状态
watch(() => props.visible, (visible) => {
if (visible) {
// 重置状态
error.value = null;
successMessage.value = null;
testResult.value = null;
// 加载现有配置
loadConfig();
}
});
// 清除成功消息的定时器
watch(successMessage, (message) => {
if (message) {
setTimeout(() => {
if (successMessage.value === message) {
successMessage.value = null;
}
}, 3000);
}
});
onMounted(() => {
if (props.visible) {
loadConfig();
}
});
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.database-config-modal {
background: var(--color-bg-primary);
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-secondary);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.modal-header h3::before {
content: '🗄️';
font-size: 16px;
}
.close-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.close-icon {
width: 16px;
height: 16px;
}
.modal-content {
padding: 24px;
max-height: calc(90vh - 200px);
overflow-y: auto;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
}
.error {
margin-bottom: 20px;
}
.success-message {
margin-bottom: 20px;
padding: 12px 16px;
background: var(--color-success-light, #f0f9ff);
border: 1px solid var(--color-success, #10b981);
border-radius: 8px;
color: var(--color-success-dark, #065f46);
}
.success-content {
display: flex;
align-items: center;
gap: 8px;
}
.success-icon {
width: 16px;
height: 16px;
color: var(--color-success);
}
.config-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.form-section {
background: var(--color-bg-secondary);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--color-border);
}
.form-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.form-section h4::before {
content: '';
width: 3px;
height: 16px;
background: var(--color-primary);
border-radius: 2px;
}
.form-group {
margin-bottom: 16px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.form-input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 14px;
outline: none;
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-light);
}
.form-input:disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-tertiary);
cursor: not-allowed;
}
.form-input::placeholder {
color: var(--color-text-tertiary);
}
.password-input-group {
position: relative;
display: flex;
align-items: center;
}
.password-toggle {
position: absolute;
right: 8px;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}
.password-toggle:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.toggle-icon {
width: 14px;
height: 14px;
}
.form-help {
display: block;
margin-top: 4px;
font-size: 12px;
color: var(--color-text-tertiary);
line-height: 1.4;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: 500;
color: var(--color-text-primary);
}
.form-checkbox {
width: 16px;
height: 16px;
accent-color: var(--color-primary);
}
.test-result {
margin-top: 20px;
padding: 16px;
border-radius: 8px;
border: 1px solid;
}
.test-result.success {
background: var(--color-success-light, #f0f9ff);
border-color: var(--color-success, #10b981);
color: var(--color-success-dark, #065f46);
}
.test-result.error {
background: var(--color-danger-light, #fef2f2);
border-color: var(--color-danger, #ef4444);
color: var(--color-danger-dark, #991b1b);
}
.test-result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.result-icon {
width: 16px;
height: 16px;
}
.result-title {
font-weight: 600;
font-size: 14px;
}
.result-message {
font-size: 14px;
margin-bottom: 8px;
}
.result-details {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
opacity: 0.8;
}
.detail-item {
display: flex;
gap: 8px;
}
.detail-key {
font-weight: 500;
min-width: 80px;
}
.detail-value {
flex: 1;
}
.modal-footer {
display: flex;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid var(--color-border);
justify-content: flex-end;
background: var(--color-bg-secondary);
}
.btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-icon {
width: 14px;
height: 14px;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-hover);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--color-info);
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-info-hover);
transform: translateY(-1px);
}
.btn-outline {
background: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-outline:hover:not(:disabled) {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
border-color: var(--color-border-hover);
}
/* 响应式设计 */
@media (max-width: 768px) {
.database-config-modal {
width: 95%;
max-height: 95vh;
}
.modal-content {
padding: 16px;
max-height: calc(95vh - 160px);
}
.modal-header,
.modal-footer {
padding: 16px;
}
.form-section {
padding: 16px;
}
.modal-footer {
flex-direction: column-reverse;
}
.btn {
width: 100%;
justify-content: center;
}
}
</style>
+474 -4
View File
@@ -60,6 +60,92 @@
<div class="registry-config">
<h4>配置选项</h4>
<div class="config-form">
<!-- 存储模式配置 -->
<div class="config-section">
<div class="config-section-title">
<SvgIcon name="database" class="section-icon" />
存储模式配置
</div>
<div class="storage-mode-section">
<div class="storage-options">
<div class="storage-option" :class="{ active: selectedStorageMode === 'json' }">
<label>
<input type="radio" v-model="selectedStorageMode" value="json" :disabled="migrationLoading" />
<div class="option-content">
<div class="option-header">
<SvgIcon name="file" class="option-icon" />
<span class="option-title">JSON文件存储</span>
<span class="option-badge basic">默认</span>
</div>
<small class="option-description">使用本地JSON文件存储注册表数据简单可靠</small>
</div>
</label>
</div>
<div class="storage-option" :class="{ active: selectedStorageMode === 'database', disabled: !databaseConnected }">
<label>
<input type="radio" v-model="selectedStorageMode" value="database" :disabled="migrationLoading || !databaseConnected" />
<div class="option-content">
<div class="option-header">
<SvgIcon name="database" class="option-icon" />
<span class="option-title">MySQL数据库存储</span>
<span class="option-badge advanced" :class="{ connected: databaseConnected }">
{{ databaseConnected ? '已连接' : '未连接' }}
</span>
</div>
<small class="option-description">使用MySQL数据库存储注册表数据支持高并发和大数据量</small>
</div>
</label>
</div>
</div>
<div class="storage-config-actions">
<div class="config-status">
<span v-if="hasStorageModeChanges" class="config-indicator unsaved">
<SvgIcon name="warning" class="indicator-icon" />
配置已修改需要保存
</span>
<span v-else class="config-indicator saved">
<SvgIcon name="check" class="indicator-icon" />
当前模式: {{ storageMode === 'json' ? 'JSON文件存储' : 'MySQL数据库存储' }}
</span>
</div>
<div class="action-buttons">
<button
@click="applyStorageModeConfig"
class="btn btn-enhanced btn-primary"
:disabled="migrationLoading || !hasStorageModeChanges || (selectedStorageMode === 'database' && !databaseConnected)"
>
<SvgIcon name="save" class="btn-icon" />
{{ migrationLoading ? '应用中...' : '应用配置' }}
</button>
<button
@click="resetStorageModeConfig"
class="btn btn-enhanced btn-secondary"
:disabled="migrationLoading || !hasStorageModeChanges"
>
<SvgIcon name="refresh" class="btn-icon" />
重置
</button>
</div>
</div>
<div class="database-actions">
<button @click="openDatabaseConfig" class="btn btn-enhanced btn-secondary" :disabled="migrationLoading">
<SvgIcon name="settings" class="btn-icon" />
数据库配置
</button>
<div v-if="migrationLoading" class="migration-status">
<LoadingSpinner text="数据迁移中..." />
</div>
</div>
</div>
</div>
<!-- 检测模式配置 -->
<div class="config-section">
<div class="config-section-title">
<SvgIcon name="settings" class="section-icon" />
@@ -75,7 +161,7 @@
<span class="option-title">注册表检测</span>
<span class="option-badge recommended">推荐</span>
</div>
<small class="option-description">优先使用JSON注册表检测作品是否已下载速度最快</small>
<small class="option-description">优先使用注册表检测作品是否已下载速度最快</small>
</div>
</label>
</div>
@@ -205,6 +291,13 @@
</div>
</div>
</div>
<!-- 数据库配置模态框 -->
<DatabaseConfigModal
:visible="showDatabaseConfig"
@close="closeDatabaseConfig"
@saved="handleDatabaseConfigSaved"
/>
</div>
</template>
@@ -213,14 +306,28 @@ import { ref, onMounted, watch, computed, onUnmounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useRegistryStore } from '@/stores/registry';
import downloadService from '@/services/download';
import databaseService from '@/services/database';
import SvgIcon from './SvgIcon.vue';
import LoadingSpinner from './LoadingSpinner.vue';
import ErrorMessage from './ErrorMessage.vue';
import DatabaseConfigModal from './DatabaseConfigModal.vue';
const registryStore = useRegistryStore();
const isOpen = ref(false);
const successMessage = ref<string | null>(null);
// 数据库配置相关状态
const showDatabaseConfig = ref(false);
const databaseConnected = ref(false);
const storageMode = ref<'json' | 'database'>('json');
const selectedStorageMode = ref<'json' | 'database'>('json'); // 用户选择的存储模式
const migrationLoading = ref(false);
// 计算属性:检查存储模式是否有变更
const hasStorageModeChanges = computed(() => {
return selectedStorageMode.value !== storageMode.value;
});
// 从store中获取响应式数据
const { stats, loading, error, config } = storeToRefs(registryStore);
@@ -460,6 +567,25 @@ const updateDetectionMethod = async () => {
}
};
// 保存存储模式配置
const saveStorageModeConfig = async (mode: 'json' | 'database') => {
try {
const result = await registryStore.updateConfig({
useRegistryCheck: config.value.useRegistryCheck,
fallbackToScan: config.value.fallbackToScan,
storageMode: mode
});
if (result.success) {
console.log('存储模式配置已保存:', mode);
} else {
console.warn('保存存储模式配置失败:', result.error);
}
} catch (error) {
console.error('保存存储模式配置时出错:', error);
}
};
// 更新配置(保留原方法以防其他地方调用)
const updateConfig = async () => {
const result = await registryStore.updateConfig({
@@ -490,6 +616,98 @@ const showError = (message: string) => {
registryStore.error = message;
};
// 数据库配置相关方法
const openDatabaseConfig = () => {
showDatabaseConfig.value = true;
};
const closeDatabaseConfig = () => {
showDatabaseConfig.value = false;
};
const handleDatabaseConfigSaved = async () => {
showDatabaseConfig.value = false;
await checkDatabaseConnection();
showSuccess('数据库配置已保存');
};
const checkDatabaseConnection = async () => {
try {
const result = await databaseService.getConnectionStatus();
databaseConnected.value = result.success && (result.data?.connected || false);
} catch (error) {
console.error('检查数据库连接失败:', error);
databaseConnected.value = false;
}
};
// 应用存储模式配置
const applyStorageModeConfig = async () => {
if (selectedStorageMode.value === 'database' && !databaseConnected.value) {
showError('请先配置并连接数据库');
return;
}
if (selectedStorageMode.value === storageMode.value) {
return;
}
const confirmMessage = selectedStorageMode.value === 'database'
? '确定要切换到数据库存储模式吗?这将使用MySQL数据库存储注册表数据。'
: '确定要切换到JSON文件存储模式吗?这将使用本地JSON文件存储注册表数据。';
if (!confirm(confirmMessage)) {
return;
}
try {
migrationLoading.value = true;
if (selectedStorageMode.value === 'database') {
// 从JSON迁移到数据库
const result = await databaseService.migrateData('json-to-db');
if (result.success) {
storageMode.value = 'database';
showSuccess(`成功迁移到数据库存储,处理了 ${result.data?.recordsProcessed || 0} 条记录`);
} else {
throw new Error(result.error || '迁移到数据库失败');
}
} else {
// 从数据库迁移到JSON
const result = await databaseService.migrateData('db-to-json');
if (result.success) {
storageMode.value = 'json';
showSuccess(`成功迁移到JSON存储,处理了 ${result.data?.recordsProcessed || 0} 条记录`);
} else {
throw new Error(result.error || '迁移到JSON失败');
}
}
// 保存存储模式配置到后端
await saveStorageModeConfig(selectedStorageMode.value);
// 刷新统计信息
await refreshStats();
} catch (error) {
console.error('存储模式切换失败:', error);
showError(`存储模式切换失败: ${error instanceof Error ? error.message : '未知错误'}`);
// 恢复选择状态
selectedStorageMode.value = storageMode.value;
} finally {
migrationLoading.value = false;
}
};
// 重置存储模式配置
const resetStorageModeConfig = () => {
selectedStorageMode.value = storageMode.value;
};
const switchStorageMode = async (mode: 'json' | 'database') => {
// 保留原函数以防其他地方调用,但现在只是更新选择状态
selectedStorageMode.value = mode;
};
// 格式化日期
const formatDate = (dateString?: string): string => {
if (!dateString) return '未知';
@@ -507,18 +725,35 @@ const initDetectionMethod = () => {
}
};
// 初始化存储模式
const initStorageMode = () => {
if (config.value.storageMode) {
storageMode.value = config.value.storageMode;
selectedStorageMode.value = config.value.storageMode; // 同时更新选择状态
} else {
storageMode.value = 'json'; // 默认值
selectedStorageMode.value = 'json';
}
};
// 组件挂载时初始化
onMounted(async () => {
// 从后端获取配置并初始化检测方法
// 从后端获取配置并初始化检测方法和存储模式
try {
await registryStore.fetchConfig();
initDetectionMethod();
initStorageMode();
} catch (error) {
console.error('获取配置失败:', error);
// 如果获取配置失败,使用默认值
detectionMethod.value = 'hybrid';
storageMode.value = 'json';
selectedStorageMode.value = 'json';
}
// 检查数据库连接状态
await checkDatabaseConnection();
// 初始化时加载统计数据
refreshStats();
});
@@ -528,9 +763,10 @@ onUnmounted(() => {
stopProgressPolling();
});
// 监听配置变化,自动更新检测方法
// 监听配置变化,自动更新检测方法和存储模式
watch(config, () => {
initDetectionMethod();
initStorageMode();
}, { deep: true });
</script>
@@ -1703,7 +1939,241 @@ watch(config, () => {
font-size: 1rem;
}
/* 响应式设计 - 进度显示 */
/* 存储模式配置样式 */
.storage-mode-section {
display: flex;
flex-direction: column;
gap: var(--spacing-lg, 1rem);
}
.storage-options {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 0.75rem);
}
.storage-option {
background: var(--color-bg-primary, white);
border: 2px solid var(--color-border-light, #e2e8f0);
border-radius: var(--radius-lg, 0.5rem);
padding: var(--spacing-md, 0.75rem);
transition: all var(--transition-normal);
cursor: pointer;
position: relative;
overflow: hidden;
}
.storage-option:hover {
border-color: var(--color-border-hover, #cbd5e1);
box-shadow: var(--shadow-sm);
}
.storage-option.active {
border-color: var(--color-primary, #3b82f6);
background: var(--color-primary-light, #eff6ff);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.storage-option.disabled {
opacity: 0.6;
cursor: not-allowed;
background: var(--color-bg-tertiary, #f8fafc);
}
.storage-option.disabled:hover {
border-color: var(--color-border-light, #e2e8f0);
box-shadow: none;
}
.storage-option label {
display: flex;
align-items: flex-start;
gap: var(--spacing-md, 0.75rem);
cursor: pointer;
width: 100%;
}
.storage-option.disabled label {
cursor: not-allowed;
}
.storage-option input[type="radio"] {
width: 1.25rem;
height: 1.25rem;
margin-top: 0.125rem;
accent-color: var(--color-primary);
cursor: pointer;
flex-shrink: 0;
}
.storage-option.disabled input[type="radio"] {
cursor: not-allowed;
}
.option-badge.advanced {
background: var(--color-info-light, #e0f2fe);
color: var(--color-info-text, #0c4a6e);
border: 1px solid var(--color-info-border, #7dd3fc);
}
.option-badge.advanced.connected {
background: var(--color-success-light, #dcfce7);
color: var(--color-success-text, #166534);
border: 1px solid var(--color-success-border, #bbf7d0);
}
.database-actions {
display: flex;
align-items: center;
gap: var(--spacing-md, 0.75rem);
padding: var(--spacing-md, 0.75rem);
background: var(--color-bg-secondary, #f8fafc);
border-radius: var(--radius-md, 0.375rem);
border: 1px solid var(--color-border-light, #e2e8f0);
}
.migration-status {
display: flex;
align-items: center;
gap: var(--spacing-sm, 0.5rem);
color: var(--color-text-secondary, #6b7280);
font-size: 0.875rem;
}
/* 存储配置操作样式 */
.storage-config-actions {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 0.75rem);
padding: var(--spacing-md, 0.75rem);
background: var(--color-bg-secondary, #f8fafc);
border-radius: var(--radius-md, 0.375rem);
border: 1px solid var(--color-border-light, #e2e8f0);
}
.config-status {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm, 0.5rem);
}
.config-indicator {
display: flex;
align-items: center;
gap: var(--spacing-xs, 0.25rem);
font-size: 0.875rem;
font-weight: 500;
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
border-radius: var(--radius-sm, 0.25rem);
transition: all var(--transition-normal);
}
.config-indicator.saved {
color: var(--color-success-text, #166534);
background: var(--color-success-light, #dcfce7);
border: 1px solid var(--color-success-border, #bbf7d0);
}
.config-indicator.unsaved {
color: var(--color-warning-text, #92400e);
background: var(--color-warning-light, #fef3c7);
border: 1px solid var(--color-warning-border, #fbbf24);
animation: pulse 2s ease-in-out infinite;
}
.indicator-icon {
width: 0.875rem;
height: 0.875rem;
flex-shrink: 0;
}
.storage-config-actions .action-buttons {
display: flex;
gap: var(--spacing-sm, 0.5rem);
justify-content: center;
}
.storage-config-actions .btn {
flex: 1;
min-width: 0;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 配置节样式增强 */
.config-section {
margin-bottom: var(--spacing-xl, 1.5rem);
background: var(--color-bg-primary, white);
border: 1px solid var(--color-border-light, #e2e8f0);
border-radius: var(--radius-lg, 0.5rem);
overflow: hidden;
position: relative;
}
.config-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
}
.config-section-title {
display: flex;
align-items: center;
gap: var(--spacing-sm, 0.5rem);
padding: var(--spacing-lg, 1rem);
background: var(--color-bg-secondary, #f8fafc);
border-bottom: 1px solid var(--color-border-light, #e2e8f0);
font-weight: 600;
color: var(--color-text-primary, #374151);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.section-icon {
width: 1rem;
height: 1rem;
color: var(--color-primary);
}
.config-options,
.storage-options {
padding: var(--spacing-lg, 1rem);
}
/* 响应式设计 - 存储配置 */
@media (max-width: 768px) {
.database-actions {
flex-direction: column;
align-items: stretch;
}
.migration-status {
justify-content: center;
}
.storage-config-actions .action-buttons {
flex-direction: column;
}
.storage-config-actions .btn {
flex: none;
}
}
/* 原有的配置选项样式保持不变 */
@media (max-width: 768px) {
.progress-stats {
grid-template-columns: repeat(2, 1fr);