ai-business-write/static/template_field_manager.html

1425 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模板字段关联管理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 24px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #fafafa;
}
.section h2 {
font-size: 18px;
margin-bottom: 15px;
color: #333;
padding-bottom: 10px;
border-bottom: 2px solid #2196F3;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #da190b;
}
.btn-info {
background: #2196F3;
color: white;
}
.btn-info:hover {
background: #0b7dda;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.message {
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.show {
display: block;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.fields-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.field-section {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 20px;
background: #fafafa;
}
.field-section h3 {
font-size: 16px;
margin-bottom: 15px;
color: #333;
padding-bottom: 10px;
border-bottom: 2px solid #4CAF50;
}
.field-section.output h3 {
border-bottom-color: #2196F3;
}
.field-list {
max-height: 500px;
overflow-y: auto;
padding: 10px 0;
}
.field-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin-bottom: 8px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
transition: all 0.2s;
}
.field-item:hover {
background: #f0f0f0;
border-color: #4CAF50;
}
.field-item.checked {
background: #e8f5e9;
border-color: #4CAF50;
}
.field-info {
flex: 1;
cursor: pointer;
}
.field-name {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.field-code {
font-size: 12px;
color: #999;
font-family: 'Courier New', monospace;
}
.field-actions {
display: flex;
gap: 5px;
}
.field-actions button {
padding: 5px 10px;
font-size: 12px;
}
.search-box {
margin-bottom: 15px;
}
.search-box input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f0f7ff;
border-radius: 4px;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2196F3;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #000;
}
.tabs {
display: flex;
border-bottom: 2px solid #e0e0e0;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: none;
font-size: 14px;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab.active {
color: #2196F3;
border-bottom-color: #2196F3;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
table th,
table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
table th {
background: #f5f5f5;
font-weight: 500;
}
table tr:hover {
background: #f9f9f9;
}
</style>
</head>
<body>
<div class="container">
<h1>模板字段关联管理</h1>
<p class="subtitle">维护模板与输入字段、输出字段的关联关系,支持字段的增删改查和数据库备份恢复</p>
<div id="message" class="message"></div>
<!-- Tenant ID 选择区域 -->
<div class="section">
<h2>租户选择</h2>
<div class="form-group">
<label for="tenantSelect">选择租户ID</label>
<div class="btn-group">
<select id="tenantSelect" style="flex: 1; max-width: 300px;">
<option value="">请先查询租户ID...</option>
</select>
<button class="btn btn-info" onclick="loadTenantIds()">查询租户ID</button>
</div>
</div>
</div>
<!-- 字段管理区域 -->
<div class="section" id="fieldManagementSection" style="display: none;">
<h2>字段管理</h2>
<div class="btn-group" style="margin-bottom: 15px;">
<button class="btn btn-primary" onclick="showAddFieldModal()">新增字段</button>
<button class="btn btn-info" onclick="refreshFields()">刷新字段列表</button>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('input')">输入字段</button>
<button class="tab" onclick="switchTab('output')">输出字段</button>
</div>
<div id="inputFieldsTab" class="tab-content active">
<div class="search-box">
<input type="text" id="inputFieldSearch" placeholder="搜索输入字段..." oninput="filterFields('input')">
</div>
<div id="inputFieldsTable"></div>
</div>
<div id="outputFieldsTab" class="tab-content">
<div class="search-box">
<input type="text" id="outputFieldSearch" placeholder="搜索输出字段..." oninput="filterFields('output')">
</div>
<div id="outputFieldsTable"></div>
</div>
</div>
<!-- 模板字段关联区域 -->
<div class="section" id="templateFieldSection" style="display: none;">
<h2>模板字段关联</h2>
<div class="form-group">
<label for="templateSelect">选择模板:</label>
<select id="templateSelect">
<option value="">请选择模板...</option>
</select>
</div>
<div id="stats" class="stats" style="display: none;">
<div class="stat-item">
<div class="stat-value" id="inputCount">0</div>
<div class="stat-label">已选输入字段</div>
</div>
<div class="stat-item">
<div class="stat-value" id="outputCount">0</div>
<div class="stat-label">已选输出字段</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalCount">0</div>
<div class="stat-label">总字段数</div>
</div>
</div>
<div class="fields-container" id="fieldsContainer" style="display: none;">
<div class="field-section input">
<h3>
输入字段
<span class="field-count" id="inputFieldCount">(0)</span>
</h3>
<div class="search-box">
<input type="text" id="inputSearch" placeholder="搜索输入字段...">
</div>
<div class="field-list" id="inputFieldsList"></div>
</div>
<div class="field-section output">
<h3>
输出字段
<span class="field-count" id="outputFieldCount">(0)</span>
</h3>
<div class="search-box">
<input type="text" id="outputSearch" placeholder="搜索输出字段...">
</div>
<div class="field-list" id="outputFieldsList"></div>
</div>
</div>
<div class="btn-group" id="actions" style="display: none; margin-top: 20px;">
<button class="btn btn-secondary" onclick="resetSelection()">重置</button>
<button class="btn btn-primary" onclick="saveRelations()">保存关联关系</button>
</div>
</div>
<!-- 数据库备份恢复区域 -->
<div class="section" id="backupSection" style="display: none;">
<h2>数据库备份与恢复</h2>
<div class="btn-group">
<button class="btn btn-info" onclick="backupDatabase()">备份数据库</button>
<button class="btn btn-secondary" onclick="showRestoreModal()">恢复数据库</button>
</div>
</div>
<div id="loading" class="loading">加载中...</div>
</div>
<!-- 新增/编辑字段模态框 -->
<div id="fieldModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="fieldModalTitle">新增字段</h2>
<span class="close" onclick="closeFieldModal()">&times;</span>
</div>
<form id="fieldForm" onsubmit="saveField(event)">
<input type="hidden" id="fieldId">
<div class="form-group">
<label for="fieldName">字段名称:</label>
<input type="text" id="fieldName" required>
</div>
<div class="form-group">
<label for="fieldCode">字段编码:</label>
<input type="text" id="fieldCode" required>
</div>
<div class="form-group">
<label for="fieldType">字段类型:</label>
<select id="fieldType" required>
<option value="1">输入字段</option>
<option value="2">输出字段</option>
</select>
</div>
<div class="form-group">
<label for="fieldState">状态:</label>
<select id="fieldState">
<option value="1">启用</option>
<option value="0">未启用</option>
</select>
</div>
<div class="btn-group" style="margin-top: 20px;">
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-secondary" onclick="closeFieldModal()">取消</button>
</div>
</form>
</div>
</div>
<!-- 恢复数据库模态框 -->
<div id="restoreModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>恢复数据库</h2>
<span class="close" onclick="closeRestoreModal()">&times;</span>
</div>
<div class="form-group">
<label for="restoreFile">选择备份文件:</label>
<input type="file" id="restoreFile" accept=".json">
</div>
<div class="btn-group" style="margin-top: 20px;">
<button class="btn btn-primary" onclick="restoreDatabase()">恢复</button>
<button class="btn btn-secondary" onclick="closeRestoreModal()">取消</button>
</div>
</div>
</div>
<script>
let currentTenantId = null; // 存储为字符串,避免大整数精度问题
let templates = [];
let inputFields = [];
let outputFields = [];
let relations = {};
let currentTemplateId = null;
let selectedInputFields = new Set();
let selectedOutputFields = new Set();
let allFields = [];
// 页面加载时初始化
window.onload = function() {
// 确保 currentTenantId 初始化为 null
currentTenantId = null;
console.log('页面加载,初始化 currentTenantId 为 null');
// 清除可能存在的 localStorage 缓存
try {
localStorage.removeItem('currentTenantId');
sessionStorage.removeItem('currentTenantId');
} catch (e) {
console.warn('清除缓存失败:', e);
}
loadTenantIds();
};
// 加载租户ID列表
async function loadTenantIds() {
try {
console.log('开始加载租户ID列表...');
const response = await fetch('/api/tenant-ids');
console.log('API响应状态:', response.status);
const result = await response.json();
console.log('API返回结果:', result);
if (result.isSuccess) {
const tenantIds = result.data.tenant_ids || [];
console.log('获取到的租户ID列表:', tenantIds);
const select = document.getElementById('tenantSelect');
// 不保存当前选中的值,每次都重新选择
select.innerHTML = '<option value="">请选择租户ID...</option>';
if (tenantIds.length === 0) {
select.innerHTML = '<option value="">数据库中没有租户ID数据</option>';
showMessage('数据库中没有找到任何租户ID数据', 'error');
currentTenantId = null;
return;
}
tenantIds.forEach(tenantId => {
const option = document.createElement('option');
// tenantId 已经是字符串,直接使用
option.value = tenantId;
option.textContent = tenantId;
select.appendChild(option);
});
// 移除所有旧的事件监听器
const newSelect = select.cloneNode(true);
select.parentNode.replaceChild(newSelect, select);
const freshSelect = document.getElementById('tenantSelect');
// 设置新的事件监听器(使用 onchange 而不是 addEventListener避免重复
freshSelect.onchange = function() {
const tenantId = this.value;
console.log('租户ID选择变化:', tenantId, '之前的值:', currentTenantId);
if (tenantId) {
// 将 tenantId 转换为数字(用于数据库查询)
// 注意:大整数在 JavaScript 中可能会有精度问题,但这里只是用于显示和传递
const oldTenantId = currentTenantId;
currentTenantId = tenantId; // 保持为字符串,在发送请求时再转换
console.log('更新 currentTenantId:', oldTenantId, '->', currentTenantId);
console.log('准备调用 loadDatacurrentTenantId =', currentTenantId);
loadData();
} else {
currentTenantId = null;
hideAllSections();
}
};
showMessage(`成功加载 ${tenantIds.length} 个租户ID`, 'success');
} else {
console.error('API返回错误:', result);
showMessage('加载租户ID列表失败: ' + (result.errorMsg || '未知错误'), 'error');
}
} catch (error) {
console.error('加载租户ID列表异常:', error);
showMessage('加载租户ID列表失败: ' + error.message, 'error');
}
}
// 加载数据
async function loadData() {
console.log('========== loadData 被调用 ==========');
console.log('currentTenantId 的值:', currentTenantId);
console.log('currentTenantId 的类型:', typeof currentTenantId);
if (!currentTenantId) {
console.warn('currentTenantId 为空,无法加载数据');
return;
}
document.getElementById('loading').style.display = 'block';
hideAllSections();
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
// JavaScript 的 Number.MAX_SAFE_INTEGER 是 2^53 - 1 = 9007199254740991
// 615873064429507639 超过了这个值parseInt 会丢失精度
const tenantId = currentTenantId; // 直接使用字符串
console.log('使用的 tenant_id (字符串):', tenantId);
console.log('tenant_id 的类型:', typeof tenantId);
console.log('准备加载数据,使用的 tenant_id:', tenantId);
console.log('API URL 1:', `/api/template-field-relations?tenant_id=${tenantId}`);
console.log('API URL 2:', `/api/field-management/fields?tenant_id=${tenantId}`);
// 同时加载模板字段关联数据和字段管理数据
const [relationsResponse, fieldsResponse] = await Promise.all([
fetch(`/api/template-field-relations?tenant_id=${tenantId}`),
fetch(`/api/field-management/fields?tenant_id=${tenantId}`)
]);
console.log('API 响应状态:', {
relations: relationsResponse.status,
fields: fieldsResponse.status
});
const relationsResult = await relationsResponse.json();
const fieldsResult = await fieldsResponse.json();
console.log('API 返回结果:', {
relations: relationsResult,
fields: fieldsResult
});
if (relationsResult.isSuccess && fieldsResult.isSuccess) {
// 处理模板字段关联数据
templates = relationsResult.data.templates || [];
inputFields = relationsResult.data.input_fields || [];
outputFields = relationsResult.data.output_fields || [];
let rawRelations = relationsResult.data.relations || {};
// 处理字段管理数据(包含所有字段,包括未启用的)
allFields = fieldsResult.data.fields || [];
// 确保 relations 对象的 key 是字符串类型JSON 序列化后 key 是字符串)
relations = {};
for (const [key, value] of Object.entries(rawRelations)) {
// key 可能是字符串或数字,统一转换为字符串
const keyStr = String(key);
relations[keyStr] = value;
}
console.log('========== 加载数据 ==========');
console.log('数据统计:', {
tenant_id: tenantId,
templates: templates.length,
inputFields: inputFields.length,
outputFields: outputFields.length,
allFields: allFields.length,
relationsCount: Object.keys(relations).length
});
if (Object.keys(relations).length > 0) {
const firstKey = Object.keys(relations)[0];
console.log('relations 对象示例:', {
firstKey: firstKey,
firstKeyType: typeof firstKey,
firstValueCount: relations[firstKey].length,
firstValueSample: relations[firstKey].slice(0, 3)
});
}
console.log('==============================');
// 更新模板选择框
populateTemplateSelect();
// 显示所有区域
document.getElementById('fieldManagementSection').style.display = 'block';
document.getElementById('templateFieldSection').style.display = 'block';
document.getElementById('backupSection').style.display = 'block';
// 确保tab正确显示然后渲染字段表格
switchTab('input');
document.getElementById('loading').style.display = 'none';
} else {
const errorMsg = relationsResult.isSuccess ? fieldsResult.errorMsg : relationsResult.errorMsg;
showMessage('加载数据失败: ' + errorMsg, 'error');
console.error('加载数据失败:', {
relationsResult,
fieldsResult
});
document.getElementById('loading').style.display = 'none';
}
} catch (error) {
showMessage('加载数据失败: ' + error.message, 'error');
document.getElementById('loading').style.display = 'none';
}
}
// 隐藏所有区域
function hideAllSections() {
document.getElementById('fieldManagementSection').style.display = 'none';
document.getElementById('templateFieldSection').style.display = 'none';
document.getElementById('backupSection').style.display = 'none';
document.getElementById('fieldsContainer').style.display = 'none';
document.getElementById('actions').style.display = 'none';
document.getElementById('stats').style.display = 'none';
}
// 填充模板选择框
function populateTemplateSelect() {
const select = document.getElementById('templateSelect');
select.innerHTML = '<option value="">请选择模板...</option>';
templates.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = template.name;
select.appendChild(option);
});
select.onchange = function() {
const templateId = parseInt(this.value);
if (templateId) {
loadTemplateFields(templateId);
} else {
hideFields();
}
};
}
// 加载模板字段
function loadTemplateFields(templateId) {
currentTemplateId = templateId;
const template = templates.find(t => t.id === templateId);
// 确保 templateId 是数字类型
const templateIdNum = parseInt(templateId);
// 获取该模板关联的所有字段ID
// relations 对象的 key 在 JSON 序列化后是字符串,所以使用字符串 key 查找
const templateIdStr = String(templateIdNum);
let relatedFieldIdsArray = relations[templateIdStr] || relations[templateIdNum] || [];
// 转换为 Set并确保类型一致都转换为数字进行比较
const relatedFieldIds = new Set(relatedFieldIdsArray.map(id => {
const numId = parseInt(id);
return isNaN(numId) ? null : numId;
}).filter(id => id !== null));
console.log('========== 加载模板字段 ==========');
console.log('模板信息:', {
templateId: templateIdNum,
templateIdStr: templateIdStr,
templateName: template ? template.name : '未知',
relationsKeysCount: Object.keys(relations).length,
relationsKeysSample: Object.keys(relations).slice(0, 5)
});
console.log('关联关系查找:', {
'relations[templateIdStr]': relations[templateIdStr],
'relations[templateIdNum]': relations[templateIdNum],
relatedFieldIdsArray: relatedFieldIdsArray,
relatedFieldIds: Array.from(relatedFieldIds),
relatedFieldIdsCount: relatedFieldIds.size
});
console.log('字段列表:', {
inputFieldsCount: inputFields.length,
outputFieldsCount: outputFields.length,
inputFieldIds: inputFields.slice(0, 3).map(f => ({id: f.id, name: f.name})),
outputFieldIds: outputFields.slice(0, 3).map(f => ({id: f.id, name: f.name}))
});
selectedInputFields = new Set();
selectedOutputFields = new Set();
// 检查输入字段关联
let inputMatchCount = 0;
inputFields.forEach(field => {
const fieldId = parseInt(field.id);
if (!isNaN(fieldId) && relatedFieldIds.has(fieldId)) {
selectedInputFields.add(fieldId);
inputMatchCount++;
console.log(`[输入字段匹配 ${inputMatchCount}]`, field.name, `(ID: ${fieldId})`);
}
});
// 检查输出字段关联
let outputMatchCount = 0;
outputFields.forEach(field => {
const fieldId = parseInt(field.id);
// 确保 fieldId 是有效数字
if (!isNaN(fieldId)) {
// 检查是否在关联字段ID集合中
if (relatedFieldIds.has(fieldId)) {
selectedOutputFields.add(fieldId);
outputMatchCount++;
console.log(`[输出字段匹配 ${outputMatchCount}]`, field.name, `(ID: ${fieldId})`);
} else {
// 调试:检查为什么没有匹配
if (relatedFieldIds.size > 0 && outputMatchCount === 0) {
console.log(`[调试] 字段 ${field.name} (ID: ${fieldId}) 不在关联集合中`);
console.log(`[调试] 关联集合包含: ${Array.from(relatedFieldIds).slice(0, 5)}`);
}
}
} else {
console.warn(`[警告] 字段 ${field.name} 的ID无效: ${field.id}`);
}
});
console.log('最终选中的字段:', {
inputFields: Array.from(selectedInputFields),
outputFields: Array.from(selectedOutputFields),
inputFieldsSize: selectedInputFields.size,
outputFieldsSize: selectedOutputFields.size
});
console.log('====================================');
renderFields();
updateStats();
document.getElementById('fieldsContainer').style.display = 'grid';
document.getElementById('actions').style.display = 'flex';
document.getElementById('stats').style.display = 'flex';
}
// 渲染字段列表
function renderFields() {
renderFieldList('inputFieldsList', inputFields, selectedInputFields, 'input');
renderFieldList('outputFieldsList', outputFields, selectedOutputFields, 'output');
document.getElementById('inputFieldCount').textContent = `(${inputFields.length})`;
document.getElementById('outputFieldCount').textContent = `(${outputFields.length})`;
}
// 渲染单个字段列表
function renderFieldList(containerId, fields, selectedSet, type) {
const container = document.getElementById(containerId);
const searchTerm = type === 'input'
? document.getElementById('inputSearch').value.toLowerCase()
: document.getElementById('outputSearch').value.toLowerCase();
const filteredFields = fields.filter(field => {
return field.name.toLowerCase().includes(searchTerm) ||
field.filed_code.toLowerCase().includes(searchTerm);
});
if (filteredFields.length === 0) {
container.innerHTML = '<div class="empty-state">没有找到匹配的字段</div>';
return;
}
container.innerHTML = filteredFields.map(field => {
// 确保 field.id 和 selectedSet 中的值类型一致(都转换为数字)
const fieldId = parseInt(field.id);
const isChecked = selectedSet.has(fieldId);
return `
<div class="field-item ${isChecked ? 'checked' : ''}" onclick="toggleField(${fieldId}, '${type}')">
<div class="field-info">
<div class="field-name">${escapeHtml(field.name)}</div>
<div class="field-code">${escapeHtml(field.filed_code)}</div>
</div>
<input type="checkbox" ${isChecked ? 'checked' : ''}
onclick="event.stopPropagation(); toggleField(${fieldId}, '${type}')">
</div>
`;
}).join('');
}
// 切换字段选择
function toggleField(fieldId, type) {
// 确保 fieldId 是数字类型
fieldId = parseInt(fieldId);
if (type === 'input') {
if (selectedInputFields.has(fieldId)) {
selectedInputFields.delete(fieldId);
} else {
selectedInputFields.add(fieldId);
}
renderFieldList('inputFieldsList', inputFields, selectedInputFields, 'input');
} else {
if (selectedOutputFields.has(fieldId)) {
selectedOutputFields.delete(fieldId);
} else {
selectedOutputFields.add(fieldId);
}
renderFieldList('outputFieldsList', outputFields, selectedOutputFields, 'output');
}
updateStats();
}
// 更新统计信息
function updateStats() {
document.getElementById('inputCount').textContent = selectedInputFields.size;
document.getElementById('outputCount').textContent = selectedOutputFields.size;
document.getElementById('totalCount').textContent = selectedInputFields.size + selectedOutputFields.size;
}
// 设置搜索功能
function setupSearch() {
document.getElementById('inputSearch').oninput = function() {
renderFieldList('inputFieldsList', inputFields, selectedInputFields, 'input');
};
document.getElementById('outputSearch').oninput = function() {
renderFieldList('outputFieldsList', outputFields, selectedOutputFields, 'output');
};
}
// 重置选择
function resetSelection() {
if (confirm('确定要重置当前选择吗?')) {
selectedInputFields.clear();
selectedOutputFields.clear();
renderFields();
updateStats();
}
}
// 保存关联关系
async function saveRelations() {
if (!currentTemplateId || !currentTenantId) {
showMessage('请先选择租户和模板', 'error');
return;
}
const btn = event.target;
btn.disabled = true;
btn.textContent = '保存中...';
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
const response = await fetch('/api/template-field-relations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tenant_id: tenantId, // 后端会处理字符串到数字的转换
template_id: currentTemplateId,
input_field_ids: Array.from(selectedInputFields),
output_field_ids: Array.from(selectedOutputFields)
})
});
const result = await response.json();
if (result.isSuccess) {
showMessage('保存成功!', 'success');
relations[currentTemplateId] = [
...selectedInputFields,
...selectedOutputFields
];
} else {
showMessage('保存失败: ' + result.errorMsg, 'error');
}
} catch (error) {
showMessage('保存失败: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = '保存关联关系';
}
}
// 隐藏字段区域
function hideFields() {
document.getElementById('fieldsContainer').style.display = 'none';
document.getElementById('actions').style.display = 'none';
document.getElementById('stats').style.display = 'none';
}
// 显示消息
function showMessage(message, type) {
const msgDiv = document.getElementById('message');
msgDiv.textContent = message;
msgDiv.className = 'message ' + type + ' show';
setTimeout(() => {
msgDiv.classList.remove('show');
}, 3000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 字段管理功能
function switchTab(type) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
if (type === 'input') {
const tabs = document.querySelectorAll('.tab');
if (tabs[0]) tabs[0].classList.add('active');
document.getElementById('inputFieldsTab').classList.add('active');
} else {
const tabs = document.querySelectorAll('.tab');
if (tabs[1]) tabs[1].classList.add('active');
document.getElementById('outputFieldsTab').classList.add('active');
}
renderFieldsTable();
}
async function refreshFields() {
if (!currentTenantId) {
return;
}
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
// 同时刷新字段管理数据和模板字段关联数据
const [fieldsResponse, relationsResponse] = await Promise.all([
fetch(`/api/field-management/fields?tenant_id=${tenantId}`),
fetch(`/api/template-field-relations?tenant_id=${tenantId}`)
]);
const fieldsResult = await fieldsResponse.json();
const relationsResult = await relationsResponse.json();
if (fieldsResult.isSuccess && relationsResult.isSuccess) {
// 更新字段管理数据
allFields = fieldsResult.data.fields || [];
// 更新模板字段关联数据
templates = relationsResult.data.templates || [];
inputFields = relationsResult.data.input_fields || [];
outputFields = relationsResult.data.output_fields || [];
relations = relationsResult.data.relations || {};
// 更新模板选择框
populateTemplateSelect();
// 更新字段表格
renderFieldsTable();
// 如果当前选择了模板,重新加载模板字段
if (currentTemplateId) {
loadTemplateFields(currentTemplateId);
}
} else {
const errorMsg = fieldsResult.isSuccess ? relationsResult.errorMsg : fieldsResult.errorMsg;
showMessage('刷新字段列表失败: ' + errorMsg, 'error');
}
} catch (error) {
showMessage('刷新字段列表失败: ' + error.message, 'error');
}
}
function renderFieldsTable() {
const activeTab = document.querySelector('.tab-content.active');
if (!activeTab) {
// 如果没有活动的tab默认显示输入字段tab
const inputTab = document.getElementById('inputFieldsTab');
if (inputTab) {
inputTab.classList.add('active');
const tabs = document.querySelectorAll('.tab');
if (tabs[0]) tabs[0].classList.add('active');
} else {
return;
}
}
const isInput = activeTab ? activeTab.id === 'inputFieldsTab' : true;
// 使用 allFields 而不是 inputFields/outputFields因为 allFields 包含所有字段(包括未启用的)
const fields = allFields.filter(f => f.field_type === (isInput ? 1 : 2));
const searchTerm = (isInput ?
(document.getElementById('inputFieldSearch')?.value || '').toLowerCase() :
(document.getElementById('outputFieldSearch')?.value || '').toLowerCase());
const filteredFields = fields.filter(field => {
const name = (field.name || '').toLowerCase();
const code = (field.filed_code || '').toLowerCase();
return name.includes(searchTerm) || code.includes(searchTerm);
});
const tableId = isInput ? 'inputFieldsTable' : 'outputFieldsTable';
const container = document.getElementById(tableId);
if (!container) {
console.error('找不到容器:', tableId);
return;
}
console.log('渲染字段表格:', {
isInput,
allFieldsCount: allFields.length,
fieldsCount: fields.length,
filteredCount: filteredFields.length
});
if (filteredFields.length === 0) {
if (allFields.length === 0) {
container.innerHTML = '<div class="empty-state">该租户下暂无字段数据</div>';
} else {
container.innerHTML = '<div class="empty-state">没有找到匹配的字段</div>';
}
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>字段名称</th>
<th>字段编码</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${filteredFields.map(field => `
<tr>
<td>${escapeHtml(field.name)}</td>
<td><code>${escapeHtml(field.filed_code)}</code></td>
<td>${(field.state === 1 || field.state === '1') ? '<span style="color: green;">启用</span>' : '<span style="color: red;">未启用</span>'}</td>
<td>
<div class="field-actions">
<button class="btn btn-secondary" onclick="editField(${field.id})">编辑</button>
<button class="btn btn-danger" onclick="deleteField(${field.id})">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function filterFields(type) {
renderFieldsTable();
}
function showAddFieldModal() {
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
document.getElementById('fieldModalTitle').textContent = '新增字段';
document.getElementById('fieldForm').reset();
document.getElementById('fieldId').value = '';
document.getElementById('fieldState').value = '1';
document.getElementById('fieldModal').style.display = 'block';
}
function editField(fieldId) {
const field = allFields.find(f => f.id === fieldId);
if (!field) {
showMessage('字段不存在', 'error');
return;
}
document.getElementById('fieldModalTitle').textContent = '编辑字段';
document.getElementById('fieldId').value = field.id;
document.getElementById('fieldName').value = field.name;
document.getElementById('fieldCode').value = field.filed_code;
document.getElementById('fieldType').value = field.field_type;
// 确保 state 值正确设置(可能是数字或字符串)
document.getElementById('fieldState').value = String(field.state || 1);
document.getElementById('fieldModal').style.display = 'block';
}
function closeFieldModal() {
document.getElementById('fieldModal').style.display = 'none';
}
async function saveField(event) {
event.preventDefault();
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
const fieldId = document.getElementById('fieldId').value;
const isEdit = !!fieldId;
const url = isEdit ? `/api/field-management/fields/${fieldId}` : '/api/field-management/fields';
const method = isEdit ? 'PUT' : 'POST';
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
const data = {
tenant_id: tenantId, // 后端会处理字符串到数字的转换
name: document.getElementById('fieldName').value,
filed_code: document.getElementById('fieldCode').value,
field_type: parseInt(document.getElementById('fieldType').value),
state: parseInt(document.getElementById('fieldState').value)
};
if (isEdit) {
data.tenant_id = tenantId;
}
try {
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.isSuccess) {
showMessage(isEdit ? '字段更新成功!' : '字段创建成功!', 'success');
closeFieldModal();
refreshFields();
loadData(); // 重新加载关联数据
} else {
showMessage((isEdit ? '更新' : '创建') + '字段失败: ' + result.errorMsg, 'error');
}
} catch (error) {
showMessage((isEdit ? '更新' : '创建') + '字段失败: ' + error.message, 'error');
}
}
async function deleteField(fieldId) {
if (!confirm('确定要删除这个字段吗?')) {
return;
}
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
const response = await fetch(`/api/field-management/fields/${fieldId}?tenant_id=${tenantId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.isSuccess) {
showMessage('字段删除成功!', 'success');
refreshFields();
loadData(); // 重新加载关联数据
} else {
showMessage('删除字段失败: ' + result.errorMsg, 'error');
}
} catch (error) {
showMessage('删除字段失败: ' + error.message, 'error');
}
}
// 数据库备份恢复功能
async function backupDatabase() {
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
const response = await fetch('/api/database/backup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tenant_id: tenantId // 后端会处理字符串到数字的转换
})
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `db_backup_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showMessage('数据库备份成功!', 'success');
} else {
const result = await response.json();
showMessage('备份失败: ' + (result.errorMsg || '未知错误'), 'error');
}
} catch (error) {
showMessage('备份失败: ' + error.message, 'error');
}
}
function showRestoreModal() {
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
document.getElementById('restoreFile').value = '';
document.getElementById('restoreModal').style.display = 'block';
}
function closeRestoreModal() {
document.getElementById('restoreModal').style.display = 'none';
}
async function restoreDatabase() {
const fileInput = document.getElementById('restoreFile');
const file = fileInput.files[0];
if (!file) {
showMessage('请选择备份文件', 'error');
return;
}
if (!currentTenantId) {
showMessage('请先选择租户ID', 'error');
return;
}
if (!confirm('确定要恢复数据库吗?这将覆盖当前租户的所有数据!')) {
return;
}
try {
// 不要转换为数字!直接使用字符串,避免大整数精度丢失
const tenantId = currentTenantId; // 直接使用字符串
const formData = new FormData();
formData.append('file', file);
formData.append('tenant_id', tenantId);
const response = await fetch(`/api/database/restore?tenant_id=${tenantId}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.isSuccess) {
showMessage('数据库恢复成功!', 'success');
closeRestoreModal();
loadData(); // 重新加载数据
} else {
showMessage('恢复失败: ' + result.errorMsg, 'error');
}
} catch (error) {
showMessage('恢复失败: ' + error.message, 'error');
}
}
// 初始化搜索功能
setupSearch();
</script>
</body>
</html>