Enhance database management features by adding an API endpoint to drop all tables with double confirmation. Implement configuration file upload and download functionalities, including backup of the existing configuration. Update UI elements for improved user experience, including pagination controls and toast notifications. Adjust Docker and git ignore files to optimize build processes.

This commit is contained in:
2026-01-14 18:27:05 +08:00
parent 866fc1cb06
commit 5b50c9750f
19 changed files with 2424 additions and 166 deletions
+259 -45
View File
@@ -456,7 +456,7 @@ body {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
padding: 0 12px;
height: 32px;
border-radius: var(--td-radius-default);
font-size: 14px;
@@ -465,6 +465,8 @@ body {
color: #e34d59;
cursor: pointer;
transition: all var(--td-transition);
white-space: nowrap;
flex-shrink: 0;
}
.restart-btn:hover {
@@ -481,9 +483,10 @@ body {
transform: scale(0.95);
}
.restart-btn span {
font-size: 14px;
line-height: 1;
.restart-btn svg {
width: 16px;
height: 16px;
display: block;
}
.page-body {
@@ -655,6 +658,7 @@ body {
border: 1px solid var(--td-border-color-light);
border-radius: var(--td-radius-default);
background: var(--td-bg-color-container);
margin-top: 20px;
}
.file-item {
@@ -1178,7 +1182,7 @@ body {
.row-count {
font-size: 12px;
color: var(--td-text-color-placeholder);
color: var(--td-text-color-secondary);
background: var(--td-bg-color-component);
padding: 2px 8px;
border-radius: 10px;
@@ -1190,26 +1194,6 @@ body {
gap: 8px;
}
.search-input {
padding: 6px 12px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color);
border-radius: var(--td-radius-default);
color: var(--td-text-color-primary);
font-size: 13px;
width: 180px;
transition: all var(--td-transition);
outline: none;
}
.search-input::placeholder {
color: var(--td-text-color-placeholder);
}
.search-input:focus {
border-color: var(--td-brand-color);
box-shadow: 0 0 0 2px var(--td-brand-color-focus);
}
.data-table-container {
flex: 1;
@@ -1251,19 +1235,128 @@ body {
.pagination {
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
gap: 12px;
gap: 16px;
padding: 12px 16px;
border-top: 1px solid var(--td-border-color-light);
background: var(--td-bg-color-secondarycontainer);
}
#pageInfo {
.pagination-left {
display: flex;
align-items: center;
gap: 12px;
}
.pagination-total {
font-size: 13px;
color: var(--td-text-color-secondary);
}
.pagination-size-select {
padding: 4px 8px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color);
border-radius: var(--td-radius-default);
color: var(--td-text-color-primary);
font-size: 13px;
cursor: pointer;
outline: none;
transition: all var(--td-transition);
}
.pagination-size-select:hover {
border-color: var(--td-brand-color);
}
.pagination-size-select:focus {
border-color: var(--td-brand-color);
box-shadow: 0 0 0 2px var(--td-brand-color-focus);
}
.pagination-center {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-btn {
min-width: 32px;
height: 32px;
padding: 0 8px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color);
border-radius: var(--td-radius-default);
color: var(--td-text-color-primary);
font-size: 14px;
cursor: pointer;
transition: all var(--td-transition);
display: inline-flex;
align-items: center;
justify-content: center;
outline: none;
}
.pagination-btn:hover:not(:disabled) {
border-color: var(--td-brand-color);
color: var(--td-brand-color);
background: var(--td-brand-color-light);
}
.pagination-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination-btn:active:not(:disabled) {
background: var(--td-bg-color-container-active);
}
.pagination-jump {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: var(--td-text-color-secondary);
}
.pagination-input {
width: 50px;
height: 32px;
padding: 0 8px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color);
border-radius: var(--td-radius-default);
color: var(--td-text-color-primary);
font-size: 13px;
text-align: center;
outline: none;
transition: all var(--td-transition);
/* 去除 number input 的调节器 */
-moz-appearance: textfield;
-webkit-appearance: none;
}
.pagination-input::-webkit-outer-spin-button,
.pagination-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pagination-input:hover {
border-color: var(--td-brand-color);
}
.pagination-input:focus {
border-color: var(--td-brand-color);
box-shadow: 0 0 0 2px var(--td-brand-color-focus);
}
.pagination-total-pages {
color: var(--td-text-color-secondary);
}
/* ==================== 设置页面 ==================== */
.settings-page-body {
display: grid;
@@ -1294,6 +1387,41 @@ body {
justify-content: space-between;
align-items: center;
width: 100%;
gap: 12px;
}
.card-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.field-search-input {
padding: 6px 12px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color);
border-radius: var(--td-radius-default);
color: var(--td-text-color-primary);
font-size: 13px;
width: 200px;
transition: all var(--td-transition);
outline: none;
}
.field-search-input::placeholder {
color: var(--td-text-color-placeholder);
}
.field-search-input:focus {
border-color: var(--td-brand-color);
box-shadow: 0 0 0 2px var(--td-brand-color-focus);
}
.card-header-actions {
display: flex;
justify-content: flex-end;
width: 100%;
margin-top: 4px;
}
.field-mapping-card .card-header .form-hint {
@@ -1306,12 +1434,14 @@ body {
color: var(--td-text-color-placeholder);
}
.settings-card .card-body {
padding: 16px 20px;
}
.settings-card .card-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid var(--td-border-color-light);
@@ -1896,55 +2026,139 @@ body {
/* ==================== Toast 通知 ==================== */
.toast-container {
position: fixed;
top: 80px;
right: 16px;
top: 16px;
left: 50%;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
transform: translateX(-50%);
width: auto;
}
.toast {
padding: 12px 16px;
border-radius: var(--td-radius-default);
padding: 10px 16px;
border-radius: 4px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-border-color-light);
box-shadow: var(--td-shadow-2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
gap: 8px;
font-size: 13px;
animation: toastIn 0.3s ease;
min-width: 280px;
min-width: 240px;
max-width: 90vw;
color: var(--td-text-color-primary);
pointer-events: auto;
transition: all 0.3s ease;
transform: translateY(0);
}
.toast-message {
flex: 1;
}
@keyframes toastIn {
from {
transform: translateX(100%);
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateX(0);
transform: translateY(0);
opacity: 1;
}
}
@keyframes toastOut {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-100%);
opacity: 0;
}
}
.toast-icon {
width: 20px;
height: 20px;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-size: 11px;
font-weight: 600;
color: white;
flex-shrink: 0;
}
.toast.success .toast-icon { background: var(--td-success-color); }
.toast.error .toast-icon { background: var(--td-error-color); }
.toast.warning .toast-icon { background: var(--td-warning-color); }
.toast.info .toast-icon { background: var(--td-info-color); }
.toast.success {
background: rgba(0, 200, 83, 0.08);
border-color: rgba(0, 200, 83, 0.25);
}
.toast.success .toast-icon {
background: var(--td-success-color);
}
.toast.error {
background: rgba(245, 63, 63, 0.08);
border-color: rgba(245, 63, 63, 0.25);
}
.toast.error .toast-icon {
background: var(--td-error-color);
}
.toast.warning {
background: rgba(255, 177, 0, 0.08);
border-color: rgba(255, 177, 0, 0.25);
}
.toast.warning .toast-icon {
background: var(--td-warning-color);
}
.toast.info {
background: rgba(0, 122, 255, 0.08);
border-color: rgba(0, 122, 255, 0.25);
}
.toast.info .toast-icon {
background: var(--td-info-color);
}
.toast-close {
width: 18px;
height: 18px;
border: none;
background: transparent;
color: var(--td-text-color-secondary);
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0;
margin-left: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.toast-close:hover {
background: rgba(0, 0, 0, 0.06);
color: var(--td-text-color-primary);
}
.toast-close:active {
background: rgba(0, 0, 0, 0.1);
}
/* ==================== 模态框 ==================== */
.modal {
+63 -24
View File
@@ -65,7 +65,10 @@
<span>🌓</span>
</button>
<button class="restart-btn" id="restartService" title="重启服务">
<span>⏻</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="4.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</header>
@@ -173,28 +176,25 @@
<h1>处理历史</h1>
</div>
<div class="page-header-actions">
<button class="btn btn-sm btn-outline" id="refreshHistory">
<span>🔄</span> 刷新
</button>
<button class="btn btn-sm btn-danger" id="clearHistory">
<span>🗑️</span> 清空历史
</button>
<span class="total-size" style="margin: 0 8px; font-size: 13px; color: var(--td-text-color-secondary);">总占用: <span id="totalHistorySize">计算中...</span></span>
<button class="theme-toggle" id="themeToggle" title="切换主题">
<span>🌓</span>
</button>
<button class="restart-btn" id="restartService" title="重启服务">
<span>⏻</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="4.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</header>
<div class="page-body">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-outline" id="refreshHistory">
<span>🔄</span> 刷新
</button>
<button class="btn btn-danger" id="clearHistory">
<span>🗑️</span> 清空历史
</button>
</div>
<div class="toolbar-right">
<span class="total-size">总占用: <span id="totalHistorySize">计算中...</span></span>
</div>
</div>
<div class="history-list" id="historyList">
<div class="empty-state">
<span class="empty-icon">📭</span>
@@ -214,11 +214,17 @@
<h1>数据管理</h1>
</div>
<div class="page-header-actions">
<button class="btn btn-sm btn-danger" id="dropAllTables" title="删除所有表">
🗑️ 删除全部表
</button>
<button class="theme-toggle" id="themeToggle" title="切换主题">
<span>🌓</span>
</button>
<button class="restart-btn" id="restartService" title="重启服务">
<span>⏻</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="4.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</header>
@@ -250,7 +256,6 @@
<span class="row-count" id="rowCount"></span>
</div>
<div class="toolbar-right">
<input type="text" class="search-input" id="searchInput" placeholder="搜索...">
<button class="btn btn-sm btn-primary" id="downloadTable">📥 导出</button>
<button class="btn btn-sm btn-warning" id="truncateTable">清空</button>
<button class="btn btn-sm btn-danger" id="dropTable">删除</button>
@@ -263,9 +268,27 @@
</div>
</div>
<div class="pagination" id="pagination" style="display: none;">
<button class="btn btn-sm btn-outline" id="prevPage">上一页</button>
<span id="pageInfo">第 1 页</span>
<button class="btn btn-sm btn-outline" id="nextPage">下一页</button>
<div class="pagination-left">
<span class="pagination-total">共 <span id="totalRecords">0</span> 条</span>
<select class="pagination-size-select" id="pageSizeSelect">
<option value="50">50 条/页</option>
<option value="100">100 条/页</option>
<option value="200">200 条/页</option>
<option value="500">500 条/页</option>
</select>
</div>
<div class="pagination-center">
<button class="pagination-btn" id="firstPage" title="首页">«</button>
<button class="pagination-btn" id="prevPage" title="上一页">‹</button>
<div class="pagination-jump">
<span>第</span>
<input type="number" class="pagination-input" id="pageJumpInput" min="1" value="1">
<span>页</span>
<span class="pagination-total-pages">/ <span id="totalPagesDisplay">1</span></span>
</div>
<button class="pagination-btn" id="nextPage" title="下一页">›</button>
<button class="pagination-btn" id="lastPage" title="末页">»</button>
</div>
</div>
</div>
</div>
@@ -294,7 +317,10 @@
<span>🌓</span>
</button>
<button class="restart-btn" id="restartService" title="重启服务">
<span>⏻</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="4.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</header>
@@ -319,11 +345,21 @@
<h1>系统设置</h1>
</div>
<div class="page-header-actions">
<button class="btn btn-sm btn-outline" id="downloadConfig" title="下载配置文件">
📥 下载配置
</button>
<label class="btn btn-sm btn-outline" for="uploadConfig" title="上传配置文件" style="cursor: pointer; margin: 0;">
📤 上传配置
<input type="file" id="uploadConfig" accept=".json" style="display: none;">
</label>
<button class="theme-toggle" id="themeToggle" title="切换主题">
<span>🌓</span>
</button>
<button class="restart-btn" id="restartService" title="重启服务">
<span>⏻</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="4.5" stroke="currentColor" stroke-width="1.5" fill="none"/>
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</header>
@@ -399,8 +435,11 @@
<div class="card settings-card field-mapping-card">
<div class="card-header">
<div class="card-header-top">
<span class="card-title">字段映射配置</span>
<span class="field-count" id="fieldCount">0 个字段</span>
<div class="card-header-left">
<span class="card-title">字段映射配置</span>
<span class="field-count" id="fieldCount">0 个字段</span>
</div>
<input type="text" class="field-search-input" id="fieldSearchInput" placeholder="搜索字段...">
</div>
<p class="form-hint">定义 Excel 列名到数据库字段的映射规则,提取来源匹配任意一个即可</p>
</div>
+261 -61
View File
@@ -52,14 +52,29 @@ function showToast(message, type = 'info') {
toast.innerHTML = `
<span class="toast-icon">${icons[type] || 'i'}</span>
<span>${message}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" aria-label="关闭">×</button>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastIn 0.3s ease reverse';
// 触发重排,确保动画生效
toast.offsetHeight;
// 关闭函数
const closeToast = () => {
if (toast.timeoutId) {
clearTimeout(toast.timeoutId);
}
toast.style.animation = 'toastOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 3000);
};
// 绑定关闭按钮事件
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', closeToast);
// 自动关闭
toast.timeoutId = setTimeout(closeToast, 3000);
}
// 确认对话框
@@ -1242,28 +1257,40 @@ class DatabaseManager {
}
init() {
$('#searchInput').addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.search();
}
});
$('#downloadTable').addEventListener('click', () => this.downloadTable());
$('#truncateTable').addEventListener('click', () => this.truncateTable());
$('#dropTable').addEventListener('click', () => this.dropTable());
$('#dropAllTables').addEventListener('click', () => this.dropAllTables());
$('#prevPage').addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.loadData();
// 分页按钮事件
$('#firstPage').addEventListener('click', () => this.goToPage(1));
$('#prevPage').addEventListener('click', () => this.goToPage(this.currentPage - 1));
$('#nextPage').addEventListener('click', () => this.goToPage(this.currentPage + 1));
$('#lastPage').addEventListener('click', () => this.goToPage(this.totalPages));
// 页面大小选择
$('#pageSizeSelect').addEventListener('change', (e) => {
this.pageSize = parseInt(e.target.value);
this.currentPage = 1;
this.loadData();
});
// 跳转页面输入框
$('#pageJumpInput').addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
const page = parseInt(e.target.value);
if (page >= 1 && page <= this.totalPages) {
this.goToPage(page);
} else {
e.target.value = this.currentPage;
showToast(`页码必须在 1-${this.totalPages} 之间`, 'error');
}
}
});
$('#nextPage').addEventListener('click', () => {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.loadData();
}
// 失去焦点时恢复当前页
$('#pageJumpInput').addEventListener('blur', (e) => {
e.target.value = this.currentPage;
});
window.addEventListener('pagechange', (e) => {
@@ -1391,6 +1418,10 @@ class DatabaseManager {
this.currentTable = tableName;
this.currentPage = 1;
// 重置页面大小为默认值
$('#pageSizeSelect').value = this.pageSize.toString();
$('#pageJumpInput').value = '1';
$('#dbToolbar').style.display = 'flex';
$('#currentTable').textContent = tableName;
$('#pagination').style.display = 'flex';
@@ -1410,47 +1441,43 @@ class DatabaseManager {
}
}
goToPage(page) {
if (page < 1) page = 1;
if (page > this.totalPages) page = this.totalPages;
if (page === this.currentPage) return;
this.currentPage = page;
this.loadData();
}
async loadData() {
const container = $('#dataTableContainer');
container.innerHTML = '<div class="loading">加载中...</div>';
try {
const searchValue = $('#searchInput').value.trim();
let result;
if (searchValue && this.columns.length > 0) {
// 构建筛选条件(只用第一个字段搜索以提高性能)
const filters = {};
if (this.columns[0]) {
filters[this.columns[0]] = searchValue;
}
result = await api('/database/table/query', {
method: 'POST',
body: JSON.stringify({
table_name: this.currentTable,
page: this.currentPage,
page_size: this.pageSize,
filters: filters
})
});
} else {
result = await api('/database/table/data', {
method: 'POST',
body: JSON.stringify({
table_name: this.currentTable,
page: this.currentPage,
page_size: this.pageSize
})
});
}
const result = await api('/database/table/data', {
method: 'POST',
body: JSON.stringify({
table_name: this.currentTable,
page: this.currentPage,
page_size: this.pageSize
})
});
this.totalPages = result.total_pages;
this.renderTable(result.data);
$('#pageInfo').textContent = `第 ${this.currentPage} / ${this.totalPages} 页 (共 ${result.total} 条)`;
// 更新分页信息
$('#totalRecords').textContent = result.total;
$('#totalPagesDisplay').textContent = this.totalPages;
$('#pageJumpInput').value = this.currentPage;
$('#pageJumpInput').max = this.totalPages;
// 更新按钮状态
$('#firstPage').disabled = this.currentPage <= 1;
$('#prevPage').disabled = this.currentPage <= 1;
$('#nextPage').disabled = this.currentPage >= this.totalPages;
$('#lastPage').disabled = this.currentPage >= this.totalPages;
} catch (error) {
container.innerHTML = `<div class="empty-state"><p>加载失败: ${error.message}</p></div>`;
@@ -1460,13 +1487,16 @@ class DatabaseManager {
renderTable(data) {
const container = $('#dataTableContainer');
if (data.length === 0) {
// 使用已保存的字段列表,如果没有则从数据中获取
const columns = this.columns && this.columns.length > 0
? this.columns
: (data.length > 0 ? Object.keys(data[0]) : []);
if (columns.length === 0) {
container.innerHTML = '<div class="empty-state"><p>暂无数据</p></div>';
return;
}
const columns = Object.keys(data[0]);
container.innerHTML = `
<table class="data-table">
<thead>
@@ -1475,7 +1505,15 @@ class DatabaseManager {
</tr>
</thead>
<tbody>
${data.map(row => `
${data.length === 0 ? `
<tr class="empty-row">
<td colspan="${columns.length}" style="text-align: center; padding: 40px; color: var(--td-text-color-placeholder);">
<div class="empty-state" style="padding: 0;">
<p>暂无数据</p>
</div>
</td>
</tr>
` : data.map(row => `
<tr>
${columns.map(col => `<td title="${row[col] || ''}">${row[col] ?? ''}</td>`).join('')}
</tr>
@@ -1485,11 +1523,6 @@ class DatabaseManager {
`;
}
search() {
this.currentPage = 1;
this.loadData();
}
async downloadTable() {
if (!this.currentTable) return;
try {
@@ -1561,6 +1594,59 @@ class DatabaseManager {
showToast(`删除失败: ${error.message}`, 'error');
}
}
async dropAllTables() {
// 首先获取所有表
let tables = [];
try {
const result = await api('/database/tables', { method: 'POST' });
tables = result.tables || [];
} catch (error) {
showToast(`获取表列表失败: ${error.message}`, 'error');
return;
}
if (tables.length === 0) {
showToast('数据库中没有表', 'info');
return;
}
// 第一次确认
const firstConfirm = await showConfirm(
'删除全部表',
`警告:此操作将删除数据库中的所有 ${tables.length} 个表!\n\n表列表:${tables.slice(0, 10).join(', ')}${tables.length > 10 ? '...' : ''}\n\n此操作不可恢复,请谨慎操作!`
);
if (!firstConfirm) return;
// 第二次确认(双重确认,防止误操作)
const secondConfirm = await showConfirm(
'最后确认',
`您确定要删除所有 ${tables.length} 个表吗?\n\n此操作将永久删除所有数据,无法恢复!\n\n请再次确认。`
);
if (!secondConfirm) return;
// 执行删除
try {
const result = await api('/database/table/drop-all', {
method: 'POST'
});
showToast(`已删除 ${result.dropped_count} 个表`, 'success');
// 重置当前状态
this.currentTable = null;
$('#dbToolbar').style.display = 'none';
$('#pagination').style.display = 'none';
$('#dataTableContainer').innerHTML = '<div class="empty-state"><span class="empty-icon">📭</span><p>数据库中没有表</p></div>';
// 重新加载表列表
this.loadTables();
} catch (error) {
showToast(`删除失败: ${error.message}`, 'error');
}
}
}
@@ -1570,6 +1656,7 @@ class SettingsManager {
constructor() {
this.sheetFilters = [];
this.extractFields = [];
this.fieldSearchKeyword = '';
this.init();
}
@@ -1592,6 +1679,16 @@ class SettingsManager {
$('#addFieldMapping').addEventListener('click', () => this.addFieldMapping());
$('#saveFieldMapping').addEventListener('click', () => this.saveFieldMappings());
// 字段搜索
$('#fieldSearchInput').addEventListener('input', (e) => {
this.fieldSearchKeyword = e.target.value.trim().toLowerCase();
this.renderFieldMappings();
});
// 配置管理
$('#downloadConfig').addEventListener('click', () => this.downloadConfig());
$('#uploadConfig').addEventListener('change', (e) => this.uploadConfig(e));
window.addEventListener('pagechange', (e) => {
if (e.detail.page === 'settings') {
this.loadConfig();
@@ -1738,6 +1835,7 @@ class SettingsManager {
renderFieldMappings() {
const container = $('#fieldMappingList');
const countEl = $('#fieldCount');
// 更新字段数量显示
@@ -1759,8 +1857,16 @@ class SettingsManager {
{ value: 'text', label: '长文本' }
];
container.innerHTML = this.extractFields.map((field, index) => `
<div class="field-mapping-item" data-index="${index}">
container.innerHTML = this.extractFields.map((field, index) => {
// 根据搜索关键词决定是否显示
const shouldShow = !this.fieldSearchKeyword || (() => {
const fieldName = (field.Field || '').toLowerCase();
const extractSources = (field.Extract || []).join(' ').toLowerCase();
return fieldName.includes(this.fieldSearchKeyword) || extractSources.includes(this.fieldSearchKeyword);
})();
return `
<div class="field-mapping-item" data-index="${index}" ${!shouldShow ? 'style="display: none;"' : ''}>
<span class="field-mapping-number">${index + 1}</span>
<button class="btn-icon remove-mapping" data-index="${index}" title="删除此映射">✕</button>
<div class="field-mapping-header">
@@ -1803,7 +1909,8 @@ class SettingsManager {
`}
</div>
</div>
`).join('');
`;
}).join('');
// 绑定删除映射事件
container.querySelectorAll('.remove-mapping').forEach(btn => {
@@ -1907,6 +2014,99 @@ class SettingsManager {
showToast(`保存失败: ${error.message}`, 'error');
}
}
async downloadConfig() {
try {
const response = await fetch('/api/config/download', {
method: 'GET'
});
if (!response.ok) {
throw new Error('下载失败');
}
// 获取文件名(从 Content-Disposition 头或使用默认名称)
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'Configure.json';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showToast('配置文件下载成功', 'success');
} catch (error) {
showToast(`下载失败: ${error.message}`, 'error');
}
}
async uploadConfig(event) {
const file = event.target.files[0];
if (!file) return;
// 验证文件类型
if (!file.name.endsWith('.json')) {
showToast('只支持 JSON 格式的配置文件', 'error');
event.target.value = '';
return;
}
// 确认上传
const confirmed = await showConfirm(
'上传配置',
`确定要上传配置文件 "${file.name}" 吗?当前配置将被替换,系统会自动备份原配置。`
);
if (!confirmed) {
event.target.value = '';
return;
}
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/config/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.detail || '上传失败');
}
// 重新加载配置
await this.loadConfig();
let message = '配置文件上传成功';
if (result.backup) {
message += `,原配置已备份为 ${result.backup}`;
}
showToast(message, 'success');
$('#configUpdate').textContent = `更新时间: ${result.update}`;
} catch (error) {
showToast(`上传失败: ${error.message}`, 'error');
} finally {
// 清空文件选择,允许重复选择同一文件
event.target.value = '';
}
}
}