finyx_data_ai/tests/test_parse_document.html
2026-01-11 07:48:19 +08:00

668 lines
30 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>文档解析接口测试 - Finyx Data AI</title>
<link rel="stylesheet" href="test_common.css">
<style>
.upload-area {
border: 2px dashed var(--border-color);
border-radius: var(--radius);
padding: 40px;
text-align: center;
cursor: pointer;
transition: var(--transition);
background-color: var(--light-color);
}
.upload-area:hover {
border-color: var(--primary-color);
background-color: #e9ecef;
}
.upload-area.dragover {
border-color: var(--primary-color);
background-color: #e3f2fd;
}
.upload-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--text-muted);
}
.upload-text {
font-size: 14px;
color: var(--text-muted);
margin-bottom: 12px;
}
.file-list {
margin-top: 16px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
background: var(--white);
border: 1px solid var(--border-color);
border-radius: var(--radius);
margin-bottom: 8px;
}
.file-icon {
font-size: 24px;
margin-right: 12px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
font-size: 14px;
}
.file-size {
font-size: 12px;
color: var(--text-muted);
}
.file-remove {
color: var(--danger-color);
cursor: pointer;
font-size: 20px;
}
.file-remove:hover {
color: #c62828;
}
.table-preview {
border: 1px solid var(--border-color);
border-radius: var(--radius);
margin-bottom: 16px;
overflow: hidden;
}
.table-preview-header {
background-color: var(--light-color);
padding: 12px 16px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-preview-body {
padding: 16px;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container">
<!-- 页面头部 -->
<header class="page-header">
<h1 class="page-title">📄 文档解析接口测试</h1>
<p class="page-subtitle">
解析上传的数据字典文档Excel/Word/PDF提取表结构信息
</p>
</header>
<div class="content-grid">
<!-- 左侧:文件上传 -->
<div class="col-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">文件上传</h3>
</div>
<div class="card-body">
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽文件到此处</div>
<div class="upload-text" style="font-size: 12px;">
支持 .xlsx, .xls, .doc, .docx, .pdf 格式
</div>
</div>
<input type="file" id="fileInput" accept=".xlsx,.xls,.doc,.docx,.pdf" style="display: none;" multiple>
<div class="form-group">
<label class="form-label" for="projectId">项目ID *</label>
<input type="text" id="projectId" class="form-control" value="project_001" placeholder="输入项目ID">
</div>
<div class="form-group">
<label class="form-label">快速使用虚拟文件</label>
<div class="btn-group">
<button type="button" class="btn btn-info btn-sm" onclick="loadVirtualFile('excel')">Excel 示例</button>
<button type="button" class="btn btn-info btn-sm" onclick="loadVirtualFile('word')">Word 示例</button>
<button type="button" class="btn btn-info btn-sm" onclick="loadVirtualFile('pdf')">PDF 示例</button>
</div>
</div>
<div class="btn-group" style="margin-top: 20px;">
<button type="button" class="btn btn-primary" onclick="parseDocument()">🚀 开始解析</button>
<button type="button" class="btn btn-outline" onclick="resetFiles()">清空</button>
</div>
</div>
</div>
<!-- 文件列表 -->
<div class="card">
<div class="card-header">
<h3 class="card-title">已上传文件</h3>
</div>
<div class="card-body">
<div id="fileList" class="file-list">
<p style="text-align: center; color: var(--text-muted); padding: 20px;">暂无文件</p>
</div>
</div>
</div>
<!-- API 调用信息 -->
<div class="card">
<div class="card-header">
<h3 class="card-title">API 调用信息</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">请求端点</label>
<code style="display: block; padding: 8px; background: var(--light-color); border-radius: var(--radius); font-size: 12px;">
POST /api/v1/inventory/parse-document
</code>
</div>
<div id="requestInfo" class="form-group">
<label class="form-label">请求数据</label>
<pre id="requestJson" style="max-height: 200px; overflow: auto; font-size: 11px; background: var(--light-color); padding: 8px; border-radius: var(--radius);">等待提交...</pre>
</div>
<div id="responseInfo" class="form-group" style="display: none;">
<label class="form-label">响应数据</label>
<pre id="responseJson" style="max-height: 300px; overflow: auto; font-size: 11px; background: var(--light-color); padding: 8px; border-radius: var(--radius);"></pre>
</div>
</div>
</div>
</div>
<!-- 右侧:结果展示 -->
<div class="col-8">
<!-- 加载状态 -->
<div id="loadingArea" style="display: none;"></div>
<!-- 结果区域 -->
<div id="resultArea" style="display: none;">
<!-- 解析统计 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">文件数</div>
<div class="stat-value" id="statFiles">0</div>
</div>
<div class="stat-card">
<div class="stat-label">总表数</div>
<div class="stat-value" id="statTables">0</div>
</div>
<div class="stat-card">
<div class="stat-label">总字段数</div>
<div class="stat-value" id="statFields">0</div>
</div>
<div class="stat-card success">
<div class="stat-label">解析耗时</div>
<div class="stat-value" id="statTime">0s</div>
</div>
</div>
<!-- 文件类型分布 -->
<div class="card">
<div class="card-header">
<h3 class="card-title">📊 文件类型分布</h3>
</div>
<div class="card-body">
<div id="fileTypeChart"></div>
</div>
</div>
<!-- 表识别结果 -->
<div class="card">
<div class="card-header">
<h3 class="card-title">📋 表结构识别结果</h3>
</div>
<div class="card-body">
<div id="tableResults"></div>
</div>
</div>
<!-- 字段类型分布 -->
<div class="card">
<div class="card-header">
<h3 class="card-title">🔧 字段类型分布</h3>
</div>
<div class="card-body">
<div id="fieldTypeChart"></div>
</div>
</div>
</div>
<!-- 空状态 -->
<div id="emptyState" class="card" style="text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📄</div>
<h3 style="margin-bottom: 12px;">等待上传文件</h3>
<p style="color: var(--text-muted);">
点击或拖拽文件到上传区域<br>
支持 Excel、Word、PDF 格式的数据字典文档<br>
也可以使用虚拟文件进行测试
</p>
</div>
</div>
</div>
</div>
<script src="base_test_framework.js"></script>
<script>
// ==================== 文件管理 ====================
let uploadedFiles = [];
let virtualFileType = null;
// 虚拟文件数据
const virtualFiles = {
excel: {
name: '数据字典_零售系统.xlsx',
size: 256000,
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
extension: 'xlsx',
tables: [
{
raw_name: '用户信息表',
display_name: '用户基础信息',
description: '存储用户的注册信息和联系方式',
fields: [
{ raw_name: 'user_id', display_name: '用户ID', type: 'varchar(64)', comment: '用户唯一标识符', is_primary_key: true, is_nullable: false },
{ raw_name: 'phone', display_name: '手机号', type: 'varchar(11)', comment: '用户手机号码', is_primary_key: false, is_nullable: false },
{ raw_name: 'email', display_name: '邮箱', type: 'varchar(100)', comment: '用户电子邮箱', is_primary_key: false, is_nullable: true },
{ raw_name: 'nickname', display_name: '昵称', type: 'varchar(50)', comment: '用户昵称', is_primary_key: false, is_nullable: true },
{ raw_name: 'create_time', display_name: '创建时间', type: 'datetime', comment: '注册时间', is_primary_key: false, is_nullable: false }
],
field_count: 5
},
{
raw_name: '订单信息表',
display_name: '订单信息',
description: '存储订单的基本信息',
fields: [
{ raw_name: 'order_id', display_name: '订单ID', type: 'bigint', comment: '订单唯一标识符', is_primary_key: true, is_nullable: false },
{ raw_name: 'user_id', display_name: '用户ID', type: 'varchar(64)', comment: '下单用户ID', is_primary_key: false, is_nullable: false },
{ raw_name: 'total_amount', display_name: '订单金额', type: 'decimal(10,2)', comment: '订单总金额', is_primary_key: false, is_nullable: false },
{ raw_name: 'status', display_name: '订单状态', type: 'tinyint', comment: '1待支付 2已支付 3已取消', is_primary_key: false, is_nullable: false },
{ raw_name: 'create_time', display_name: '创建时间', type: 'datetime', comment: '订单创建时间', is_primary_key: false, is_nullable: false }
],
field_count: 5
},
{
raw_name: '商品信息表',
display_name: '商品信息',
description: '存储商品的基本信息',
fields: [
{ raw_name: 'product_id', display_name: '商品ID', type: 'bigint', comment: '商品唯一标识符', is_primary_key: true, is_nullable: false },
{ raw_name: 'product_name', display_name: '商品名称', type: 'varchar(200)', comment: '商品名称', is_primary_key: false, is_nullable: false },
{ raw_name: 'price', display_name: '价格', type: 'decimal(10,2)', comment: '商品单价', is_primary_key: false, is_nullable: false },
{ raw_name: 'stock', display_name: '库存', type: 'int', comment: '商品库存数量', is_primary_key: false, is_nullable: false },
{ raw_name: 'category_id', display_name: '分类ID', type: 'int', comment: '商品分类ID', is_primary_key: false, is_nullable: true }
],
field_count: 5
}
]
},
word: {
name: '数据字典_文档版.docx',
size: 128000,
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
extension: 'docx',
tables: [
{
raw_name: '会员表',
display_name: '会员信息',
description: '会员基础信息和会员等级数据',
fields: [
{ raw_name: 'member_id', display_name: '会员编号', type: 'varchar(64)', comment: '会员唯一标识', is_primary_key: true, is_nullable: false },
{ raw_name: 'member_name', display_name: '会员姓名', type: 'varchar(50)', comment: '会员姓名', is_primary_key: false, is_nullable: true },
{ raw_name: 'level', display_name: '会员等级', type: 'tinyint', comment: '1普通 2银卡 3金卡 4钻石', is_primary_key: false, is_nullable: false },
{ raw_name: 'points', display_name: '积分', type: 'int', comment: '会员积分余额', is_primary_key: false, is_nullable: false, default_value: '0' },
{ raw_name: 'register_date', display_name: '注册日期', type: 'date', comment: '会员注册日期', is_primary_key: false, is_nullable: false }
],
field_count: 5
}
]
},
pdf: {
name: '数据字典_PDF版.pdf',
size: 512000,
type: 'application/pdf',
extension: 'pdf',
tables: [
{
raw_name: '交易流水表',
display_name: '交易记录',
description: '记录所有交易流水',
fields: [
{ raw_name: 'trans_id', display_name: '交易流水号', type: 'varchar(32)', comment: '交易唯一标识', is_primary_key: true, is_nullable: false },
{ raw_name: 'account_no', display_name: '账户号码', type: 'varchar(32)', comment: '账户号码', is_primary_key: false, is_nullable: false },
{ raw_name: 'amount', display_name: '交易金额', type: 'decimal(18,2)', comment: '交易金额', is_primary_key: false, is_nullable: false },
{ raw_name: 'trans_type', display_name: '交易类型', type: 'varchar(20)', comment: '交易类型', is_primary_key: false, is_nullable: false },
{ raw_name: 'trans_time', display_name: '交易时间', type: 'datetime', comment: '交易时间', is_primary_key: false, is_nullable: false }
],
field_count: 5
}
]
}
};
// 文件拖拽
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function() {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
document.getElementById('fileInput').addEventListener('change', function(e) {
handleFiles(e.target.files);
});
// 处理文件
function handleFiles(files) {
const validExtensions = ['.xlsx', '.xls', '.doc', '.docx', '.pdf'];
for (const file of files) {
const extension = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(extension)) {
showToast(`文件 ${file.name} 格式不支持`);
continue;
}
uploadedFiles.push(file);
}
renderFileList();
showToast(`已添加 ${files.length} 个文件`);
}
// 渲染文件列表
function renderFileList() {
const container = document.getElementById('fileList');
if (uploadedFiles.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--text-muted); padding: 20px;">暂无文件</p>';
return;
}
let html = '';
uploadedFiles.forEach((file, index) => {
const icon = getFileIcon(file.name);
const size = formatFileSize(file.size);
html += `
<div class="file-item">
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${size}</div>
</div>
<div class="file-remove" onclick="removeFile(${index})">×</div>
</div>
`;
});
container.innerHTML = html;
}
// 获取文件图标
function getFileIcon(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
const icons = {
'xlsx': '📊',
'xls': '📊',
'docx': '📝',
'doc': '📝',
'pdf': '📕'
};
return icons[extension] || '📄';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
// 移除文件
function removeFile(index) {
uploadedFiles.splice(index, 1);
renderFileList();
}
// 清空文件
function resetFiles() {
uploadedFiles = [];
virtualFileType = null;
renderFileList();
document.getElementById('resultArea').style.display = 'none';
document.getElementById('emptyState').style.display = 'block';
document.getElementById('requestInfo').style.display = 'block';
document.getElementById('responseInfo').style.display = 'none';
showToast('已清空文件');
}
// 加载虚拟文件
function loadVirtualFile(type) {
virtualFileType = type;
const virtualFile = virtualFiles[type];
uploadedFiles = [{
name: virtualFile.name,
size: virtualFile.size,
type: virtualFile.type,
isVirtual: true
}];
renderFileList();
showToast(`已加载${{ 'excel': 'Excel', 'word': 'Word', 'pdf': 'PDF' }[type]}虚拟文件`);
}
// 解析文档
async function parseDocument() {
if (uploadedFiles.length === 0) {
showToast('请先上传文件或使用虚拟文件');
return;
}
const projectId = document.getElementById('projectId').value;
if (!projectId) {
showToast('请输入项目ID');
return;
}
// 构建请求数据
const requestData = {
project_id: projectId,
file_type: virtualFileType
};
// 显示请求数据
document.getElementById('requestJson').textContent = JSON.stringify(requestData, null, 2);
document.getElementById('responseInfo').style.display = 'none';
// 显示加载状态
document.getElementById('emptyState').style.display = 'none';
document.getElementById('resultArea').style.display = 'none';
showLoading('loadingArea');
try {
// 模拟API调用
await delay(2000);
// 生成模拟响应
const response = generateMockResponse();
// 显示结果
hideLoading('loadingArea', '');
document.getElementById('resultArea').style.display = 'block';
// 显示响应数据
document.getElementById('responseInfo').style.display = 'block';
document.getElementById('responseJson').textContent = JSON.stringify(response, null, 2);
// 渲染统计信息
renderStatistics(response);
// 渲染图表
renderCharts(response, virtualFileType);
// 渲染表结果
renderTableResults(response.tables);
showSuccess('loadingArea', '✅ 解析完成!');
setTimeout(() => {
document.getElementById('loadingArea').style.display = 'none';
}, 2000);
} catch (error) {
hideLoading('loadingArea', '');
showError('loadingArea', error.message);
}
}
// 生成模拟响应
function generateMockResponse() {
const virtualFile = virtualFiles[virtualFileType];
const tables = virtualFile.tables || [];
let totalFields = 0;
let fileType = {
'excel': 0,
'word': 0,
'pdf': 0
};
fileType[virtualFileType] = 1;
let fieldTypes = {};
tables.forEach(table => {
totalFields += table.fields.length;
table.fields.forEach(field => {
const baseType = field.type.split('(')[0].toLowerCase();
fieldTypes[baseType] = (fieldTypes[baseType] || 0) + 1;
});
});
return {
tables: tables,
total_tables: tables.length,
total_fields: totalFields,
parse_time: (Math.random() * 2 + 0.5).toFixed(2),
file_type: virtualFileType,
fileType: fileType,
fieldTypes: fieldTypes
};
}
// 渲染统计信息
function renderStatistics(response) {
document.getElementById('statFiles').textContent = 1;
document.getElementById('statTables').textContent = response.total_tables;
document.getElementById('statFields').textContent = response.total_fields;
document.getElementById('statTime').textContent = response.parse_time + 's';
}
// 渲染图表
function renderCharts(response, fileType) {
// 文件类型分布
const fileTypeData = Object.entries(response.fileType)
.filter(([_, value]) => value > 0)
.map(([key, value]) => ({
label: key.toUpperCase(),
value: value,
color: key === 'excel' ? '#1cc88a' : (key === 'word' ? '#36b9cc' : '#e74a3b')
}));
renderBarChart('fileTypeChart', fileTypeData, '文件类型分布');
// 字段类型分布
if (Object.keys(response.fieldTypes).length > 0) {
const fieldTypeData = Object.entries(response.fieldTypes)
.map(([key, value]) => ({
label: key.toUpperCase(),
value: value
}));
renderBarChart('fieldTypeChart', fieldTypeData, '字段类型分布');
} else {
document.getElementById('fieldTypeChart').innerHTML = '<p style="text-align: center; color: var(--text-muted);">暂无数据</p>';
}
}
// 渲染表结果
function renderTableResults(tables) {
const container = document.getElementById('tableResults');
let html = '';
tables.forEach((table, index) => {
const primaryKeyFields = table.fields.filter(f => f.is_primary_key);
html += `
<div class="table-preview">
<div class="table-preview-header" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.querySelector('.toggle-icon').textContent = this.nextElementSibling.style.display === 'none' ? '▼' : '▲';">
<div>
<strong style="font-size: 14px;">${table.display_name || table.raw_name}</strong>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
原始名称: ${table.raw_name} | 字段数: ${table.field_count}
${primaryKeyFields.length > 0 ? `<span style="margin-left: 8px; color: #f57c00;">🔑 主键: ${primaryKeyFields.map(f => f.raw_name).join(', ')}</span>` : ''}
</div>
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="table-preview-body">
<p style="margin-bottom: 12px; color: var(--text-muted); font-size: 13px;">${table.description || '无描述'}</p>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr style="background-color: var(--light-color);">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid var(--border-color);">字段名</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid var(--border-color);">显示名</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid var(--border-color);">类型</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid var(--border-color);">注释</th>
<th style="padding: 8px; text-align: center; border-bottom: 2px solid var(--border-color);">主键</th>
<th style="padding: 8px; text-align: center; border-bottom: 2px solid var(--border-color);">可为空</th>
</tr>
</thead>
<tbody>
`;
table.fields.forEach(field => {
html += `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 8px; font-weight: 500;">${field.raw_name}</td>
<td style="padding: 8px;">${field.display_name || '-'}</td>
<td style="padding: 8px; color: var(--info-color);">${field.type}</td>
<td style="padding: 8px; color: var(--text-muted);">${field.comment || '-'}</td>
<td style="padding: 8px; text-align: center;">${field.is_primary_key ? '✓' : ''}</td>
<td style="padding: 8px; text-align: center;">${field.is_nullable ? '✓' : ''}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
</div>
`;
});
container.innerHTML = html;
}
</script>
</body>
</html>