注册表新增数据库存储同步
This commit is contained in:
@@ -18,4 +18,6 @@ export const navigationIcons = {
|
||||
'cleanup-history2': 'M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z',
|
||||
'loading': 'M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z',
|
||||
'menu': 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z',
|
||||
}
|
||||
'test-connect':'M228.864 257.28a231.850667 231.850667 0 0 0-27.562667 90.282667c-5.888 59.306667 9.472 111.701333 43.733334 147.626666l56.32 59.52a58.624 58.624 0 0 0 40.96 18.261334h1.578666c14.976 0 29.44-5.76 40.277334-16.085334l28.288-26.752 38.442666 39.381334-25.6 24.192a58.538667 58.538667 0 0 0-2.218666 82.858666l56.32 59.477334c31.658667 33.408 79.189333 51.754667 133.845333 51.754666 44.458667 0 88.533333-12.16 118.528-31.914666l185.898667 185.898666a29.312 29.312 0 0 0 41.429333-41.386666l-187.733333-187.776a231.850667 231.850667 0 0 0 27.52-90.282667c5.802667-59.136-9.557333-111.786667-43.52-147.712l-56.32-59.477333a58.581333 58.581333 0 0 0-40.96-18.304h-1.877334c-14.976 0-29.44 5.76-40.277333 16.085333l-28.8 27.392-38.442667-39.253333 26.325334-24.96a58.624 58.624 0 0 0 2.176-82.816l-56.32-59.52c-31.616-33.365333-79.146667-51.754667-133.802667-51.754667-44.458667 0-88.533333 12.202667-118.528 31.957333L137.258667 82.858667a29.312 29.312 0 0 0-41.429334 41.386666L228.864 257.28z m427.349333 198.4l56.32 59.52c49.92 52.608 26.197333 156.16-5.12 185.728-16.725333 15.872-54.954667 28.501333-94.421333 28.501333-33.706667 0-68.266667-9.173333-91.306667-33.450666l-56.32-59.52 190.848-180.778667z m-104.96 18.773333l-64.853333 61.44-38.4-39.338666 64.853333-61.696 38.4 39.594666zM292.693333 269.141333c16.597333-15.872 54.784-28.501333 94.293334-28.501333 33.664 0 68.266667 9.216 91.264 33.493333l56.32 59.477334-190.677334 180.736-56.32-59.477334C237.653333 402.218667 261.376 298.666667 292.693333 269.141333z'
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const LARGE_VIEWBOX_ICONS: Record<string, string> = {
|
||||
'trending-up': '0 0 1024 1024',
|
||||
'trending-down': '0 0 1024 1024',
|
||||
'database': '0 0 1024 1024',
|
||||
'test-connect': '0 0 1024 1024',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
connectionLimit: number;
|
||||
acquireTimeout: number;
|
||||
ssl: boolean;
|
||||
}
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ConnectionTestResult {
|
||||
connected: boolean;
|
||||
version?: string;
|
||||
serverInfo?: string;
|
||||
connectionId?: number;
|
||||
uptime?: number;
|
||||
}
|
||||
|
||||
interface MigrationResult {
|
||||
success: boolean;
|
||||
direction: 'json-to-db' | 'db-to-json';
|
||||
recordsProcessed: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class DatabaseService {
|
||||
private baseURL = '/api/database';
|
||||
|
||||
/**
|
||||
* 测试数据库连接
|
||||
*/
|
||||
async testConnection(config: DatabaseConfig): Promise<ApiResponse<ConnectionTestResult>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/test-connection`, config);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '连接测试失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据库配置
|
||||
*/
|
||||
async saveConfig(config: DatabaseConfig): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/config`, config);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '保存配置失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库配置
|
||||
*/
|
||||
async getConfig(): Promise<ApiResponse<DatabaseConfig>> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/config`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '获取配置失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库连接状态
|
||||
*/
|
||||
async getConnectionStatus(): Promise<ApiResponse<{ connected: boolean; config?: Partial<DatabaseConfig> }>> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/status`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '获取连接状态失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库表结构
|
||||
*/
|
||||
async initializeTables(): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/initialize`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '初始化数据库失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据迁移
|
||||
*/
|
||||
async migrateData(direction: 'json-to-db' | 'db-to-json', overwrite: boolean = true): Promise<ApiResponse<MigrationResult>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/migrate/${direction}`, {
|
||||
overwrite
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '数据迁移失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表统计信息(数据库版本)
|
||||
*/
|
||||
async getRegistryStats(): Promise<ApiResponse<{
|
||||
artistCount: number;
|
||||
artworkCount: number;
|
||||
version: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/registry/stats`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '获取统计信息失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出数据库注册表数据
|
||||
*/
|
||||
async exportRegistry(): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/registry/export`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '导出数据失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据到数据库注册表
|
||||
*/
|
||||
async importRegistry(data: any): Promise<ApiResponse<{
|
||||
addedArtists: number;
|
||||
addedArtworks: number;
|
||||
skippedArtworks: number;
|
||||
totalArtists: number;
|
||||
totalArtworks: number;
|
||||
}>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/registry/import`, data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '导入数据失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件系统重建数据库注册表
|
||||
*/
|
||||
async rebuildRegistry(): Promise<ApiResponse<{ taskId: string }>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/registry/rebuild`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '重建注册表失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理数据库注册表
|
||||
*/
|
||||
async cleanupRegistry(): Promise<ApiResponse<{
|
||||
removedArtists: number;
|
||||
removedArtworks: number;
|
||||
remainingArtists: number;
|
||||
remainingArtworks: number;
|
||||
}>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/registry/cleanup`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '清理注册表失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开数据库连接
|
||||
*/
|
||||
async disconnect(): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}/disconnect`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || '断开连接失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseService();
|
||||
export type { DatabaseConfig, ApiResponse, ConnectionTestResult, MigrationResult };
|
||||
+51
-31
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import downloadService from '@/services/download';
|
||||
import databaseService from '@/services/database';
|
||||
import { getApiBaseUrl } from '@/services/api';
|
||||
|
||||
export interface RegistryStats {
|
||||
@@ -12,6 +13,7 @@ export interface RegistryStats {
|
||||
export interface RegistryConfig {
|
||||
useRegistryCheck: boolean;
|
||||
fallbackToScan: boolean;
|
||||
storageMode?: 'json' | 'database';
|
||||
}
|
||||
|
||||
export const useRegistryStore = defineStore('registry', () => {
|
||||
@@ -19,18 +21,25 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
const stats = ref<RegistryStats | null>(null);
|
||||
const config = ref<RegistryConfig>({
|
||||
useRegistryCheck: true,
|
||||
fallbackToScan: false
|
||||
fallbackToScan: false,
|
||||
storageMode: 'json'
|
||||
});
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// 获取注册表统计信息
|
||||
const fetchStats = async () => {
|
||||
const fetchStats = async (useDatabase = false) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.getRegistryStats();
|
||||
let response;
|
||||
if (useDatabase) {
|
||||
response = await databaseService.getRegistryStats();
|
||||
} else {
|
||||
response = await downloadService.getRegistryStats();
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
// 映射API响应数据到组件期望的格式
|
||||
stats.value = {
|
||||
@@ -50,27 +59,23 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
};
|
||||
|
||||
// 导出注册表
|
||||
const exportRegistry = async () => {
|
||||
const exportRegistry = async (useDatabase = false) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.exportRegistry();
|
||||
let response;
|
||||
if (useDatabase) {
|
||||
response = await databaseService.exportRegistry();
|
||||
} else {
|
||||
response = await downloadService.exportRegistry();
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([JSON.stringify(response, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `download-registry-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true };
|
||||
if (response.success) {
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '导出注册表失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '导出注册表失败';
|
||||
console.error('导出注册表失败:', err);
|
||||
@@ -81,18 +86,21 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
};
|
||||
|
||||
// 导入注册表
|
||||
const importRegistry = async (file: File) => {
|
||||
const importRegistry = async (data: any, useDatabase = false) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const text = await file.text();
|
||||
const registryData = JSON.parse(text);
|
||||
let response;
|
||||
if (useDatabase) {
|
||||
response = await databaseService.importRegistry(data);
|
||||
} else {
|
||||
response = await downloadService.importRegistry(data);
|
||||
}
|
||||
|
||||
const response = await downloadService.importRegistry(registryData);
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
await fetchStats(useDatabase);
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '导入注册表失败');
|
||||
@@ -107,15 +115,21 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
};
|
||||
|
||||
// 重建注册表
|
||||
const rebuildRegistry = async () => {
|
||||
const rebuildRegistry = async (useDatabase = false) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.rebuildRegistry();
|
||||
let response;
|
||||
if (useDatabase) {
|
||||
response = await databaseService.rebuildRegistry();
|
||||
} else {
|
||||
response = await downloadService.rebuildRegistry();
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
await fetchStats(useDatabase);
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '重建注册表失败');
|
||||
@@ -130,15 +144,21 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
};
|
||||
|
||||
// 清理注册表
|
||||
const cleanupRegistry = async () => {
|
||||
const cleanupRegistry = async (useDatabase = false) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.cleanupRegistry();
|
||||
let response;
|
||||
if (useDatabase) {
|
||||
response = await databaseService.cleanupRegistry();
|
||||
} else {
|
||||
response = await downloadService.cleanupRegistry();
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
await fetchStats(useDatabase);
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '清理注册表失败');
|
||||
@@ -159,7 +179,7 @@ export const useRegistryStore = defineStore('registry', () => {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
config.value = result.data;
|
||||
config.value = { ...config.value, ...result.data };
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(result.error || '获取配置失败');
|
||||
|
||||
+21
-4
@@ -21,6 +21,23 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
const isChecking = ref(false)
|
||||
const lastCheckTime = ref<Date | null>(null)
|
||||
|
||||
// 从localStorage加载上次检查时间
|
||||
const loadLastCheckTime = () => {
|
||||
const stored = localStorage.getItem('pixiv-manager-last-update-check')
|
||||
if (stored) {
|
||||
lastCheckTime.value = new Date(stored)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存检查时间到localStorage
|
||||
const saveLastCheckTime = (time: Date) => {
|
||||
lastCheckTime.value = time
|
||||
localStorage.setItem('pixiv-manager-last-update-check', time.toISOString())
|
||||
}
|
||||
|
||||
// 初始化时加载上次检查时间
|
||||
loadLastCheckTime()
|
||||
|
||||
// 检查更新
|
||||
const checkUpdate = async (silent = false): Promise<UpdateInfo | null> => {
|
||||
if (isChecking.value) return null
|
||||
@@ -33,7 +50,7 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
|
||||
if (result.success) {
|
||||
updateInfo.value = result.data
|
||||
lastCheckTime.value = new Date()
|
||||
saveLastCheckTime(new Date())
|
||||
return result.data
|
||||
} else {
|
||||
if (!silent) {
|
||||
@@ -53,8 +70,8 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
|
||||
// 自动检查更新(登录后调用)
|
||||
const autoCheckUpdate = async () => {
|
||||
// 如果距离上次检查不足1小时,跳过
|
||||
if (lastCheckTime.value && Date.now() - lastCheckTime.value.getTime() < 60 * 60 * 1000) {
|
||||
// 如果距离上次检查不足24小时(1天),跳过
|
||||
if (lastCheckTime.value && Date.now() - lastCheckTime.value.getTime() < 24 * 60 * 60 * 1000) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,4 +106,4 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
autoCheckUpdate,
|
||||
getCurrentVersion
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user