重做谈话审批表并且修改测试代码
This commit is contained in:
parent
557c9ae351
commit
fb7fb985ad
340
app.py
340
app.py
@ -9,9 +9,13 @@ import pymysql
|
||||
import json
|
||||
import tempfile
|
||||
import zipfile
|
||||
import re
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from flask import send_file
|
||||
from docx import Document
|
||||
from minio import Minio
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from services.ai_service import AIService
|
||||
from services.field_service import FieldService
|
||||
@ -1680,6 +1684,342 @@ def restore_database():
|
||||
return error_response(500, f"恢复数据库失败: {str(e)}")
|
||||
|
||||
|
||||
def get_minio_client():
|
||||
"""获取 MinIO 客户端"""
|
||||
minio_endpoint = os.getenv('MINIO_ENDPOINT')
|
||||
minio_access_key = os.getenv('MINIO_ACCESS_KEY')
|
||||
minio_secret_key = os.getenv('MINIO_SECRET_KEY')
|
||||
minio_secure = os.getenv('MINIO_SECURE', 'false').lower() == 'true'
|
||||
|
||||
if not all([minio_endpoint, minio_access_key, minio_secret_key]):
|
||||
raise ValueError("MinIO配置不完整")
|
||||
|
||||
return Minio(
|
||||
minio_endpoint,
|
||||
access_key=minio_access_key,
|
||||
secret_key=minio_secret_key,
|
||||
secure=minio_secure
|
||||
)
|
||||
|
||||
|
||||
def extract_placeholders_from_docx(file_path: str) -> list:
|
||||
"""
|
||||
从docx文件中提取所有占位符
|
||||
|
||||
Args:
|
||||
file_path: docx文件路径
|
||||
|
||||
Returns:
|
||||
占位符列表,格式: ['field_code1', 'field_code2', ...]
|
||||
"""
|
||||
placeholders = set()
|
||||
pattern = r'\{\{([^}]+)\}\}' # 匹配 {{field_code}} 格式
|
||||
|
||||
try:
|
||||
doc = Document(file_path)
|
||||
|
||||
# 从段落中提取占位符
|
||||
for paragraph in doc.paragraphs:
|
||||
text = paragraph.text
|
||||
matches = re.findall(pattern, text)
|
||||
for match in matches:
|
||||
cleaned = match.strip()
|
||||
# 过滤掉不完整的占位符(包含 { 或 } 的)
|
||||
if cleaned and '{' not in cleaned and '}' not in cleaned:
|
||||
placeholders.add(cleaned)
|
||||
|
||||
# 从表格中提取占位符
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for paragraph in cell.paragraphs:
|
||||
text = paragraph.text
|
||||
matches = re.findall(pattern, text)
|
||||
for match in matches:
|
||||
cleaned = match.strip()
|
||||
# 过滤掉不完整的占位符(包含 { 或 } 的)
|
||||
if cleaned and '{' not in cleaned and '}' not in cleaned:
|
||||
placeholders.add(cleaned)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"读取文件失败: {str(e)}")
|
||||
|
||||
return sorted(list(placeholders))
|
||||
|
||||
|
||||
@app.route('/api/template/upload', methods=['POST'])
|
||||
def upload_template():
|
||||
"""
|
||||
上传新模板文件
|
||||
1. 接收上传的 Word 文档
|
||||
2. 提取占位符
|
||||
3. 匹配数据库中的字段
|
||||
4. 返回未匹配的占位符列表,等待用户输入字段名称
|
||||
"""
|
||||
try:
|
||||
# 检查是否有文件
|
||||
if 'file' not in request.files:
|
||||
return error_response(400, "未找到上传的文件")
|
||||
|
||||
file = request.files['file']
|
||||
tenant_id = request.form.get('tenant_id', type=int)
|
||||
|
||||
if not tenant_id:
|
||||
return error_response(400, "缺少 tenant_id 参数")
|
||||
|
||||
if file.filename == '':
|
||||
return error_response(400, "文件名为空")
|
||||
|
||||
# 检查文件扩展名
|
||||
if not file.filename.lower().endswith('.docx'):
|
||||
return error_response(400, "只支持 .docx 格式的文件")
|
||||
|
||||
# 保存临时文件
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file_path = os.path.join(temp_dir, secure_filename(file.filename))
|
||||
file.save(temp_file_path)
|
||||
|
||||
try:
|
||||
# 提取占位符
|
||||
placeholders = extract_placeholders_from_docx(temp_file_path)
|
||||
|
||||
# 查询数据库中的字段
|
||||
conn = pymysql.connect(
|
||||
host=os.getenv('DB_HOST'),
|
||||
port=int(os.getenv('DB_PORT', 3306)),
|
||||
user=os.getenv('DB_USER'),
|
||||
password=os.getenv('DB_PASSWORD'),
|
||||
database=os.getenv('DB_NAME'),
|
||||
charset='utf8mb4'
|
||||
)
|
||||
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
# 查询所有字段
|
||||
cursor.execute("""
|
||||
SELECT id, name, filed_code, field_type, state
|
||||
FROM f_polic_field
|
||||
WHERE tenant_id = %s
|
||||
""", (tenant_id,))
|
||||
|
||||
fields = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 构建字段映射
|
||||
field_map = {}
|
||||
for field in fields:
|
||||
state = field['state']
|
||||
if isinstance(state, bytes):
|
||||
state = int.from_bytes(state, byteorder='big') if len(state) == 1 else 1
|
||||
|
||||
if state == 1: # 只使用启用的字段
|
||||
field_map[field['filed_code']] = {
|
||||
'id': field['id'],
|
||||
'name': field['name'],
|
||||
'field_type': field['field_type']
|
||||
}
|
||||
|
||||
# 匹配占位符
|
||||
matched_fields = []
|
||||
unmatched_placeholders = []
|
||||
|
||||
for placeholder in placeholders:
|
||||
if placeholder in field_map:
|
||||
matched_fields.append({
|
||||
'placeholder': placeholder,
|
||||
'field_id': field_map[placeholder]['id'],
|
||||
'field_name': field_map[placeholder]['name'],
|
||||
'field_type': field_map[placeholder]['field_type']
|
||||
})
|
||||
else:
|
||||
unmatched_placeholders.append(placeholder)
|
||||
|
||||
# 返回结果
|
||||
return success_response({
|
||||
'filename': file.filename,
|
||||
'placeholders': placeholders,
|
||||
'matched_fields': matched_fields,
|
||||
'unmatched_placeholders': unmatched_placeholders,
|
||||
'temp_file_path': temp_file_path # 临时保存路径,用于后续保存
|
||||
}, "模板解析成功")
|
||||
|
||||
except Exception as e:
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.remove(temp_file_path)
|
||||
os.rmdir(temp_dir)
|
||||
except:
|
||||
pass
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
return error_response(500, f"上传模板失败: {str(e)}")
|
||||
|
||||
|
||||
@app.route('/api/template/save', methods=['POST'])
|
||||
def save_template():
|
||||
"""
|
||||
保存模板
|
||||
1. 接收模板信息(文件名、tenant_id、字段关联关系、新字段信息)
|
||||
2. 创建新字段(如果有)
|
||||
3. 上传文件到 MinIO
|
||||
4. 保存模板到数据库
|
||||
5. 保存字段关联关系
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
tenant_id = data.get('tenant_id')
|
||||
filename = data.get('filename')
|
||||
temp_file_path = data.get('temp_file_path')
|
||||
field_relations = data.get('field_relations', []) # [{field_id, field_type}, ...]
|
||||
new_fields = data.get('new_fields', []) # [{placeholder, name, field_type}, ...]
|
||||
template_name = data.get('template_name') # 用户输入的模板名称
|
||||
|
||||
if not all([tenant_id, filename, temp_file_path]):
|
||||
return error_response(400, "缺少必要参数")
|
||||
|
||||
if not os.path.exists(temp_file_path):
|
||||
return error_response(400, "临时文件不存在")
|
||||
|
||||
# 连接数据库
|
||||
conn = pymysql.connect(
|
||||
host=os.getenv('DB_HOST'),
|
||||
port=int(os.getenv('DB_PORT', 3306)),
|
||||
user=os.getenv('DB_USER'),
|
||||
password=os.getenv('DB_PASSWORD'),
|
||||
database=os.getenv('DB_NAME'),
|
||||
charset='utf8mb4'
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
created_by = 655162080928945152 # 默认创建者ID
|
||||
updated_by = 655162080928945152
|
||||
|
||||
try:
|
||||
# 1. 创建新字段(如果有)
|
||||
new_field_map = {} # placeholder -> field_id
|
||||
if new_fields:
|
||||
for new_field in new_fields:
|
||||
placeholder = new_field.get('placeholder')
|
||||
field_name = new_field.get('name')
|
||||
field_type = new_field.get('field_type', 2) # 默认为输出字段
|
||||
|
||||
if not placeholder or not field_name:
|
||||
continue
|
||||
|
||||
# 生成字段ID
|
||||
field_id = generate_id()
|
||||
|
||||
# 插入新字段
|
||||
cursor.execute("""
|
||||
INSERT INTO f_polic_field
|
||||
(id, tenant_id, name, filed_code, field_type, created_time, created_by, updated_time, updated_by, state)
|
||||
VALUES (%s, %s, %s, %s, %s, NOW(), %s, NOW(), %s, 1)
|
||||
""", (field_id, tenant_id, field_name, placeholder, field_type, created_by, updated_by))
|
||||
|
||||
new_field_map[placeholder] = field_id
|
||||
|
||||
# 2. 上传文件到 MinIO
|
||||
minio_client = get_minio_client()
|
||||
bucket_name = os.getenv('MINIO_BUCKET', 'finyx')
|
||||
|
||||
# 确保存储桶存在
|
||||
if not minio_client.bucket_exists(bucket_name):
|
||||
minio_client.make_bucket(bucket_name)
|
||||
|
||||
# 生成 MinIO 路径
|
||||
now = datetime.now()
|
||||
object_name = f'{tenant_id}/TEMPLATE/{now.year}/{now.month:02d}/{filename}'
|
||||
|
||||
minio_client.fput_object(
|
||||
bucket_name,
|
||||
object_name,
|
||||
temp_file_path,
|
||||
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
)
|
||||
|
||||
minio_path = f"/{object_name}"
|
||||
|
||||
# 3. 保存模板到数据库
|
||||
template_id = generate_id()
|
||||
template_name_final = template_name or filename.replace('.docx', '')
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO f_polic_file_config
|
||||
(id, tenant_id, parent_id, name, input_data, file_path, created_time, created_by, updated_time, updated_by, state)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, NOW(), %s, 1)
|
||||
""", (
|
||||
template_id,
|
||||
tenant_id,
|
||||
None, # parent_id
|
||||
template_name_final,
|
||||
json.dumps({'template_name': template_name_final}, ensure_ascii=False),
|
||||
minio_path,
|
||||
created_by,
|
||||
updated_by
|
||||
))
|
||||
|
||||
# 4. 保存字段关联关系
|
||||
all_field_ids = []
|
||||
|
||||
# 添加用户选择的字段关联
|
||||
for relation in field_relations:
|
||||
field_id = relation.get('field_id')
|
||||
if field_id:
|
||||
all_field_ids.append(field_id)
|
||||
|
||||
# 添加新创建的字段关联
|
||||
for placeholder, field_id in new_field_map.items():
|
||||
if field_id not in all_field_ids:
|
||||
all_field_ids.append(field_id)
|
||||
|
||||
# 添加必需的输入字段
|
||||
cursor.execute("""
|
||||
SELECT id FROM f_polic_field
|
||||
WHERE tenant_id = %s AND filed_code IN ('clue_info', 'target_basic_info_clue') AND state = 1
|
||||
""", (tenant_id,))
|
||||
required_fields = cursor.fetchall()
|
||||
for row in required_fields:
|
||||
if row[0] not in all_field_ids:
|
||||
all_field_ids.append(row[0])
|
||||
|
||||
# 插入关联关系
|
||||
for field_id in all_field_ids:
|
||||
cursor.execute("""
|
||||
INSERT INTO f_polic_file_field
|
||||
(tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
|
||||
VALUES (%s, %s, %s, NOW(), %s, NOW(), %s, 1)
|
||||
""", (tenant_id, template_id, field_id, created_by, updated_by))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.remove(temp_file_path)
|
||||
os.rmdir(os.path.dirname(temp_file_path))
|
||||
except:
|
||||
pass
|
||||
|
||||
return success_response({
|
||||
'template_id': template_id,
|
||||
'template_name': template_name_final,
|
||||
'file_path': minio_path,
|
||||
'field_count': len(all_field_ids)
|
||||
}, "模板保存成功")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise e
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
return error_response(500, f"保存模板失败: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 确保static目录存在
|
||||
os.makedirs('static', exist_ok=True)
|
||||
|
||||
@ -412,6 +412,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传新模板区域 -->
|
||||
<div class="section">
|
||||
<h2>上传新模板</h2>
|
||||
<div class="form-group">
|
||||
<label>上传 Word 模板文件:</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="file" id="templateFileInput" accept=".docx" style="flex: 1; padding: 8px;">
|
||||
<button class="btn btn-primary" onclick="uploadTemplate()" id="uploadBtn" disabled>上传并解析</button>
|
||||
</div>
|
||||
<small style="color: #666; margin-top: 5px; display: block;">
|
||||
支持 .docx 格式,系统将自动扫描占位符并匹配字段
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- 上传结果展示区域 -->
|
||||
<div id="uploadResultArea" style="display: none; margin-top: 20px;">
|
||||
<div id="uploadResultContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段管理区域 -->
|
||||
<div class="section" id="fieldManagementSection" style="display: none;">
|
||||
<h2>字段管理</h2>
|
||||
@ -586,6 +606,23 @@
|
||||
console.warn('清除缓存失败:', e);
|
||||
}
|
||||
|
||||
// 监听文件选择和租户选择
|
||||
const fileInput = document.getElementById('templateFileInput');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = !this.files.length || !currentTenantId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听租户选择变化,更新上传按钮状态
|
||||
const tenantSelect = document.getElementById('tenantSelect');
|
||||
if (tenantSelect) {
|
||||
// 注意:这里会在 loadTenantIds 中重新设置 onchange,所以这里只设置初始状态
|
||||
}
|
||||
|
||||
loadTenantIds();
|
||||
};
|
||||
|
||||
@ -1419,6 +1456,347 @@
|
||||
|
||||
// 初始化搜索功能
|
||||
setupSearch();
|
||||
// 上传模板文件
|
||||
async function uploadTemplate() {
|
||||
const fileInput = document.getElementById('templateFileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
showMessage('请选择要上传的文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTenantId) {
|
||||
showMessage('请先选择租户ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.name.toLowerCase().endsWith('.docx')) {
|
||||
showMessage('只支持 .docx 格式的文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('tenant_id', currentTenantId);
|
||||
|
||||
try {
|
||||
showMessage('正在上传并解析模板...', 'info');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = '上传中...';
|
||||
|
||||
const response = await fetch('/api/template/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.isSuccess) {
|
||||
displayUploadResult(result.data);
|
||||
showMessage('模板解析成功', 'success');
|
||||
} else {
|
||||
showMessage('上传失败: ' + result.errorMsg, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
showMessage('上传失败: ' + error.message, 'error');
|
||||
} finally {
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = '上传并解析';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示上传结果
|
||||
function displayUploadResult(data) {
|
||||
const resultArea = document.getElementById('uploadResultArea');
|
||||
const resultContent = document.getElementById('uploadResultContent');
|
||||
|
||||
let html = `
|
||||
<div style="background: #f0f7ff; padding: 20px; border-radius: 6px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">模板信息</h3>
|
||||
<p><strong>文件名:</strong>${data.filename}</p>
|
||||
<p><strong>占位符总数:</strong>${data.placeholders.length}</p>
|
||||
<p><strong>已匹配字段:</strong>${data.matched_fields.length}</p>
|
||||
<p><strong>未匹配占位符:</strong>${data.unmatched_placeholders.length}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 显示已匹配的字段
|
||||
if (data.matched_fields.length > 0) {
|
||||
html += `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3>已匹配的字段</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background: #f5f5f5;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">占位符</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">字段名称</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">字段类型</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
data.matched_fields.forEach(field => {
|
||||
html += `
|
||||
<tr>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;"><code>${field.placeholder}</code></td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${field.field_name}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${field.field_type === 1 ? '输入字段' : '输出字段'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||
<button class="btn btn-secondary" onclick="removeMatchedField('${field.placeholder}')" style="padding: 5px 10px; font-size: 12px;">移除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 显示未匹配的占位符
|
||||
if (data.unmatched_placeholders.length > 0) {
|
||||
html += `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3>未匹配的占位符(需要创建新字段)</h3>
|
||||
<p style="color: #666; margin-bottom: 10px;">以下占位符在数据库中不存在,请为它们创建新字段:</p>
|
||||
<button class="btn btn-primary" onclick="showUnmatchedFieldsModal(${JSON.stringify(data.unmatched_placeholders).replace(/"/g, '"')})">
|
||||
为未匹配占位符创建字段
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 添加模板名称输入和保存按钮
|
||||
html += `
|
||||
<div style="margin-top: 20px; padding: 20px; background: #fff; border: 1px solid #ddd; border-radius: 6px;">
|
||||
<h3>保存模板</h3>
|
||||
<div class="form-group">
|
||||
<label for="templateNameInput">模板名称:</label>
|
||||
<input type="text" id="templateNameInput" value="${data.filename.replace('.docx', '')}" style="width: 100%; padding: 8px; margin-top: 5px;">
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top: 15px;">
|
||||
<button class="btn btn-primary" onclick="saveUploadedTemplate(${JSON.stringify(data).replace(/"/g, '"')})">保存模板</button>
|
||||
<button class="btn btn-secondary" onclick="cancelUpload()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultContent.innerHTML = html;
|
||||
resultArea.style.display = 'block';
|
||||
|
||||
// 保存上传数据到全局变量
|
||||
window.uploadedTemplateData = data;
|
||||
}
|
||||
|
||||
// 显示未匹配字段输入弹窗
|
||||
function showUnmatchedFieldsModal(placeholders) {
|
||||
let html = `
|
||||
<div class="modal" id="unmatchedFieldsModal" style="display: block;">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>为未匹配占位符创建字段</h2>
|
||||
<span class="close" onclick="closeUnmatchedFieldsModal()">×</span>
|
||||
</div>
|
||||
<div style="max-height: 60vh; overflow-y: auto;">
|
||||
<p style="color: #666; margin-bottom: 15px;">请为以下占位符输入字段名称和类型:</p>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background: #f5f5f5;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">占位符</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">字段名称</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">字段类型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
placeholders.forEach(placeholder => {
|
||||
html += `
|
||||
<tr>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;"><code>${placeholder}</code></td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||
<input type="text" class="new-field-name" data-placeholder="${placeholder}"
|
||||
placeholder="请输入字段名称" style="width: 100%; padding: 5px;" required>
|
||||
</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||
<select class="new-field-type" data-placeholder="${placeholder}" style="width: 100%; padding: 5px;">
|
||||
<option value="2">输出字段</option>
|
||||
<option value="1">输入字段</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top: 20px;">
|
||||
<button class="btn btn-primary" onclick="confirmNewFields()">确认创建</button>
|
||||
<button class="btn btn-secondary" onclick="closeUnmatchedFieldsModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 创建并显示模态框
|
||||
const modal = document.createElement('div');
|
||||
modal.innerHTML = html;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// 确认创建新字段
|
||||
function confirmNewFields() {
|
||||
const nameInputs = document.querySelectorAll('.new-field-name');
|
||||
const typeSelects = document.querySelectorAll('.new-field-type');
|
||||
|
||||
const newFields = [];
|
||||
let hasError = false;
|
||||
|
||||
nameInputs.forEach(input => {
|
||||
const placeholder = input.dataset.placeholder;
|
||||
const name = input.value.trim();
|
||||
const typeSelect = document.querySelector(`.new-field-type[data-placeholder="${placeholder}"]`);
|
||||
const type = typeSelect ? parseInt(typeSelect.value) : 2;
|
||||
|
||||
if (!name) {
|
||||
hasError = true;
|
||||
input.style.borderColor = '#f44336';
|
||||
} else {
|
||||
input.style.borderColor = '#ddd';
|
||||
newFields.push({
|
||||
placeholder: placeholder,
|
||||
name: name,
|
||||
field_type: type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
showMessage('请填写所有字段名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 将新字段添加到上传数据中
|
||||
if (window.uploadedTemplateData) {
|
||||
window.uploadedTemplateData.new_fields = newFields;
|
||||
|
||||
// 更新显示
|
||||
const unmatchedPlaceholders = window.uploadedTemplateData.unmatched_placeholders.filter(
|
||||
p => !newFields.find(f => f.placeholder === p)
|
||||
);
|
||||
window.uploadedTemplateData.unmatched_placeholders = unmatchedPlaceholders;
|
||||
|
||||
// 重新显示结果
|
||||
displayUploadResult(window.uploadedTemplateData);
|
||||
}
|
||||
|
||||
closeUnmatchedFieldsModal();
|
||||
showMessage('新字段信息已保存,请继续保存模板', 'success');
|
||||
}
|
||||
|
||||
// 关闭未匹配字段弹窗
|
||||
function closeUnmatchedFieldsModal() {
|
||||
const modal = document.getElementById('unmatchedFieldsModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存上传的模板
|
||||
async function saveUploadedTemplate(data) {
|
||||
const templateName = document.getElementById('templateNameInput').value.trim();
|
||||
|
||||
if (!templateName) {
|
||||
showMessage('请输入模板名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTenantId) {
|
||||
showMessage('请先选择租户ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建字段关联关系
|
||||
const fieldRelations = data.matched_fields.map(field => ({
|
||||
field_id: field.field_id,
|
||||
field_type: field.field_type
|
||||
}));
|
||||
|
||||
// 添加新字段信息
|
||||
const newFields = data.new_fields || [];
|
||||
|
||||
const saveData = {
|
||||
tenant_id: parseInt(currentTenantId),
|
||||
filename: data.filename,
|
||||
temp_file_path: data.temp_file_path,
|
||||
template_name: templateName,
|
||||
field_relations: fieldRelations,
|
||||
new_fields: newFields
|
||||
};
|
||||
|
||||
try {
|
||||
showMessage('正在保存模板...', 'info');
|
||||
|
||||
const response = await fetch('/api/template/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(saveData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.isSuccess) {
|
||||
showMessage('模板保存成功!', 'success');
|
||||
|
||||
// 清空上传区域
|
||||
cancelUpload();
|
||||
|
||||
// 刷新数据
|
||||
if (currentTenantId) {
|
||||
await loadData();
|
||||
}
|
||||
} else {
|
||||
showMessage('保存失败: ' + result.errorMsg, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
showMessage('保存失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
function cancelUpload() {
|
||||
const resultArea = document.getElementById('uploadResultArea');
|
||||
resultArea.style.display = 'none';
|
||||
|
||||
const fileInput = document.getElementById('templateFileInput');
|
||||
fileInput.value = '';
|
||||
|
||||
window.uploadedTemplateData = null;
|
||||
}
|
||||
|
||||
// 移除已匹配的字段
|
||||
function removeMatchedField(placeholder) {
|
||||
if (window.uploadedTemplateData) {
|
||||
window.uploadedTemplateData.matched_fields = window.uploadedTemplateData.matched_fields.filter(
|
||||
f => f.placeholder !== placeholder
|
||||
);
|
||||
window.uploadedTemplateData.unmatched_placeholders.push(placeholder);
|
||||
displayUploadResult(window.uploadedTemplateData);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
template_finish/2-初核模版/2.谈话审批/走读式谈话审批/2谈话审批表-重新制作表格.docx
Normal file
BIN
template_finish/2-初核模版/2.谈话审批/走读式谈话审批/2谈话审批表-重新制作表格.docx
Normal file
Binary file not shown.
@ -34,7 +34,10 @@ def extract_placeholders_from_docx(file_path: str) -> set:
|
||||
|
||||
matches = re.findall(pattern, text)
|
||||
for match in matches:
|
||||
# 清理占位符:去除首尾空格,并将中间的空格/换行符替换为下划线
|
||||
cleaned = match.strip()
|
||||
# 将中间的空格、换行符、制表符等空白字符替换为下划线
|
||||
cleaned = re.sub(r'\s+', '_', cleaned)
|
||||
# 过滤掉不完整的占位符(包含 { 或 } 的)
|
||||
if cleaned and '{' not in cleaned and '}' not in cleaned:
|
||||
placeholders.add(cleaned)
|
||||
@ -50,7 +53,10 @@ def extract_placeholders_from_docx(file_path: str) -> set:
|
||||
|
||||
matches = re.findall(pattern, cell_text)
|
||||
for match in matches:
|
||||
# 清理占位符:去除首尾空格,并将中间的空格/换行符替换为下划线
|
||||
cleaned = match.strip()
|
||||
# 将中间的空格、换行符、制表符等空白字符替换为下划线
|
||||
cleaned = re.sub(r'\s+', '_', cleaned)
|
||||
# 过滤掉不完整的占位符(包含 { 或 } 的)
|
||||
if cleaned and '{' not in cleaned and '}' not in cleaned:
|
||||
placeholders.add(cleaned)
|
||||
@ -199,9 +205,22 @@ def test_template_replacement(template_path: str, output_dir: str):
|
||||
# 执行替换
|
||||
final_text = full_text
|
||||
for field_code, field_value in field_data.items():
|
||||
placeholder = f"{{{{{field_code}}}}}"
|
||||
replacement_value = str(field_value) if field_value else ''
|
||||
# 尝试多种格式的占位符替换(处理空格问题)
|
||||
# 标准格式
|
||||
placeholder = f"{{{{{field_code}}}}}"
|
||||
final_text = final_text.replace(placeholder, replacement_value)
|
||||
# 带空格的格式(空格替换为下划线后的字段名)
|
||||
placeholder_with_spaces = f"{{{{ {field_code.replace('_', ' ')} }}}}"
|
||||
if placeholder_with_spaces in final_text:
|
||||
final_text = final_text.replace(placeholder_with_spaces, replacement_value)
|
||||
# 正则表达式匹配(处理各种空格情况)
|
||||
placeholder_pattern_variants = [
|
||||
re.compile(re.escape(f"{{{{ {field_code.replace('_', ' ')} }}}}")),
|
||||
re.compile(re.escape(f"{{{{{field_code.replace('_', ' ')}}}}}")),
|
||||
]
|
||||
for variant_pattern in placeholder_pattern_variants:
|
||||
final_text = variant_pattern.sub(replacement_value, final_text)
|
||||
|
||||
# 替换段落文本(保持格式)
|
||||
if len(paragraph.runs) == 1:
|
||||
@ -234,9 +253,22 @@ def test_template_replacement(template_path: str, output_dir: str):
|
||||
# 执行替换
|
||||
final_text = full_text
|
||||
for field_code, field_value in field_data.items():
|
||||
placeholder = f"{{{{{field_code}}}}}"
|
||||
replacement_value = str(field_value) if field_value else ''
|
||||
# 尝试多种格式的占位符替换(处理空格问题)
|
||||
# 标准格式
|
||||
placeholder = f"{{{{{field_code}}}}}"
|
||||
final_text = final_text.replace(placeholder, replacement_value)
|
||||
# 带空格的格式(空格替换为下划线后的字段名)
|
||||
placeholder_with_spaces = f"{{{{ {field_code.replace('_', ' ')} }}}}"
|
||||
if placeholder_with_spaces in final_text:
|
||||
final_text = final_text.replace(placeholder_with_spaces, replacement_value)
|
||||
# 正则表达式匹配(处理各种空格情况)
|
||||
placeholder_pattern_variants = [
|
||||
re.compile(re.escape(f"{{{{ {field_code.replace('_', ' ')} }}}}")),
|
||||
re.compile(re.escape(f"{{{{{field_code.replace('_', ' ')}}}}}")),
|
||||
]
|
||||
for variant_pattern in placeholder_pattern_variants:
|
||||
final_text = variant_pattern.sub(replacement_value, final_text)
|
||||
|
||||
# 替换段落文本
|
||||
if len(paragraph.runs) == 1:
|
||||
@ -303,8 +335,7 @@ def main():
|
||||
project_root = Path(__file__).parent
|
||||
|
||||
# 模板文件路径
|
||||
template1_path = project_root / "template_finish" / "2-初核模版" / "2.谈话审批" / "走读式谈话审批" / "2谈话审批表.docx"
|
||||
template2_path = project_root / "template_finish" / "2-初核模版" / "3.初核结论" / "8-1请示报告卡(初核报告结论) .docx"
|
||||
template_path = project_root / "template_finish" / "2-初核模版" / "2.谈话审批" / "走读式谈话审批" / "2谈话审批表-重新制作表格.docx"
|
||||
|
||||
# 输出目录
|
||||
output_dir = project_root / "output_temp"
|
||||
@ -313,18 +344,14 @@ def main():
|
||||
print("模板占位符识别和替换测试")
|
||||
print("="*80)
|
||||
|
||||
# 测试第一个模板
|
||||
success1 = test_template_replacement(str(template1_path), str(output_dir))
|
||||
|
||||
# 测试第二个模板
|
||||
success2 = test_template_replacement(str(template2_path), str(output_dir))
|
||||
# 测试模板
|
||||
success = test_template_replacement(str(template_path), str(output_dir))
|
||||
|
||||
# 总结
|
||||
print(f"\n{'='*80}")
|
||||
print("测试总结")
|
||||
print(f"{'='*80}")
|
||||
print(f"模板1 (2谈话审批表.docx): {'[成功]' if success1 else '[失败]'}")
|
||||
print(f"模板2 (8-1请示报告卡(初核报告结论).docx): {'[成功]' if success2 else '[失败]'}")
|
||||
print(f"模板 (2谈话审批表-重新制作表格.docx): {'[成功]' if success else '[失败]'}")
|
||||
print(f"\n输出目录: {output_dir}")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user