注册表新增数据库存储同步
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user