通用技术 记一次测试工具从 V1.0 到 V1.1 的回溯和成果(一)

dun · August 06, 2025 · 729 hits

基于公司(KPI)需要,测试需要有一些 AI 相关产出和落地情况,然后测试自己就开始做工具(造轮子),经过 7 个工作日的努力,写出了一个基于 pdf、图片的测试用例生成(按概率随机)工具。

1.0 版本工原理(能看不好用的 demo):

环境安装配置(python)

  1. //EasyOCR 识别依赖组件 pip install easyocr opencv-python openai
  2. //json 转 Xmind 依赖组件 pip install xmind==1.2.0
  3. //运行 OcrToTestDebug 需要依赖的组件 pip install pytesseract pillow

单机版代码(demo),经费无,服务器要自费,就简单实现个流程

import cv2
import easyocr
import json
import os
import zipfile
import uuid
import numpy as np
from openai import OpenAI
from pathlib import Path

def ocr_image(image_path):
    """使用EasyOCR识别图片中的文字,支持中文路径"""
    # 规范化路径
    image_path = os.path.normpath(image_path)

    # 检查文件是否存在
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"文件不存在: {image_path}")

    # 使用numpy先读取文件,然后再用OpenCV解码,解决中文路径问题
    try:
        img_array = np.fromfile(image_path, dtype=np.uint8)
        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)

        # 检查图像是否成功加载
        if img is None:
            raise ValueError(f"无法解码图像: {image_path}")

        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # 初始化OCR读取器
        reader = easyocr.Reader(['ch_sim', 'en'])
        results = reader.readtext(gray)

        # 提取文本
        text = ""
        for detection in results:
            text += detection[1] + "\n"

        return text
    except Exception as e:
        raise Exception(f"处理图像时出错: {e}, 路径: {image_path}")

def text_to_test_points(text):
    """将OCR文本转换为测试点JSON结构"""
    client = OpenAI(
        # 避免api_key硬编码,从本地读取api_key文件
        api_key=Path('D:/deepseek_api_key.txt').read_text(encoding='utf-8'),
        base_url="https://api.deepseek.com",
    )

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {
                "role": "system",
                "content": "请将OCR识别的文字内容转换为测试点JSON。格式要求:\n"
                          "{\n"
                          "  \"topic\": \"主标题\",\n"
                          "  \"children\": [\n"
                          "    {\n"
                          "      \"topic\": \"子标题1\",\n"
                          "      \"children\": [{\"topic\": \"内容1\"}, {\"topic\": \"内容2\"}]\n"
                          "    },\n"
                          "    {\n"
                          "      \"topic\": \"子标题2\",\n"
                          "      \"children\": [{\"topic\": \"内容3\"}, {\"topic\": \"内容4\"}]\n"
                          "    }\n"
                          "  ]\n"
                          "}"
            },
            {"role": "user", "content": text},
        ],
        response_format={"type": "json_object"},
    )
    return response.choices[0].message.content

def json_to_xmind(json_data, output_file):
    """将JSON测试点转换为XMind文件"""
    # 解析JSON数据
    data = json.loads(json_data)

    # 创建内容数据结构
    content = {
        "id": f"root:{uuid.uuid4().hex}",
        "class": "sheet",
        "title": "测试点列表",
        "rootTopic": {
            "id": f"root-topic:{uuid.uuid4().hex}",
            "class": "topic",
            "title": data["topic"],
            "children": {"attached": []}
        },
        "theme": {
            "id": "theme:1",
            "centralTopic": {}
        }
    }

    # 递归构建主题树
    def build_topics(parent, children_data):
        children = []
        for child in children_data:
            topic = {
                "id": f"topic:{uuid.uuid4().hex}",
                "class": "topic",
                "title": child["topic"]
            }

            if "children" in child and child["children"]:
                topic["children"] = {"attached": []}
                build_topics(topic["children"]["attached"], child["children"])

            children.append(topic)

        parent.extend(children)

    # 构建主题树
    build_topics(content["rootTopic"]["children"]["attached"], data["children"])

    # 创建元数据
    manifest = {
        "file-entries": {
            "content.json": {},
            "metadata.json": {}
        }
    }

    metadata = {
        "creator": {
            "name": "OCR-TestPoints-Generator",
            "version": "1.0"
        },
        "created-time": "",
    }

    # 创建输出目录
    os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)

    # 创建ZIP文件(XMind文件本质上是ZIP文件)
    with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
        # 写入内容JSON
        zipf.writestr('content.json', json.dumps([content], indent=2))
        # 写入元数据
        zipf.writestr('metadata.json', json.dumps(metadata, indent=2))
        # 写入清单文件
        zipf.writestr('manifest.json', json.dumps(manifest, indent=2))

    print(f"XMind文件已生成: {os.path.abspath(output_file)}")

def image_to_xmind(image_path, output_path=None):
    """完整处理流程:图片 -> OCR识别 -> 测试点JSON -> XMind文件"""
    # 规范化输入路径
    image_path = os.path.normpath(image_path)

    # 如果未指定输出路径,则基于输入文件名生成
    if output_path is None:
        base_name = os.path.splitext(os.path.basename(image_path))[0]
        output_dir = "output"
        os.makedirs(output_dir, exist_ok=True)
        output_path = os.path.join(output_dir, f"{base_name}_测试点.xmind")

    # 规范化输出路径
    output_path = os.path.normpath(output_path)

    # 1. OCR识别图片
    print(f"正在进行OCR识别...路径: {image_path}")
    text = ocr_image(image_path)
    print(f"识别文字:\n{text[:200]}...")

    # 2. 转换为测试点JSON
    print("正在生成测试点JSON...")
    json_data = text_to_test_points(text)
    print("JSON生成完成")

    # 3. 生成XMind文件
    print(f"正在创建XMind文件...路径: {output_path}")
    json_to_xmind(json_data, output_path)
    return output_path

# 使用示例
if __name__ == "__main__":
    # 示例图片路径 - 支持中文路径
    image_path = r"D:/XXXX.png"

    try:
        # 转换为XMind
        xmind_file = image_to_xmind(image_path)
        print(f"处理完成,XMind文件保存在: {xmind_file}")
    except Exception as e:
        print(f"处理失败: {e}") 

### 1.0 单机 demo 跑通了, 需求更新(采购了 1 台服务器,需要前端访问页面)

实现思路:springBoot+前端页面(不想做 vue 的志愿者,使用很简单的原生页面)

前端页面

  1. 文件上传,按照系统内置 promot 生成测试用例
  2. xmind 文件预览
  3. xmind 文件自定义 promot 生成
  4. 文件下载
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>工具页面</title>
    <link rel="stylesheet" href="css/styles.css">
    <script src="js/main.js"></script>
</head>
<body>
<div class="container-fluid">
    <header>
        <nav>
            <ul>
                <li><a href="#mysql">MySQL查询工具</a></li>
                <li><a href="#xmind">XMind转换工具</a></li>
                <li><a href="#testcase">测试点生成工具</a></li>
            </ul>
        </nav>
    </header>

    <section id="mysql">
        <h2>MySQL查询工具</h2>
        <div class="mysql-tool card">
            <div class="db-row" style="display: flex; gap: 24px; align-items: flex-start;">
                <div class="db-select card-body" style="flex: 1; display: flex; flex-direction: column; height: 260px;">
                    <h3 style="flex-shrink: 0;">选择数据库</h3>
                    <div class="scrollable" style="flex: 1; overflow-y: auto; min-height: 0;">
                        <ul id="dataSourceList"></ul>
                    </div>
                    <button type="button" class="btn" style="flex-shrink: 0; margin-bottom: 8px;">加载表</button>
                </div>
                <div class="table-list card-body" style="flex: 2;">
                    <h3>数据库表列表</h3>
                    <div class="scrollable">
                        <ul id="tableList"></ul>
                    </div>
                </div>
            </div>
            <div class="table-structures" id="tableStructures" style="display: flex; gap: 16px; margin-top: 20px;"></div>
            <div class="generate-section">
                <h3>生成SQL</h3>
                <label for="promptInput">输入提示:</label>
                <textarea id="promptInput" rows="3" placeholder="输入生成SQL的描述..."></textarea>
                <div class="selected-tables">
                    <p>已选表:</p>
                    <ul id="selectedTablesList"></ul>
                </div>
                <button id="generateSqlBtn">生成SQL</button>
            </div>
            <div class="sql-editor">
                <textarea id="sqlTextarea" class="CodeMirror" placeholder="生成或输入SQL语句..."></textarea>
                <button type="button" id="executeSql">执行SQL</button>
                <button type="button" id="clearSql">清空</button>
            </div>
            <div class="query-result">
                <h3>查询结果</h3>
                <div class="result-table" id="queryResultContainer">
                    <!-- 动态结果将在这里显示 -->
                </div>
            </div>
        </div>
    </section>

    <section id="xmind" style="display: none;">
        <h2>XMind转换工具</h2>
        <div class="xmind-converter">
            <h3>XMind 文件转换</h3>
            <form id="xmindForm">
                <input type="file" id="xmindFile" accept=".xmind">
                <select id="templateType">
                    <option value="ones">ones</option>
                    <option value="MeterSphere">MeterSphere</option>
                </select>
                <button type="button" onclick="handleUpload()">开始转换</button>
                <div class="xmind-date card-body">
                    <h3>转换信息</h3>
                    <textarea class="xmind-response" id="xmindResponse" readonly></textarea>
                </div>
                <button type="button" onclick="handleDownload()" id="downloadBtn" disabled>下载Excel</button>
            </form>
            <div id="statusMessage"></div>
        </div>
    </section>

    <section id="testcase" style="display: none;">
        <h2>测试点生成工具</h2>
        <div class="testcase-generator">
            <!--<h3>测试点生成工具</h3>-->
            <form id="testPointForm">
                <input type="file" id="testPointFile" accept=".doc,.docx,.jpg,.png">
                <button type="button" id="generateTestPointBtn">生成测试点</button>
            </form>
            <div class="testcase-result" id="testPointResult" style="display:none;">
                <p id="testPointMsg"></p>
                <div style="margin-top:16px;">
                    <div id="xmindPreviewContainer" style="width:100%;height:500px;border:1px solid #ccc;"></div>
                </div>
                <div style="margin-top:16px;">
                    <input type="text" id="regenerateInput" placeholder="请输入新的生成要求" style="width:60%;padding:6px;">
                    <button type="button" id="regenerateBtn">重新生成</button>
                    <button type="button" id="downloadTestPointBtn" disabled style="margin-left:12px;">下载文件</button>
                </div>
            </div>
        </div>
    </section>
</div>
<!-- xmind-embed-viewer CDN -->
<script src="https://unpkg.com/xmind-embed-viewer/dist/umd/xmind-embed-viewer.js"></script>
</body>
</html>
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
    const navLinks = document.querySelectorAll('nav ul li a');

    navLinks.forEach(link => {
        link.addEventListener('click', function(e) {
            e.preventDefault();
            const targetId = this.getAttribute('href').slice(1);
            document.querySelectorAll('section').forEach(section => {
                section.style.display = 'none';
            });
            document.getElementById(targetId).style.display = 'block';
        });
    });

    // ========== SQL生成工具相关js ==========
    const sqlButton = document.querySelector('.sql-editor button');
    const queryResult = document.querySelector('.query-result table tbody');
    // 1. 加载数据库列表
    function loadDataSources() {
        fetch('/api/db/dataTables')
            .then(res => {
                if (!res.ok) throw new Error('获取数据库列表失败');
                return res.json();
            })
            .then(dbs => {
                const dataSourceList = document.getElementById('dataSourceList');
                dataSourceList.innerHTML = '';
                dbs.forEach(db => {
                    const li = document.createElement('li');
                    li.textContent = db;
                    li.addEventListener('click', () => {
                        // 在数据库点击事件中添加
                        document.querySelectorAll('#dataSourceList li').forEach(l => l.classList.remove('active'));
                        li.classList.add('active');
                        dbConnection.currentDb = dbName;
                    });
                    dataSourceList.appendChild(li);
                });
                // 默认选中第一个数据库
                if (dbs.length > 0) {
                    dataSourceList.firstChild.click();
                }
            })
            .catch(err => {
                console.error(err);
                document.getElementById('dataSourceList').innerHTML = '<li style="color:red;">加载数据库失败</li>';
            });
    }
    // 添加载入表按钮事件
    document.querySelector('.db-select button').addEventListener('click', function() {
        const selectedDb = document.querySelector('#dataSourceList li.active');
        if (selectedDb) {
            loadTables(selectedDb.textContent);
        } else {
            alert('请先选择数据库');
        }
    });
    // 获取数据库列表
    // 2. 加载指定数据库的表列表(使用 form-data 请求方式)
    // 修复后的loadTables函数
    // 存储表结构信息
    const tableStructures = {};
    const selectedTables = new Set();

    // 修改 loadTables 函数,添加表多选逻辑
    function loadTables(dbName) {
        const formData = new FormData();
        formData.append('dbName', dbName);

        fetch('/api/db/tables', { method: 'POST', body: formData })
            .then(res => {
                if (!res.ok) throw new Error('获取表列表失败');
                return res.json();
            })
            .then(tables => {
                const tableList = document.getElementById('tableList');
                tableList.innerHTML = '';
                tables.forEach(table => {
                    const li = document.createElement('li');
                    li.textContent = table;
                    if (selectedTables.has(table)) li.classList.add('active');
                    li.addEventListener('click', () => {
                        if (selectedTables.has(table)) {
                            selectedTables.delete(table);
                            li.classList.remove('active');
                            delete tableStructures[table];
                        } else {
                            selectedTables.add(table);
                            li.classList.add('active');
                            loadTableStructure(table);
                        }
                        renderTableStructures();
                        updateSelectedTablesList();
                    });
                    tableList.appendChild(li);
                });
            })
            .catch(err => {
                console.error(err);
                document.getElementById('tableList').innerHTML = '<li style="color:red;">加载表列表失败</li>';
            });
    }

    // 渲染已选表结构,多列横向展示
    function renderTableStructures() {
        const container = document.getElementById('tableStructures');
        container.innerHTML = '';
        selectedTables.forEach(table => {
            const struct = tableStructures[table] || [];
            let html = `<div class="table-struct-col"><h4>${table}</h4><table><thead><tr><th>名称</th><th>类型</th><th>主键</th></tr></thead><tbody>`;
            struct.forEach(col => {
                html += `<tr><td>${col.Field || ''}</td><td>${col.Type || ''}</td><td>${col.Key || ''}</td></tr>`;
            });
            html += '</tbody></table></div>';
            container.innerHTML += html;
        });
    }

    // 修改 loadTableStructure 函数,存储表结构并渲染
    function loadTableStructure(tableName) {
        fetch(`/api/db/table/${tableName}`)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP错误! 状态码: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                if (!data || !Array.isArray(data)) {
                    throw new Error('返回的数据格式不正确');
                }
                tableStructures[tableName] = data;
                renderTableStructures();
            })
            .catch(error => {
                console.error('加载表结构失败:', error);
                tableStructures[tableName] = [{Field: '加载失败', Type: '', Key: ''}];
                renderTableStructures();
            });
    }

    // 更新已选表列表显示
    function updateSelectedTablesList() {
        const selectedTablesList = document.getElementById('selectedTablesList');
        selectedTablesList.innerHTML = '';
        selectedTables.forEach(table => {
            const li = document.createElement('li');
            li.textContent = table;
            selectedTablesList.appendChild(li);
        });
    }

    // 清空SQL输入框
    const clearBtn = document.getElementById('clearSql');
    if (clearBtn) {
        clearBtn.addEventListener('click', function() {
            document.getElementById('sqlTextarea').value = '';
        });
    }

    //生成sql
    // 初始化时绑定生成SQL按钮事件
    document.addEventListener('DOMContentLoaded', () => {
        document.getElementById('generateSqlBtn').addEventListener('click', generateSql);
    });
    // 生成SQL逻辑
    function generateSql() {
        const prompt = document.getElementById('promptInput').value.trim();
        if (!prompt) {
            alert('请输入生成SQL的提示');
            return;
        }
        if (selectedTables.size === 0) {
            alert('请选择至少一个表');
            return;
        }

        // 收集选中表的结构信息
        const selectedTablesInfo = {};
        selectedTables.forEach(table => {
            selectedTablesInfo[table] = tableStructures[table] || [];
        });

        fetch('/api/db/generate', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                prompt: prompt,
                selectedTablesInfo: selectedTablesInfo
            })
        })
            .then(res => {
                if (!res.ok) throw new Error('生成SQL失败');
                return res.json();
            })
            .then(data => {
                document.getElementById('sqlTextarea').value = data.sql || '';
            })
            .catch(err => {
                console.error('生成SQL失败:', err);
                document.getElementById('sqlTextarea').value = '生成SQL失败,请检查输入或网络';
            });
    }

    // 执行SQL查询
    document.querySelector('#executeSql').addEventListener('click', function() {
        const sql = document.querySelector('.sql-editor textarea').value;
        fetch('/api/db/execute', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({sql: sql})
        })
            .then(response => response.json())
            .then(result => {
                console.log('渲染数据', result);
                const resultContainer = document.getElementById('queryResultContainer');
                resultContainer.innerHTML = '';
                if (!result.success) {
                    resultContainer.innerHTML = `<div class="error">查询失败:${result.error || '未知错误'}</div>`;
                    return;
                }
                const data = result.data || [];
                const columns = result.columns || [];
                if (data.length === 0) {
                    resultContainer.innerHTML = '<div>无数据</div>';
                    return;
                }
                let html = '<table class="result-table"><thead><tr>';
                columns.forEach(col => {
                    html += `<th>${col}</th>`;
                });
                html += '</tr></thead><tbody>';
                data.forEach(row => {
                    html += '<tr>';
                    columns.forEach(col => {
                        html += `<td>${row[col]}</td>`;
                    });
                    html += '</tr>';
                });
                html += '</tbody></table>';
                resultContainer.innerHTML = html;
            })
            .catch(err => {
                document.getElementById('queryResultContainer').innerHTML = `<div class="error">请求异常:${err}</div>`;
            });
    });
    // 页面加载完成后初始化数据库列表
    loadDataSources();
    const generateBtn = document.getElementById('generateSqlBtn');
    if (generateBtn) {
        generateBtn.addEventListener('click', generateSql);
    }

});

// ==========xmind格式转换相关js ==========
function handleUpload() {
    const fileInput = document.getElementById('xmindFile');
    const templateType = document.getElementById('templateType').value;

    if (!fileInput.files[0]) {
        alert('请选择XMind文件');
        return;
    }

    const formData = new FormData();
    formData.append('file', fileInput.files[0]);
    formData.append('templateType', templateType);

    fetch('/xmindToExcel/importFile', {
        method: 'POST',
        body: formData
    })
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok: ' + response.statusText);
            }
            return response.json();
        })
        .then(data => {
            // 将成功信息写入到 <textarea> 中
            const responseTextarea = document.getElementById('xmindResponse');
            responseTextarea.value = '转换成功: ' + JSON.stringify(data);
            // 直接提取 excelUrl
            if (data && data.excelUrl) {
                sessionStorage.setItem('excelUrl', data.excelUrl);
                document.getElementById('downloadBtn').disabled = false;
            } else if (data && data.message && data.message.excelUrl) {
                sessionStorage.setItem('excelUrl', data.message.excelUrl);
                document.getElementById('downloadBtn').disabled = false;
            } else {
                document.getElementById('downloadBtn').disabled = true;
            }
        })
        .catch(error => {
            console.error('Error:', error);
            alert('上传失败: ' + error.message);
        });
}

function handleDownload() {
    const excelUrl = sessionStorage.getItem('excelUrl');
    if (!excelUrl) {
        alert('没有可下载的Excel文件');
        return;
    }
    // 直接用COS url下载
    const a = document.createElement('a');
    a.href = excelUrl;
    a.download = '';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}

// ========== 测试点生成工具相关 ===========
// 预览xmind文件
function previewXmindFile(url) {
    // 加时间戳防缓存
    const previewUrl = url + (url.includes('?') ? '&' : '?') + '_t=' + Date.now();
    const container = document.getElementById('xmindPreviewContainer');
    // 让容器宽高100%,高度自适应父容器
    container.style.width = '100%';
    // 取父容器高度
    const parent = container.parentElement;
    if (parent) {
        const parentHeight = parent.clientHeight;
        if (parentHeight > 0) {
            container.style.height = parentHeight + 'px';
        } else {
            container.style.height = '500px'; // 兜底
        }
    } else {
        container.style.height = '500px';
    }
    fetch(previewUrl)
        .then(res => res.arrayBuffer())
        .then(file => {
            container.innerHTML = '';
            const viewer = new window.XMindEmbedViewer({
                el: '#xmindPreviewContainer',
                style: { width: '100%', height: '100%' }
            });
            setTimeout(() => {
                const inner = container.querySelector('div');
                if (inner) {
                    inner.style.width = '100%';
                    inner.style.height = '100%';
                }
            }, 100);
            viewer.load(file);
        })
        .catch(err => {
            container.innerHTML = '预览失败:' + err + '<br>请确认文件已生成且为有效的xmind文件。';
        });
}

// 判断文件类型
function getFileType(fileName) {
    const extension = fileName.toLowerCase().split('.').pop();
    if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(extension)) {
        return 'image';
    } else if (['doc', 'docx'].includes(extension)) {
        return 'document';
    } else {
        return 'unknown';
    }
}

// 文件上传
function handleTestPointUpload(requirement) {
    const fileInput = document.getElementById('testPointFile');
    if (!fileInput.files[0]) {
        alert('请选择需求文档文件');
        return;
    }
    const formData = new FormData();
    formData.append('file', fileInput.files[0]);
    if (requirement) {
        formData.append('requirement', requirement);
    }
    fetch('/testpoint/generate', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json())
        .then(data => {
            const resultDiv = document.getElementById('testPointResult');
            const msgP = document.getElementById('testPointMsg');
            const downloadBtn = document.getElementById('downloadTestPointBtn');
            if (data.success) {
                msgP.textContent = '测试点生成成功!';
                downloadBtn.disabled = false;
                downloadBtn.dataset.url = data.downloadUrl;
                // 自动预览xmind
                previewXmindFile(data.downloadUrl);
            } else {
                msgP.textContent = '生成失败:' + (data.error || '未知错误');
                downloadBtn.disabled = true;
                downloadBtn.dataset.url = '';
                document.getElementById('xmindPreviewContainer').innerHTML = '';
            }
            resultDiv.style.display = 'block';
        })
        .catch(err => {
            alert('请求失败: ' + err);
        });
}

//文件下载
function handleTestPointDownload() {
    const downloadBtn = document.getElementById('downloadTestPointBtn');
    const url = downloadBtn.dataset.url;
    if (!url) {
        alert('没有可下载的文件');
        return;
    }
    // 提取文件名参数
    const fileParam = url.split('?file=')[1];
    if (!fileParam) {
        alert('下载链接无效');
        return;
    }
    // GET下载,直接跳转/api/download接口
    const downloadUrl = '/fileconfig/download?file=' + encodeURIComponent(fileParam);
    fetch(downloadUrl)
        .then(response => {
            if (!response.ok) throw new Error('下载失败');
            return response.blob();
        })
        .then(blob => {
            const a = document.createElement('a');
            a.href = window.URL.createObjectURL(blob);
            a.download = fileParam;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            window.URL.revokeObjectURL(a.href);
        })
        .catch(err => {
            alert('下载失败,请确认文件已生成且为有效的xmind文件。');
        });
}

// 上传文件,返回文件名
function uploadTestFile(callback) {
    const fileInput = document.getElementById('testPointFile');
    if (!fileInput.files[0]) {
        alert('请选择需求文档文件');
        return;
    }
    const formData = new FormData();
    formData.append('file', fileInput.files[0]);
    fetch('/fileconfig/fileupload', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json())
        .then(data => {
            if (data.success && data.fileName) {
                callback(data.fileName);
            } else {
                alert('文件上传失败: ' + (data.error || '未知错误'));
            }
        })
        .catch(err => {
            alert('文件上传失败: ' + err);
        });
}

// 生成测试点,根据文件类型调用不同的API
function generateTestPoints(fileName, promot, callback) {
    window.lastUploadedFileName = fileName; // 记录最近一次上传的文件名
    const fileType = getFileType(fileName);
    const done = function() { if (typeof callback === 'function') callback(); };
    if (fileType === 'image') {
        // 图片文件调用imageUpload接口
        const fileInput = document.getElementById('testPointFile');
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        if (promot) {
            formData.append('promot', promot);
        }
        fetch('/api/imageUpload', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                handleTestPointResponse(data);
                done();
            })
            .catch(err => {
                alert('请求失败: ' + err);
                done();
            });
    } else if (fileType === 'document') {
        // 文档文件调用wordUpload接口
        const formData = new FormData();
        formData.append('fileName', fileName);
        if (promot) {
            formData.append('promot', promot);
        }
        fetch('/api/wordUpload', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                handleTestPointResponse(data);
                done();
            })
            .catch(err => {
                alert('请求失败: ' + err);
                done();
            });
    } else {
        alert('不支持的文件类型,请上传图片(.jpg, .png等)或文档(.doc, .docx)文件');
        done();
    }
}

// 处理测试点生成响应
function handleTestPointResponse(data) {
    const resultDiv = document.getElementById('testPointResult');
    const msgP = document.getElementById('testPointMsg');
    const downloadBtn = document.getElementById('downloadTestPointBtn');

    if (data.success) {
        msgP.textContent = '测试点生成成功!';
        downloadBtn.disabled = false;
        downloadBtn.dataset.url = data.downloadUrl;
        // 只在xmindPreviewContainer预览区显示xmind,不切换tab
        if (data.downloadUrl) {
            previewXmindFile(data.downloadUrl);
        }
    } else {
        msgP.textContent = '生成失败:' + (data.error || '未知错误');
        downloadBtn.disabled = true;
        downloadBtn.dataset.url = '';
        document.getElementById('xmindPreviewContainer').innerHTML = '';
    }
    resultDiv.style.display = 'block';
}

function setGenerateBtnLoading(loading) {
    const genBtn = document.getElementById('generateTestPointBtn');
    if (genBtn) {
        if (loading) {
            genBtn.disabled = true;
            genBtn.textContent = '生成中...';
        } else {
            genBtn.disabled = false;
            genBtn.textContent = '生成测试点';
        }
    }
}

// 生成测试点按钮事件,默认promot
function handleGenerateBtn() {
    const defaultPromot = '请根据上传的需求文档自动提取所有功能点并生成详细的测试点,要求结构化、分层级、覆盖全面。';
    setGenerateBtnLoading(true);
    uploadTestFile(function(fileName) {
        generateTestPoints(fileName, defaultPromot, function() {
            setGenerateBtnLoading(false);
        });
    });
}

function setRegenerateBtnLoading(loading) {
    const regenBtn = document.getElementById('regenerateBtn');
    if (regenBtn) {
        if (loading) {
            regenBtn.disabled = true;
            regenBtn.textContent = '生成中...';
        } else {
            regenBtn.disabled = false;
            regenBtn.textContent = '重新生成';
        }
    }
}
// 重新生成按钮事件,使用输入框内容
function handleRegenerateBtn() {
    const promot = document.getElementById('regenerateInput').value;
    if (!promot) {
        alert('请输入新的生成要求');
        return;
    }
    // 直接用上次上传的文件名
    const lastFileName = window.lastUploadedFileName;
    if (!lastFileName) {
        alert('请先上传文件并生成一次测试点');
        return;
    }
    setRegenerateBtnLoading(true);
    generateTestPoints(lastFileName, promot, function() {
        setRegenerateBtnLoading(false);
    });
}

document.addEventListener('DOMContentLoaded', function() {
    const genBtn = document.getElementById('generateTestPointBtn');
    if (genBtn) {
        genBtn.addEventListener('click', handleGenerateBtn);
    }
    const dlBtn = document.getElementById('downloadTestPointBtn');
    if (dlBtn) {
        dlBtn.addEventListener('click', handleTestPointDownload);
    }
    // 重新生成按钮
    const regenBtn = document.getElementById('regenerateBtn');
    if (regenBtn) {
        regenBtn.addEventListener('click', handleRegenerateBtn);
    }
});
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f4f4f4;
}

.container {
    width: 80%;
    margin: 0 auto;
    background-color: #fff;
    padding: 20px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

header {
    background-color: #007bff;
    color: #fff;
    padding: 10px 0;
    text-align: center;
}

nav ul {
    list-style-type: none;
    padding: 0;
    display: flex;
    justify-content: center;
}

nav ul li {
    margin: 0 15px;
}

nav ul li a {
    color: #fff;
    text-decoration: none;
}

section {
    margin: 20px 0;
}

h2 {
    border-bottom: 2px solid #007bff;
    padding-bottom: 10px;
    margin-bottom: 20px;
}
#dataSourceList li, #tableList li {
    cursor: pointer;
    padding: 5px;
    border-bottom: 1px solid #eee;
}

#dataSourceList li:hover, #tableList li:hover {
    background-color: #f5f5f5;
}

#dataSourceList li.active {
    background-color: #007bff;
    color: white;
}

.mysql-tool, .xmind-converter, .testcase-generator {
    background-color: #f9f9f9;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}

.db-connection, .sql-generator, .sql-editor, .query-result {
    background-color: #e9ecef;
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 15px;
}

.db-tables, .table-structure {
    background-color: #e9ecef;
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 15px;
    height: 200px;
    overflow-y: auto;
}

textarea {
    width: 100%;
    height: 150px;
    padding: 10px;
    border-radius: 5px;
    border: 1px solid #ccc;
    resize: vertical;
}

button {
    padding: 10px 20px;
    margin: 5px;
    border: none;
    border-radius: 5px;
    background-color: #007bff;
    color: #fff;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}

table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 10px;
}

th, td {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: left;
}

th {
    background-color: #f2f2f2;
}

ul {
    list-style-type: none;
    padding: 0;
}

li {
    margin-bottom: 5px;
}

/* 数据库选择和表列表横向布局 */
.db-row {
    display: flex;
    gap: 24px;
    align-items: flex-start;
}
.db-select, .table-list {
    background: #e9ecef;
    border-radius: 5px;
    padding: 15px;
    min-width: 180px;
    height: 260px;
    overflow-y: auto;
}

/* 多表结构横向分列 */
.table-structures {
    display: flex;
    gap: 16px;
    margin-top: 20px;
    flex-wrap: wrap;
}
.table-struct-col {
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 5px;
    padding: 10px;
    min-width: 220px;
    max-width: 260px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.04);
}
.table-struct-col h4 {
    margin: 0 0 8px 0;
    font-size: 16px;
    color: #007bff;
    text-align: center;
}
.table-struct-col table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}
.table-struct-col th, .table-struct-col td {
    border: 1px solid #ddd;
    padding: 4px 6px;
    text-align: left;
}
.table-struct-col th {
    background: #f2f2f2;
}

/* 表多选高亮 */
#tableList li.active {
    background-color: #28a745;
    color: #fff;
}
#tableList li {
    cursor: pointer;
    padding: 5px;
    border-bottom: 1px solid #eee;
    border-radius: 3px;
    transition: background 0.2s;
}
#tableList li:hover {
    background: #e0f7fa;
}

/* SQL输入区按钮间距 */
.sql-editor button {
    margin-right: 10px;
}

后端项目目录


后端接口实现见下一篇《记一次测试工具从 V1.0 到 V1.1 的回溯和成果(二)》

No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up