This commit is contained in:
Wyle.Gong-巩文昕
2025-04-23 11:21:08 +08:00
parent fc643727f3
commit 7a1aae1e2f
135 changed files with 221483 additions and 1 deletions
BIN
View File
Binary file not shown.
+50
View File
@@ -0,0 +1,50 @@
<!doctype html>
<html>
<head>
<title>404 - 页面未找到</title>
<script src="/ui/assets/test.js"></script>
</head>
<style>
/* 定义全局样式 */
.error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #f5f5f5;
}
.error-code {
font-size: 8rem;
color: #d32f2f;
margin-bottom: 1rem;
}
.error-message {
font-size: 2rem;
color: #757575;
margin-bottom: 2rem;
}
.back-btn {
padding: 0.5rem 2rem;
background-color: #6200ea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
<body class="error-page">
<div class="error-code">404</div>
<div class="error-message">页面未找到</div>
<button @click="goBack" class="back-btn">返回上一页</button>
</body>
<script setup>
// 返回上一页的方法
console.log(window)
goBack = () => {
history.back();
};
</script>
</html>
+754
View File
@@ -0,0 +1,754 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>文档管理</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
font-size: 12px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
font-size: 15px;
}
.header h1 {
margin: 0;
color: #333;
}
.breadcrumb {
margin-bottom: 20px;
color: #666;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.control-panel {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
flex-grow: 1;
margin-right: 20px;
}
input[type="text"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
padding: 8px 16px;
margin-right: 8px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #40a9ff;
}
button.danger {
background-color: #ff4d4f;
}
button.danger:hover {
background-color: #ff7875;
}
button.secondary {
background-color: #52c41a;
}
button.secondary:hover {
background-color: #73d13d;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #fafafa;
font-weight: 600;
}
tr:hover {
background-color: #f5f5f5;
}
.action-buttons {
display: flex;
gap: 8px;
}
.action-buttons button {
padding: 4px 10px;
font-size: 12px;
}
.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: #fff;
margin: 10% auto;
padding: 20px;
border-radius: 8px;
width: 50%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
margin: 0;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.modal-footer {
margin-top: 20px;
text-align: right;
}
.empty-state {
text-align: center;
padding: 40px 0;
color: #999;
}
.empty-state p {
margin-bottom: 20px;
}
.file-upload {
margin-top: 10px;
}
.file-upload input[type="file"] {
display: none;
}
.file-upload label {
display: inline-block;
padding: 8px 16px;
background-color: #f0f0f0;
color: #333;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-upload label:hover {
background-color: #e0e0e0;
}
.file-name {
margin-left: 10px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="project_list.html">项目列表</a> &gt;
<span id="project-name">文档管理</span>
</div>
<div class="header">
<h1>文档管理</h1>
<div>
<button id="back-btn" style="display: none">返回项目列表</button>
</div>
</div>
<div class="control-panel">
<div class="search-box">
<input type="text" id="search-input" placeholder="搜索文档..." />
</div>
<button id="add-doc-btn">添加文档</button>
<button id="refresh-btn" style="display: none">刷新</button>
</div>
<div id="docs-table-container">
<table id="docs-table">
<thead>
<tr>
<th>文档名称</th>
<th>类型</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="docs-list">
<!-- 文档列表将通过JavaScript动态生成 -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none">
<p>暂无文档数据</p>
<button id="create-first-doc-btn">创建第一个文档</button>
</div>
</div>
</div>
<!-- 添加/编辑文档的模态框 -->
<div id="doc-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">添加文档</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<form id="doc-form">
<input type="hidden" id="doc-id" />
<div class="form-group">
<label for="doc-name">文档名称</label>
<input type="text" id="doc-name" required />
</div>
<div class="form-group">
<label for="doc-type">文档类型</label>
<select id="doc-type">
<option value="requirement">需求文档</option>
<option value="api">API文档</option>
<option value="design">设计文档</option>
<option value="other">其他</option>
</select>
</div>
<div class="form-group">
<label for="doc-description">文档描述</label>
<textarea id="doc-description"></textarea>
</div>
<div class="form-group file-upload">
<label for="doc-file">上传文件</label>
<input type="file" id="doc-file" />
<span class="file-name" id="file-name">未选择文件</span>
</div>
</form>
</div>
<div class="modal-footer">
<button id="cancel-btn">取消</button>
<button id="save-doc-btn">保存</button>
</div>
</div>
</div>
<!-- 确认删除的模态框 -->
<div id="confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>确认删除</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<p>确定要删除这个文档吗?此操作不可撤销。</p>
</div>
<div class="modal-footer">
<button id="cancel-delete-btn">取消</button>
<button id="confirm-delete-btn" class="danger">删除</button>
</div>
</div>
</div>
<script>
// 全局变量
let docs = [];
let currentDocId = null;
let projectId = null;
let projectName = "";
// DOM元素
const docsListEl = document.getElementById("docs-list");
const emptyStateEl = document.getElementById("empty-state");
const searchInputEl = document.getElementById("search-input");
const docModal = document.getElementById("doc-modal");
const confirmModal = document.getElementById("confirm-modal");
const modalTitle = document.getElementById("modal-title");
const docForm = document.getElementById("doc-form");
const docIdInput = document.getElementById("doc-id");
const docNameInput = document.getElementById("doc-name");
const docTypeInput = document.getElementById("doc-type");
const docDescInput = document.getElementById("doc-description");
const docFileInput = document.getElementById("doc-file");
const fileNameEl = document.getElementById("file-name");
const projectNameEl = document.getElementById("project-name");
// 按钮
const backBtn = document.getElementById("back-btn");
const addDocBtn = document.getElementById("add-doc-btn");
const refreshBtn = document.getElementById("refresh-btn");
const createFirstDocBtn = document.getElementById("create-first-doc-btn");
const saveDocBtn = document.getElementById("save-doc-btn");
const cancelBtn = document.getElementById("cancel-btn");
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
const closeButtons = document.querySelectorAll(".close");
// 初始化
// 初始化
init();
function init() {
const urlParams = new URLSearchParams(window.location.search);
projectId = urlParams.get("project_id");
if (!projectId) {
alert("未指定项目ID,将返回项目列表");
window.location.href = "project_list.html";
return;
}
// 获取项目信息
fetchProjectInfo();
// 获取文档列表
fetchDocs();
// 设置事件监听器
setupEventListeners();
}
// 设置事件监听器
function setupEventListeners() {
// 返回按钮
backBtn.addEventListener("click", () => {
window.location.href = "project_list.html";
});
// 添加文档按钮
addDocBtn.addEventListener("click", () => openDocModal());
// 刷新按钮
refreshBtn.addEventListener("click", fetchDocs);
// 创建第一个文档按钮
createFirstDocBtn.addEventListener("click", () => openDocModal());
// 保存文档按钮
saveDocBtn.addEventListener("click", saveDoc);
// 取消按钮
cancelBtn.addEventListener("click", closeDocModal);
// 确认删除按钮
confirmDeleteBtn.addEventListener("click", deleteDoc);
// 取消删除按钮
cancelDeleteBtn.addEventListener("click", closeConfirmModal);
// 关闭按钮
closeButtons.forEach((button) => {
button.addEventListener("click", function () {
docModal.style.display = "none";
confirmModal.style.display = "none";
});
});
// 搜索输入框
searchInputEl.addEventListener("input", filterDocs);
// 文件选择监听
docFileInput.addEventListener("change", function () {
if (this.files.length > 0) {
fileNameEl.textContent = this.files[0].name;
if (docNameInput.value === "") {
console.log("changeName");
docNameInput.value = this.files[0].name;
}
} else {
fileNameEl.textContent = "未选择文件";
}
});
// 点击模态框外部关闭
window.addEventListener("click", function (event) {
if (event.target === docModal) {
closeDocModal();
}
if (event.target === confirmModal) {
closeConfirmModal();
}
});
}
// 获取项目信息
async function fetchProjectInfo() {
try {
const response = await fetch(
`http://127.0.0.1:5002/api/project/${projectId}`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && result.data) {
projectName = result.data.name || "未命名项目";
projectNameEl.textContent = projectName;
document.title = `${projectName} - 文档管理`;
} else {
console.error("获取项目信息失败:", result.message || "未知错误");
}
} catch (error) {
console.error("获取项目信息错误:", error);
}
}
// 获取文档列表
async function fetchDocs() {
try {
const response = await fetch(
`http://127.0.0.1:5002/api/doc/${projectId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
docs = result.data.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at);
});
//console.log("docs", docs)
renderDocs(docs);
} else {
console.error("获取文档失败:", result.message || "未知错误");
showEmptyState(true, "加载失败,请重试");
}
} catch (error) {
console.error("获取文档错误:", error);
showEmptyState(true, "加载失败,请重试");
}
}
// 渲染文档列表
function renderDocs(docsToRender) {
if (!docsToRender || docsToRender.length === 0) {
showEmptyState(true);
return;
}
showEmptyState(false);
docsListEl.innerHTML = "";
docsToRender.forEach((doc) => {
const row = document.createElement("tr");
// 格式化日期
const createdAt = new Date(doc.created_at);
const formattedDate = createdAt.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
// 格式化文档类型
let docType = "其他";
switch (doc.type) {
case "api":
docType = "API文档";
break;
case "requirement":
docType = "需求文档";
break;
case "design":
docType = "设计文档";
break;
}
row.innerHTML = `
<td>${doc.name}</td>
<td>${docType}</td>
<td>${formattedDate}</td>
<td class="action-buttons">
<button class="view-demands-btn secondary" data-id="${doc.id}">查看需求</button>
<button class="edit-btn" data-id="${doc.id}">编辑</button>
<button class="delete-btn danger" data-id="${doc.id}">删除</button>
</td>
`;
docsListEl.appendChild(row);
});
// 添加按钮事件
document.querySelectorAll(".view-demands-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const docId = this.getAttribute("data-id");
window.location.href = `demand.html?doc_id=${docId}`;
});
});
document.querySelectorAll(".edit-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const docId = this.getAttribute("data-id");
const doc = docs.find((d) => d.id === docId);
openDocModal(doc);
});
});
document.querySelectorAll(".delete-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const docId = this.getAttribute("data-id");
openConfirmModal(docId);
});
});
}
// 显示/隐藏空状态
function showEmptyState(show, message = "暂无文档数据") {
if (show) {
document.getElementById("docs-table").style.display = "none";
emptyStateEl.style.display = "block";
emptyStateEl.querySelector("p").textContent = message;
} else {
document.getElementById("docs-table").style.display = "table";
emptyStateEl.style.display = "none";
}
}
// 过滤文档
function filterDocs() {
const searchTerm = searchInputEl.value.toLowerCase();
if (!searchTerm) {
renderDocs(docs);
return;
}
const filteredDocs = docs.filter(
(doc) =>
doc.name.toLowerCase().includes(searchTerm) ||
(doc.description &&
doc.description.toLowerCase().includes(searchTerm))
);
renderDocs(filteredDocs);
}
// 打开文档模态框
function openDocModal(doc = null) {
modalTitle.textContent = doc ? "编辑文档" : "添加文档";
if (doc) {
docIdInput.value = doc.id;
docNameInput.value = doc.name || "";
docTypeInput.value = doc.type || "other";
docDescInput.value = doc.description || "";
fileNameEl.textContent = "保持原文件";
} else {
docForm.reset();
docIdInput.value = "";
fileNameEl.textContent = "未选择文件";
}
docModal.style.display = "block";
}
// 关闭文档模态框
function closeDocModal() {
docModal.style.display = "none";
docForm.reset();
fileNameEl.textContent = "未选择文件";
}
// 打开确认删除模态框
function openConfirmModal(docId) {
currentDocId = docId;
confirmModal.style.display = "block";
}
// 关闭确认删除模态框
function closeConfirmModal() {
confirmModal.style.display = "none";
currentDocId = null;
}
// 保存文档
async function saveDoc() {
const docId = docIdInput.value;
const name = docNameInput.value.trim();
const type = docTypeInput.value;
const description = docDescInput.value.trim();
if (!name) {
alert("请输入文档名称");
return;
}
// 创建FormData对象用于文件上传
const formData = new FormData();
formData.append("name", name);
formData.append("type", type);
formData.append("description", description);
formData.append("project_id", projectId);
// 如果有选择文件,添加到FormData
if (docFileInput.files.length > 0) {
formData.append("file", docFileInput.files[0]);
}
try {
let url = "http://127.0.0.1:5002/api/doc/";
let method = "POST";
if (docId) {
url += docId;
method = "PATCH";
}
const response = await fetch(url, {
method,
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
alert(docId ? "文档更新成功" : "文档创建成功");
closeDocModal();
fetchDocs();
} else {
alert(`操作失败: ${result.message || "未知错误"}`);
}
} catch (error) {
console.error("保存文档错误:", error);
alert(`操作失败: ${error.message}`);
}
}
// 删除文档
async function deleteDoc() {
if (!currentDocId) return;
try {
const response = await fetch(
`http://127.0.0.1:5002/api/doc/${currentDocId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
alert("文档删除成功");
closeConfirmModal();
fetchDocs();
} else {
alert(`删除失败: ${result.message || "未知错误"}`);
}
} catch (error) {
console.error("删除文档错误:", error);
alert(`删除失败: ${error.message}`);
}
}
</script>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
<link rel="stylesheet" href="/assets/style/bootstrap.css" crossorigin="anonymous">
<script src="/assets/nats.ws-1.30.3/test.js"></script>
<script type="module" src="/assets/nats.ws-1.30.3/examples/bench.js"></script>
</head>
<body>
<div class="container">
<h1 id="title">WS NATS Browser Performance</h1>
<label for="server">NATS Websocket Server</label>
<input type="text" class="form-control" id="server" placeholder="server" autocomplete="off"
value="localhost:9222"><br />
<label for="ws">Use ws:// (uncheck for wss://)</label>
<input type="checkbox" class="form-check-input" id="ws" value="true" checked><br />
<label for="subject">Subject</label>
<input type="text" class="form-control" id="subject" placeholder="subject" autocomplete="off" value="foo"><br />
<label for="count">Count</label>
<input type="number" class="form-control" id="count" placeholder="messages" autocomplete="off"
value="100000"><br />
<label for="payload">Payload size</label>
<input type="number" class="form-control" id="payload" placeholder="payload size in bytes" autocomplete="off"
value="0"><br />
<label for="callbacks">Use callbacks</label>
<input type="checkbox" class="form-check-input" id="callbacks" value="true" checked><br />
<h3>Test</h3>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="pubsub" checked id="pubsub"
name="test">Pub/Sub</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="pub" id="pub" name="test">Publish</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="sub" id="sub" name="test">Subscribe</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="sub" id="reqrep" name="test">ReqReq</label>
</div>
<br /><br />
<button id="send" onclick="benchapp.run()" class="btn btn-primary">Start</button>
</div>
<br />
<div class="container">
<pre id="results"></pre>
</div>
</body>
</html>
+3153
View File
File diff suppressed because it is too large Load Diff
+1585
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
View File
+59
View File
@@ -0,0 +1,59 @@
<!doctype html>
<html>
<head>
<title>欢迎来到我们的网站</title>
</head>
<style>
.hero {
background-color: #f8f9fa;
padding: 4rem 1rem;
text-align: center;
}
.feature-item {
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.25rem;
}
</style>
<body>
<!-- 导航栏 -->
<nav class="flex items-center justify-between p-4 bg-blue-700 text-white">
<div class="font-bold text-lg">公司名称</div>
<ul class="flex space-x-4">
<li><a href="#" class="hover:underline">首页</a></li>
<li><a href="#" class="hover:underline">产品</a></li>
<li><a href="#" class="hover:underline">关于我们</a></li>
<li><a href="#" class="hover:underline">联系我们</a></li>
</ul>
</nav>
<!-- 英雄区域 -->
<section class="hero">
<h1 class="text-4xl font-bold">欢迎来到我们的网站</h1>
<p class="mt-4 max-w-xl mx-auto text-gray-600">
这里是公司的简介和主要业务介绍文字。
</p>
</section>
<!-- 特色内容 -->
<section class="container mx-auto my-8">
<h2 class="text-2xl font-bold mb-4">我们的特色</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="feature-item" v-for="(feature, index) in features" :key="index">
<h3 class="font-bold">{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</div>
</section>
<!-- 联系方式 -->
<footer class="bg-gray-800 text-white text-center py-4 mt-8">
<p>&copy; 2024 公司名称 | 联系电话:123-4567-890 | 邮箱:info@example.com</p>
</footer>
<script setup>
// 响应式数据
features = [
{ title: '快速响应', description: '我们提供7x24小时的快速响应服务' },
{ title: '专业团队', description: '拥有经验丰富的专业团队' },
{ title: '优质服务', description: '致力于为客户提供最优质的服务体验' }
]
</script>
</html>
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<script type="module" src="./bench.js"></script>
<div class="container">
<h1 id="title">WS NATS Browser Performance</h1>
<label for="server">NATS Websocket Server</label>
<input type="text" class="form-control" id="server" placeholder="server" autocomplete="off"
value="localhost:9222"><br/>
<label for="ws">Use ws:// (uncheck for wss://)</label>
<input type="checkbox" class="form-check-input" id="ws" value="true" checked><br/>
<label for="subject">Subject</label>
<input type="text" class="form-control" id="subject" placeholder="subject" autocomplete="off" value="foo"><br/>
<label for="count">Count</label>
<input type="number" class="form-control" id="count" placeholder="messages" autocomplete="off"
value="100000"><br/>
<label for="payload">Payload size</label>
<input type="number" class="form-control" id="payload" placeholder="payload size in bytes" autocomplete="off"
value="0"><br/>
<label for="callbacks">Use callbacks</label>
<input type="checkbox" class="form-check-input" id="callbacks" value="true" checked><br/>
<h3>Test</h3>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="pubsub" checked id="pubsub"
name="test">Pub/Sub</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="pub" id="pub" name="test">Publish</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="sub" id="sub" name="test">Subscribe</label>
</div>
<div class="radio-inline">
<label><input class="form-check-input" type="radio" value="sub" id="reqrep" name="test">ReqReq</label>
</div>
<br/><br/>
<button id="send" onclick="benchapp.run()" class="btn btn-primary">Start</button>
</div>
<br/>
<div class="container">
<pre id="results"></pre>
</div>
</body>
</html>
+151
View File
@@ -0,0 +1,151 @@
/*
* Copyright 2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Bench, connect, Metric } from "../esm/nats.js";
function getString(id) {
return document.getElementById(id).value;
}
function isChecked(id) {
return document.getElementById(id).checked;
}
function getTestChoice() {
if (isChecked("pubsub")) {
return "pubsub";
} else if (isChecked("pub")) {
return "pub";
} else if (isChecked("sub")) {
return "sub";
} else if (isChecked("reqrep")) {
return "reqrep";
}
}
function getNumber(id) {
const v = getString(id);
if (!isNaN(v)) {
return parseInt(v, 10);
}
return -1;
}
function updateResults(s) {
const p = document.createElement("pre");
p.appendChild(document.createTextNode(s));
document.getElementById("results").appendChild(p);
}
async function run() {
const server = getString("server");
const ws = isChecked("ws");
const nc = await connect(
{
servers: `${ws ? "ws://" : "wss://"}${server}`,
pendingLimit: 8192,
},
);
nc.closed()
.then((err) => {
if (err) {
console.error(err);
}
});
const kind = getTestChoice();
const t = {};
t.callbacks = isChecked("callbacks");
t.msgs = getNumber("count");
t.size = getNumber("payload");
t.subject = getString("subject");
t.pub = kind === "pub" || kind === "pubsub";
t.sub = kind === "sub" || kind === "pubsub";
t.req = kind === "reqrep";
t.rep = kind === "reqrep";
const bench = new Bench(nc, t);
const m = await bench.run();
const metrics = [];
metrics.push(...m);
await nc.close();
const pubsub = metrics.filter((m) => m.name === "pubsub").reduce(
reducer,
new Metric("pubsub", 0),
);
const reqrep = metrics.filter((m) => m.name === "reqrep").reduce(
reducer,
new Metric("reqrep", 0),
);
const pub = metrics.filter((m) => m.name === "pub").reduce(
reducer,
new Metric("pub", 0),
);
const sub = metrics.filter((m) => m.name === "sub").reduce(
reducer,
new Metric("sub", 0),
);
const req = metrics.filter((m) => m.name === "req").reduce(
reducer,
new Metric("req", 0),
);
const rep = metrics.filter((m) => m.name === "rep").reduce(
reducer,
new Metric("rep", 0),
);
const report = [];
if (pubsub && pubsub.msgs) {
report.push(pubsub.toString());
}
if (reqrep && reqrep.msgs) {
report.push(reqrep.toString());
}
if (pub && pub.msgs) {
report.push(pub.toString());
}
if (sub && sub.msgs) {
report.push(sub.toString());
}
if (req && req.msgs) {
report.push(req.toString());
}
if (rep && rep.msgs) {
report.push(rep.toString());
}
updateResults(report.join("\n"));
}
const reducer = (a, m) => {
if (a) {
a.name = m.name;
a.payload = m.payload;
a.bytes += m.bytes;
a.duration += m.duration;
a.msgs += m.msgs;
a.lang = m.lang;
a.version = m.version;
a.async = m.async;
a.max = Math.max(a.max === undefined ? 0 : a.max, m.duration);
a.min = Math.min(a.min === undefined ? m.duration : a.max, m.duration);
}
return a;
};
window.benchapp = {
run: run,
};
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ws-nats chat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
crossorigin="anonymous">
<script type="module" src="/assets/nats.ws-1.30.3/examples/chat.js"></script>
</head>
<!-- when the browser exits, we publish a message -->
<body onunload="chat.exiting()">
<!-- a form for entering messages -->
<div class="container">
<h1>ws-nats chat</h1>
<input type="text" class="form-control" id="data" placeholder="Message" autocomplete="off"><br />
<button id="send" onclick="chat.send()" class="btn btn-primary">Send</button>
</div>
<br />
<!-- a place to record messages -->
<div id="chats" class="container"></div>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
/*
* Copyright 2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { connect, JSONCodec } from "/assets/nats.ws-1.30.3/esm/nats.js";
const me = Date.now();
window.chat = {
send: send,
exiting: exiting,
};
// create a decoder, the client is sending JSON
const jc = JSONCodec();
// create a connection, and register listeners
const init = async function () {
// if the connection doesn't resolve, an exception is thrown
// a real app would allow configuring the hostport and whether
// to use WSS or not.
const conn = await connect(
{ servers: "ws://localhost:9222" },
);
// handle connection to the server is closed - should disable the ui
conn.closed().then((err) => {
let m = "NATS connection closed";
addEntry(`${m} ${err ? err.message : ""}`);
});
(async () => {
for await (const s of conn.status()) {
addEntry(`Received status update: ${s.type}`);
}
})().then();
// the chat application listens for messages sent under the subject 'chat'
(async () => {
const chat = conn.subscribe("chat");
for await (const m of chat) {
const jm = jc.decode(m.data);
addEntry(
jm.id === me ? `(me): ${jm.m}` : `(${jm.id}): ${jm.m}`,
);
}
})().then();
// when a new browser joins, the joining browser publishes an 'enter' message
(async () => {
const enter = conn.subscribe("enter");
for await (const m of enter) {
const jm = jc.decode(m.data);
addEntry(`${jm.id} entered.`);
}
})().then();
(async () => {
const exit = conn.subscribe("exit");
for await (const m of exit) {
const jm = jc.decode(m.data);
if (jm.id !== me) {
addEntry(`${jm.id} exited.`);
}
}
})().then();
// we connected, and we publish our enter message
conn.publish("enter", jc.encode({ id: me }));
return conn;
};
init().then((conn) => {
window.nc = conn;
}).catch((ex) => {
addEntry(`Error connecting to NATS: ${ex}`);
});
// this is the input field
let input = document.getElementById("data");
// add a listener to detect edits. If they hit Enter, we publish it
input.addEventListener("keyup", (e) => {
if (e.key === "Enter") {
document.getElementById("send").click();
} else {
e.preventDefault();
}
});
// send a message if user typed one
function send() {
input = document.getElementById("data");
const m = input.value;
if (m !== "" && window.nc) {
window.nc.publish("chat", jc.encode({ id: me, m: m }));
input.value = "";
}
return false;
}
// send the exit message
function exiting() {
if (window.nc) {
window.nc.publish("exit", jc.encode({ id: me }));
}
}
// add an entry to the document
function addEntry(s) {
const p = document.createElement("pre");
p.appendChild(document.createTextNode(s));
document.getElementById("chats").appendChild(p);
}
+79
View File
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<!-- a place to record messages -->
<div id="messages" class="container"></div>
<!-- load a script -->
<script type="module">
import { connect, StringCodec, credsAuthenticator } from '../esm/nats.js'
// add an entry to the document
function addEntry (s) {
const p = document.createElement('pre')
p.appendChild(document.createTextNode(s))
document.getElementById('messages').appendChild(p)
}
const sc = StringCodec()
async function testConnection (nc) {
addEntry('connected to NATS!')
// simple publisher
nc.publish('hello', sc.encode('nats'))
addEntry('published a message to `hello`')
// simple subscriber, if the message has a reply subject
// send a reply
const sub = await nc.subscribe('help');
(async () => {
addEntry('listening for request on `help`')
for await (const m of sub) {
m.respond(sc.encode(`I can help ${sc.decode(m.data)}`))
}
})().then()
// request data - requests only receive one message
// to receive multiple messages, create a subscription
addEntry('making a request to `help`')
const msg = await nc.request('help', sc.encode('nats request'))
addEntry(`got response '${sc.decode(msg.data)}'`)
// close the connection
nc.close()
addEntry('closed the connection')
}
async function init () {
// To connect to NGS you need a creds or jwt authenticator
// you can create one with nsc:
// nsc add operator -u synadia
// nsc add account myaccount
// nsc add user myuser
// nsc generate creds -a myaccount -n myuser -o ./myuser.creds
// fetch the creds
const creds = await fetch('./myuser.creds')
if (!creds.ok) {
addEntry("unable to find ./myuser.creds - aborting")
return;
}
const token = await creds.text()
const auth = credsAuthenticator(sc.encode(token))
// connect
let nc = await connect({ servers: 'wss://connect.ngs.global', authenticator: auth, debug: true })
await testConnection(nc)
await nc.closed
}
init()
</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
port: 4222
websocket: {
port: 9222
no_tls: true
compression: true
}
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
crossorigin="anonymous">
<script type="module" src="/assets/nats.ws-1.30.3/examples/simple.js"></script>
</head>
<body>
<!-- a place to record messages -->
<div id="messages" class="container"></div>
<!-- load a script -->
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
port: 4222
websocket: {
port: 443
tls: {
cert_file: "../certs/cert.pem"
key_file: "../certs/key.pem"
}
}
+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<!-- a place to record messages -->
<div id="messages" class="container"></div>
<!-- load a script -->
<script type="module">
import { connect } from '../nats.js'
// add an entry to the document
function addEntry(s) {
const p = document.createElement("pre");
p.appendChild(document.createTextNode(s));
document.getElementById("messages").appendChild(p);
}
const init = async function () {
try {
// create a connection to a wss server
const nc = await connect({ servers: 'wss://localhost:9222' });
addEntry('connected!');
await nc.flush();
addEntry('did a round-trip to the server');
// close the connection
await nc.close();
addEntry('closed the connection');
} catch(err) {
addEntry(`error connecting - did you setup a wss server? ${err}`);
console.error(err)
}
}
init();
</script>
</body>
</html>
+666
View File
@@ -0,0 +1,666 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Stage管理</title>
<link rel="stylesheet" href="/ui/assets/common.css" />
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
font-size: 12px;
margin: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
font-size: 12px;
}
.btn:hover {
background-color: #40a9ff;
}
.btn-danger {
background-color: #ff4d4f;
}
.btn-danger:hover {
background-color: #ff7875;
}
.breadcrumb {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 500;
}
.table tr:hover {
background-color: #f5f5f5;
}
.action-cell {
display: flex;
gap: 8px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 24px;
border-radius: 4px;
width: 400px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.close {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- <h1 class="title">Stage管理</h1> -->
<div class="breadcrumb">
<a href="/project_list">返回 Project 管理</a> / Stage管理
</div>
<button class="btn" id="add-stage-btn">添加Stage</button>
</div>
<div id="stage-list">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>关联树节点</th>
<!-- <th>描述</th> -->
<th>操作</th>
</tr>
</thead>
<tbody id="stage-table-body">
<!-- 数据将通过JavaScript动态加载 -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none">
<p>暂无Stage数据,请添加新Stage</p>
</div>
</div>
</div>
<!-- 添加/编辑Stage的模态框 -->
<div class="modal" id="stage-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modal-title">添加Stage</h2>
<span class="close" id="close-modal">&times;</span>
</div>
<form id="stage-form">
<input type="hidden" id="stage-id" value="" />
<div class="form-group">
<label class="form-label" for="stage-name">名称</label>
<input
type="text"
class="form-input"
id="stage-name"
value=""
required
/>
</div>
<div class="form-group">
<label class="form-label" for="stage-tree-id">关联树节点</label>
<select class="form-input" id="stage-tree-id" required>
<option value="">请选择树节点</option>
<!-- 树节点选项将通过JavaScript动态加载 -->
</select>
</div>
<div class="form-actions">
<button type="button" class="btn" id="cancel-btn">取消</button>
<button type="submit" class="btn" id="save-btn">保存</button>
</div>
</form>
</div>
</div>
<!-- 确认删除的模态框 -->
<div class="modal" id="confirm-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<span class="close" id="close-confirm-modal">&times;</span>
</div>
<p>
确定要删除此Stage吗?此操作将同时删除该Stage下的所有图形节点和连接,且不可恢复。
</p>
<div class="form-actions">
<button type="button" class="btn" id="cancel-delete-btn">取消</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn">
删除
</button>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div class="loading" id="loading" style="display: none">
<div class="spinner"></div>
</div>
<script>
// 基础URL
const API_BASE_URL = "http://127.0.0.1:5002/api";
// DOM元素
const stageTableBody = document.getElementById("stage-table-body");
const emptyState = document.getElementById("empty-state");
const addStageBtn = document.getElementById("add-stage-btn");
const stageModal = document.getElementById("stage-modal");
const modalTitle = document.getElementById("modal-title");
const closeModalBtn = document.getElementById("close-modal");
const stageForm = document.getElementById("stage-form");
const stageIdInput = document.getElementById("stage-id");
const stageNameInput = document.getElementById("stage-name");
const stageTreeIdSelect = document.getElementById("stage-tree-id");
const cancelBtn = document.getElementById("cancel-btn");
const confirmModal = document.getElementById("confirm-modal");
const closeConfirmModal = document.getElementById("close-confirm-modal");
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
const loading = document.getElementById("loading");
const urlParams = new URLSearchParams(window.location.search);
// 当前要删除的StageID
let currentDeleteId = null;
// 存储树节点数据
let treeNodes = [];
let projectId = urlParams.get("project_id");
// 页面加载时获取Stage列表和树节点列表
function init() {
fetchTreeNodes();
fetchStages();
}
init();
// 添加Stage按钮点击事件
addStageBtn.addEventListener("click", () => {
modalTitle.textContent = "添加Stage";
stageForm.reset();
stageIdInput.value = "";
openModal(stageModal);
});
// 关闭模态框
closeModalBtn.addEventListener("click", () => closeModal(stageModal));
cancelBtn.addEventListener("click", () => closeModal(stageModal));
// 关闭确认删除模态框
closeConfirmModal.addEventListener("click", () =>
closeModal(confirmModal)
);
cancelDeleteBtn.addEventListener("click", () => closeModal(confirmModal));
// 确认删除
confirmDeleteBtn.addEventListener("click", deleteStage);
// 表单提交
stageForm.addEventListener("submit", handleFormSubmit);
// 获取所有Stage
async function fetchStages() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${projectId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
renderStageList(result.data);
} else {
showError("获取Stage列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage列表错误:", error);
showError("获取Stage列表错误: " + error.message);
} finally {
hideLoading();
}
}
async function fetchTreeNodes() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/demand/${projectId}`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("fetch nodes", response);
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
treeNodes = result.data;
populateTreeSelect();
} else {
showError("获取树节点列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取树节点列表错误:", error);
showError("获取树节点列表错误: " + error.message);
} finally {
hideLoading();
}
}
// 加载Stage数据用于编辑
// 填充树节点选择框
function populateTreeSelect() {
stageTreeIdSelect.innerHTML = '<option value="">请选择树节点</option>';
// 添加虚拟根节点选项
/* const rootOption = document.createElement("option");
rootOption.value = "root"; // 使用特殊值"root"表示虚拟根节点
rootOption.textContent = "所有节点 (虚拟根节点)";
stageTreeIdSelect.appendChild(rootOption);
*/
treeNodes.forEach((node) => {
const option = document.createElement("option");
option.value = node.id;
option.textContent = `${node.name} (Level: ${node.level})`;
stageTreeIdSelect.appendChild(option);
});
}
function renderStageList(stages) {
stageTableBody.innerHTML = "";
if (stages.length === 0) {
emptyState.style.display = "block";
return;
}
emptyState.style.display = "none";
stages.forEach((stage) => {
const row = document.createElement("tr");
// 截断ID以便显示
const shortId = stage.id.substring(0, 8) + "...";
// 查找关联的树节点名称
let treeNodeName = "未关联";
if (stage.demand_id) {
if (stage.demand_id === "root") {
treeNodeName = "所有节点 (虚拟根节点)";
} else {
console.log("demand_id: ", stage.demand_id);
console.log(treeNodes);
const treeNode = treeNodes.find(
(node) => node.id === stage.demand_id
);
if (treeNode) {
treeNodeName = `${treeNode.name} (Level: ${treeNode.level})`;
} else {
treeNodeName = `ID: ${stage.demand_id.substring(0, 8)}...`;
}
}
}
row.innerHTML = `
<td title="${stage.id}">${shortId}</td>
<td>${stage.name || "未命名"}</td>
<td>${treeNodeName}</td>
<td class="action-cell">
<button class="btn" data-id="${
stage.id
}" data-action="view-graph">查看Graph</button>
<button class="btn" data-id="${
stage.id
}" data-action="view-endpoint">查看Endpoint</button>
<button class="btn" data-id="${
stage.id
}" data-action="edit">编辑</button>
<button class="btn btn-danger" data-id="${
stage.id
}" data-action="delete">删除</button>
</td>
`;
stageTableBody.appendChild(row);
});
// 添加按钮事件监听
document.querySelectorAll("[data-action]").forEach((button) => {
button.addEventListener("click", handleActionClick);
});
}
// 处理操作按钮点击
async function handleActionClick(event) {
console.log("action click");
const action = event.target.getAttribute("data-action");
const stageId = event.target.getAttribute("data-id");
switch (action) {
case "view-graph":
// 跳转到图形页面
window.location.href = `/project/stage/stageGraph?project_id=${projectId}&stage_id=${stageId}`;
break;
case "view-endpoint":
// 跳转到Endpoint管理页面
window.location.href = `/project/stage/endpoint_list?project_id=${projectId}&stage_id=${stageId}`;
break;
case "edit":
await loadStageForEdit(stageId);
break;
case "delete":
openDeleteConfirmation(stageId);
break;
}
}
// 加载Stage数据用于编辑
async function loadStageForEdit(stageId) {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${project_id}/${stageId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("get");
const result = await response.json();
if (result.code === 0 && result.data) {
const stage = result.data.stage;
// 填充表单
console.log(stageIdInput);
stageIdInput.value = stage.id;
stageNameInput.value = stage.name || "";
stageTreeIdSelect.value = stage.demand_id || "";
//stageLevelInput.value = stage.level || 0;
//stageDescriptionInput.innerHTML = stage.description || "";
// 更新模态框标题
modalTitle.textContent = "编辑Stage";
// 打开模态框
openModal(stageModal);
} else {
showError("获取Stage详情失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage详情错误:", error);
showError("获取Stage详情错误: " + error.message);
} finally {
hideLoading();
}
}
async function handleFormSubmit(event) {
event.preventDefault();
const stageId = stageIdInput.value;
const isEdit = !!stageId;
const stageData = {
name: stageNameInput.value,
project_id: projectId,
demand_id: stageTreeIdSelect.value,
};
showLoading();
try {
const url = isEdit
? `${API_BASE_URL}/project/stage/${projectId}/${stageId}/`
: `${API_BASE_URL}/project/stage/`;
const method = isEdit ? "PATCH" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(stageData),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage(isEdit ? "Stage更新成功" : "Stage创建成功");
closeModal(stageModal);
fetchStages(); // 刷新列表
} else {
showError(
(isEdit ? "更新" : "创建") +
"Stage失败: " +
(result.message || "未知错误")
);
}
} catch (error) {
console.error(isEdit ? "更新Stage错误:" : "创建Stage错误:", error);
showError((isEdit ? "更新" : "创建") + "Stage错误: " + error.message);
} finally {
hideLoading();
}
}
// 打开删除确认对话框
function openDeleteConfirmation(stageId) {
currentDeleteId = stageId;
openModal(confirmModal);
}
// 删除Stage
async function deleteStage() {
if (!currentDeleteId) return;
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/stage/${project_id}/${currentDeleteId}/`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage("Stage删除成功");
closeModal(confirmModal);
fetchStages(); // 刷新列表
} else {
showError("删除Stage失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("删除Stage错误:", error);
showError("删除Stage错误: " + error.message);
} finally {
hideLoading();
currentDeleteId = null;
}
}
// 工具函数:打开模态框
function openModal(modal) {
modal.style.display = "flex";
}
// 工具函数:关闭模态框
function closeModal(modal) {
modal.style.display = "none";
}
// 工具函数:显示加载中
function showLoading() {
loading.style.display = "flex";
}
// 工具函数:隐藏加载中
function hideLoading() {
loading.style.display = "none";
}
// 工具函数:显示消息
function showMessage(message) {
alert(message); // 简单实现,可以替换为更友好的通知
}
// 工具函数:显示错误
function showError(message) {
alert("错误: " + message); // 简单实现,可以替换为更友好的通知
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+726
View File
@@ -0,0 +1,726 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Endpoint管理</title>
<link rel="stylesheet" href="/assets/common.css" />
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
margin: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #40a9ff;
}
.btn-danger {
background-color: #ff4d4f;
}
.btn-danger:hover {
background-color: #ff7875;
}
.btn-warning {
background-color: #fa8c16;
}
.btn-warning:hover {
background-color: #ffa940;
}
.btn-back {
margin-right: 10px;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 500;
}
.table tr:hover {
background-color: #f5f5f5;
}
.action-cell {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 24px;
border-radius: 4px;
width: 500px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.close {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.notification {
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 4px;
display: none;
}
.success {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.error {
background-color: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.breadcrumb {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.breadcrumb a {
color: #1890ff;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="javascript:void(0);" onclick="history.back()"
>返回 Stage 管理</a
>
/ Endpoint管理
</div>
<div class="header">
<h1 class="title">Endpoint管理</h1>
<div>
<button class="btn" id="createBtn">添加 Endpoint</button>
</div>
</div>
<div id="notification" class="notification"></div>
<div id="loading" class="loading" style="display: none">
<div class="spinner"></div>
</div>
<div id="endpointsTable" class="table-container">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>URL</th>
<th>请求方法</th>
<th>操作</th>
</tr>
</thead>
<tbody id="endpointsList"></tbody>
</table>
</div>
<div id="emptyState" class="empty-state" style="display: none">
<p>暂无 Endpoint 数据,请添加新 Endpoint</p>
</div>
<!-- 创建/编辑 Modal -->
<div id="endpointModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle">添加 Endpoint</h2>
<span class="close" id="closeModal">&times;</span>
</div>
<form id="endpointForm">
<input type="hidden" id="endpointId" />
<div class="form-group">
<label class="form-label" for="name">名称:</label>
<input type="text" class="form-input" id="name" required />
</div>
<div class="form-group">
<label class="form-label" for="url">URL:</label>
<input type="text" class="form-input" id="url" required />
</div>
<div class="form-group">
<label class="form-label" for="method">请求方法:</label>
<select class="form-select" id="method" required>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="description">描述:</label>
<textarea
class="form-textarea"
id="description"
rows="3"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn" id="cancelBtn">取消</button>
<button type="submit" class="btn" id="saveBtn">保存</button>
</div>
</form>
</div>
</div>
<!-- 查看详情 Modal -->
<div id="viewModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Endpoint详情</h2>
<span class="close" id="closeViewModal">&times;</span>
</div>
<div id="endpointDetails"></div>
<div class="form-actions">
<button type="button" class="btn" id="closeViewBtn">关闭</button>
</div>
</div>
</div>
<!-- 确认删除 Modal -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<span class="close" id="closeConfirmModal">&times;</span>
</div>
<p>确定要删除此Endpoint吗?此操作不可恢复。</p>
<div class="form-actions">
<button type="button" class="btn" id="cancelDeleteBtn">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
删除
</button>
</div>
</div>
</div>
</div>
<script>
// 基础API URL
const API_BASE_URL =
"http://127.0.0.1:5002/api/project/stage/tools/endpoint";
// 从URL获取参数
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get("project_id");
const stageId = urlParams.get("stage_id");
// DOM 元素
const endpointsList = document.getElementById("endpointsList");
const emptyState = document.getElementById("emptyState");
const endpointsTable = document.getElementById("endpointsTable");
const createBtn = document.getElementById("createBtn");
const notification = document.getElementById("notification");
const loading = document.getElementById("loading");
// Modal 元素
const endpointModal = document.getElementById("endpointModal");
const viewModal = document.getElementById("viewModal");
const confirmModal = document.getElementById("confirmModal");
const modalTitle = document.getElementById("modalTitle");
const endpointForm = document.getElementById("endpointForm");
const endpointIdInput = document.getElementById("endpointId");
const nameInput = document.getElementById("name");
const urlInput = document.getElementById("url");
const methodInput = document.getElementById("method");
const descriptionInput = document.getElementById("description");
const endpointDetails = document.getElementById("endpointDetails");
// 按钮元素
const closeModal = document.getElementById("closeModal");
const closeViewModal = document.getElementById("closeViewModal");
const closeConfirmModal = document.getElementById("closeConfirmModal");
const cancelBtn = document.getElementById("cancelBtn");
const closeViewBtn = document.getElementById("closeViewBtn");
const cancelDeleteBtn = document.getElementById("cancelDeleteBtn");
const confirmDeleteBtn = document.getElementById("confirmDeleteBtn");
// 当前要删除的ID
let currentDeleteId = null;
// 页面加载时检查参数并获取数据
if (!projectId || !stageId) {
showNotification(
"缺少必要的项目ID或Stage ID,请返回Stage管理页面重试",
true
);
return;
}
fetchEndpoints();
// 添加按钮事件监听
createBtn.addEventListener("click", showCreateModal);
endpointForm.addEventListener("submit", handleFormSubmit);
// 关闭Modal的事件
closeModal.addEventListener("click", () => closeModalFunc(endpointModal));
closeViewModal.addEventListener("click", () => closeModalFunc(viewModal));
closeConfirmModal.addEventListener("click", () =>
closeModalFunc(confirmModal)
);
cancelBtn.addEventListener("click", () => closeModalFunc(endpointModal));
closeViewBtn.addEventListener("click", () => closeModalFunc(viewModal));
cancelDeleteBtn.addEventListener("click", () =>
closeModalFunc(confirmModal)
);
confirmDeleteBtn.addEventListener("click", deleteEndpoint);
// 获取Endpoints列表
async function fetchEndpoints() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/list/${projectId}/${stageId}/`
);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const data = await response.json();
if (data.length === 0) {
endpointsTable.style.display = "none";
emptyState.style.display = "block";
} else {
renderEndpointsList(data.data);
endpointsTable.style.display = "block";
emptyState.style.display = "none";
}
} catch (error) {
showNotification(`获取Endpoint列表失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 渲染Endpoints列表
function renderEndpointsList(endpoints) {
endpointsList.innerHTML = "";
endpoints.forEach((endpoint) => {
// 截取ID以便显示
const shortId = endpoint.id
? endpoint.id.substring(0, 8) + "..."
: "-";
const row = document.createElement("tr");
row.innerHTML = `
<td title="${endpoint.id}">${shortId}</td>
<td>${endpoint.name || "-"}</td>
<td>${endpoint.path || "-"}</td>
<td>${endpoint.method || "-"}</td>
<td class="action-cell">
<button class="btn" data-id="${
endpoint.id
}" data-action="view">查看</button>
<button class="btn" data-id="${
endpoint.id
}" data-action="edit">编辑</button>
<button class="btn btn-warning" data-id="${
endpoint.id
}" data-action="debug"
data-url="${endpoint.path || ""}" data-method="${
endpoint.method || "GET"
}">调试</button>
<button class="btn btn-danger" data-id="${
endpoint.id
}" data-action="delete">删除</button>
</td>
`;
endpointsList.appendChild(row);
});
// 为按钮添加事件
document.querySelectorAll("[data-action]").forEach((button) => {
button.addEventListener("click", handleActionClick);
});
}
// 处理操作按钮点击
async function handleActionClick(event) {
const action = event.target.getAttribute("data-action");
const id = event.target.getAttribute("data-id");
switch (action) {
case "view":
await viewEndpoint(id);
break;
case "edit":
await showEditModal(id);
break;
case "delete":
showDeleteConfirmation(id);
break;
case "debug":
const url = event.target.getAttribute("data-url");
const method = event.target.getAttribute("data-method");
goToDebugger(id, url, method);
break;
}
}
// 跳转到调试页面
function goToDebugger(id, url, method) {
window.location.href = `/project/stage/utils/endpoint_debug?project_id=${projectId}&stage_id=${stageId}&endpoint_id=${id}`;
}
// 显示创建Modal
function showCreateModal() {
endpointForm.reset();
endpointIdInput.value = "";
modalTitle.textContent = "添加 Endpoint";
openModalFunc(endpointModal);
}
// 显示编辑Modal
async function showEditModal(id) {
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${id}/`);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const endpointT = await response.json();
const endpoint = endpointT.data;
endpointIdInput.value = endpoint.id;
nameInput.value = endpoint.name || "";
urlInput.value = endpoint.path || "";
methodInput.value = endpoint.method || "GET";
descriptionInput.value = endpoint.description || "";
modalTitle.textContent = "编辑 Endpoint";
openModalFunc(endpointModal);
} catch (error) {
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 查看Endpoint详情
async function viewEndpoint(id) {
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${id}/`);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const endpointT = await response.json();
const endpoint = endpointT.data;
// 格式化日期
const createdDate = endpoint.created_at
? new Date(endpoint.created_at).toLocaleString()
: "-";
const updatedDate = endpoint.updated_at
? new Date(endpoint.updated_at).toLocaleString()
: "-";
endpointDetails.innerHTML = `
<div class="form-group">
<label class="form-label">ID:</label>
<p>${endpoint.id}</p>
</div>
<div class="form-group">
<label class="form-label">名称:</label>
<p>${endpoint.name || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">URL:</label>
<p>${endpoint.path || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">请求方法:</label>
<p>${endpoint.method || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">项目ID:</label>
<p>${endpoint.project_id}</p>
</div>
<div class="form-group">
<label class="form-label">Stage ID:</label>
<p>${endpoint.stage_id}</p>
</div>
<div class="form-group">
<label class="form-label">描述:</label>
<p>${endpoint.description || "-"}</p>
</div>
<div class="form-group">
<label class="form-label">创建时间:</label>
<p>${createdDate}</p>
</div>
<div class="form-group">
<label class="form-label">更新时间:</label>
<p>${updatedDate}</p>
</div>
`;
openModalFunc(viewModal);
} catch (error) {
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
} finally {
hideLoading();
}
}
// 处理表单提交
async function handleFormSubmit(event) {
event.preventDefault();
const id = endpointIdInput.value;
const isUpdate = !!id;
const endpointData = {
name: nameInput.value.trim(),
url: urlInput.value.trim(),
method: methodInput.value,
project_id: projectId,
stage_id: stageId,
description: descriptionInput.value.trim(),
};
showLoading();
try {
const url = isUpdate ? `${API_BASE_URL}/${id}/` : API_BASE_URL;
const method = isUpdate ? "PATCH" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(endpointData),
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
closeModalFunc(endpointModal);
showNotification(`Endpoint ${isUpdate ? "更新" : "创建"}成功!`);
fetchEndpoints();
} catch (error) {
showNotification(
`${isUpdate ? "更新" : "创建"} Endpoint失败: ${error.message}`,
true
);
} finally {
hideLoading();
}
}
// 显示删除确认
function showDeleteConfirmation(id) {
currentDeleteId = id;
openModalFunc(confirmModal);
}
// 删除Endpoint
async function deleteEndpoint() {
if (!currentDeleteId) return;
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/${currentDeleteId}/`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
closeModalFunc(confirmModal);
showNotification("Endpoint删除成功!");
fetchEndpoints();
} catch (error) {
showNotification(`删除Endpoint失败: ${error.message}`, true);
} finally {
hideLoading();
currentDeleteId = null;
}
}
// 显示Modal
function openModalFunc(modal) {
modal.style.display = "flex";
}
// 关闭Modal
function closeModalFunc(modal) {
modal.style.display = "none";
}
// 显示通知
function showNotification(message, isError = false) {
notification.textContent = message;
notification.className = isError
? "notification error"
: "notification success";
notification.style.display = "block";
setTimeout(() => {
notification.style.display = "none";
}, 5000);
}
// 显示加载
function showLoading() {
loading.style.display = "flex";
}
// 隐藏加载
function hideLoading() {
loading.style.display = "none";
}
// 关闭Modal当点击外部
window.addEventListener("click", (event) => {
if (event.target === endpointModal) {
closeModalFunc(endpointModal);
}
if (event.target === viewModal) {
closeModalFunc(viewModal);
}
if (event.target === confirmModal) {
closeModalFunc(confirmModal);
}
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+664
View File
@@ -0,0 +1,664 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>API Debugger</title>
<!-- Include Tailwind CSS via CDN -->
<!-- <script src="https://cdn.tailwindcss.com"></script> -->
</head>
<body class="bg-gray-100 text-gray-800">
<div id="notifications" class="fixed top-5 right-5 z-50"></div>
<div class="container mx-auto p-4 max-w-6xl">
<header
class="flex justify-between items-center py-4 mb-6 border-b border-gray-200"
>
<div class="flex items-center">
<span class="text-2xl mr-2">⚙️</span>
<h1 class="text-2xl font-bold text-blue-600">API Debugger</h1>
</div>
</header>
<div class="bg-white rounded-lg shadow-md p-5 mb-6">
<div class="flex flex-col md:flex-row mb-4">
<select
id="method-select"
class="md:w-32 w-full p-2.5 border border-gray-300 rounded-t md:rounded-tr-none md:rounded-l text-blue-600 font-bold mb-2 md:mb-0"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
<input
type="text"
id="url-input"
placeholder="Enter request URL"
class="flex-grow p-2.5 border border-gray-300 md:rounded-none border-y md:border-l-0 md:border-r-0 mb-2 md:mb-0 outline-none"
/>
<button
id="send-btn"
class="p-2.5 bg-blue-600 text-white font-medium rounded-b md:rounded-bl-none md:rounded-r hover:bg-blue-700 transition"
>
Send
</button>
</div>
<!-- Headers Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="headers-accordion-header"
>
<span class="font-semibold">Headers</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b"
id="headers-accordion-content"
>
<div id="headers-container">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-lg">Request Headers</h3>
<button
id="add-header-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Header
</button>
</div>
<div class="header-row flex mb-2">
<input
type="text"
class="p-2 border border-gray-300 rounded-l flex-1"
placeholder="Header name"
value="Content-Type"
/>
<input
type="text"
class="p-2 border border-gray-300 border-l-0 flex-1"
placeholder="Header value"
value="application/json"
/>
<button
class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition"
>
×
</button>
</div>
</div>
</div>
</div>
<!-- Parameters Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="params-accordion-header"
>
<span class="font-semibold">Query Parameters</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
id="params-accordion-content"
>
<div id="params-container">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-lg">Query Parameters</h3>
<button
id="add-param-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Parameter
</button>
</div>
<!-- Parameters will be added here -->
</div>
</div>
</div>
<!-- Request Body Section -->
<div class="mb-4">
<div
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
id="body-accordion-header"
>
<span class="font-semibold">Request Body</span>
<span></span>
</div>
<div
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
id="body-accordion-content"
>
<div>
<div class="flex mb-3" id="body-type-selector">
<div
class="px-3 py-1.5 border border-gray-300 rounded-l cursor-pointer body-type-option bg-blue-600 text-white"
data-type="json"
>
JSON
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
data-type="form"
>
Form Data
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
data-type="x-www-form-urlencoded"
>
x-www-form-urlencoded
</div>
<div
class="px-3 py-1.5 border-t border-b border-r border-gray-300 rounded-r cursor-pointer body-type-option"
data-type="text"
>
Text
</div>
</div>
<textarea
id="body-editor"
class="w-full h-48 p-3 border border-gray-300 rounded font-mono resize-y"
placeholder="Enter request body here"
>
{\n "key": "value"\n}</textarea
>
<div id="form-data-container" class="hidden">
<div class="flex justify-between items-center mb-3 mt-4">
<h3 class="font-semibold text-lg">Form Data</h3>
<button
id="add-form-field-btn"
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
>
Add Field
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Response Section -->
<div class="bg-white rounded-lg shadow-md p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Response</h3>
<div id="response-time" class="text-gray-600"></div>
</div>
<div
class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200"
>
<div id="response-status" class="font-bold">No request sent yet</div>
<div id="response-size" class="text-gray-600"></div>
</div>
<pre
id="response-body"
class="bg-gray-800 text-white p-4 rounded overflow-auto max-h-96 font-mono"
>
Send a request to see the response</pre
>
<div class="flex justify-end mt-4 space-x-2">
<button
id="copy-response-btn"
class="px-3 py-1.5 bg-gray-700 text-white rounded hover:bg-gray-600 transition flex items-center"
>
📋 Copy
</button>
</div>
</div>
</div>
<script>
// DOM Elements
const methodSelect = document.getElementById("method-select");
const urlInput = document.getElementById("url-input");
const sendBtn = document.getElementById("send-btn");
const addHeaderBtn = document.getElementById("add-header-btn");
const headersContainer = document.getElementById("headers-container");
const addParamBtn = document.getElementById("add-param-btn");
const paramsContainer = document.getElementById("params-container");
const bodyTypeOptions = document.querySelectorAll(".body-type-option");
const bodyEditor = document.getElementById("body-editor");
const formDataContainer = document.getElementById("form-data-container");
const addFormFieldBtn = document.getElementById("add-form-field-btn");
const responseStatus = document.getElementById("response-status");
const responseTime = document.getElementById("response-time");
const responseSize = document.getElementById("response-size");
const responseBody = document.getElementById("response-body");
const copyResponseBtn = document.getElementById("copy-response-btn");
const notificationsContainer = document.getElementById("notifications");
// Accordion elements
const accordionHeaders = document.querySelectorAll(
'[id$="accordion-header"]'
);
const accordionContents = document.querySelectorAll(
'[id$="accordion-content"]'
);
// State management
let currentBodyType = "json";
let requestStartTime = 0;
let latestResponse = null;
// Accordion functionality
accordionHeaders.forEach((header) => {
header.addEventListener("click", () => {
const contentId = header.id.replace("-header", "-content");
const content = document.getElementById(contentId);
content.classList.toggle("hidden");
// Update the arrow icon
const arrow = header.querySelector("span:last-child");
if (!content.classList.contains("hidden")) {
arrow.textContent = "▼";
} else {
arrow.textContent = "▶";
}
});
});
// Request Headers
addHeaderBtn.addEventListener("click", addNewHeader);
// Add initial header row functionality
document
.querySelector(".header-row button")
.addEventListener("click", function () {
this.closest(".header-row").remove();
});
// Query Parameters
addParamBtn.addEventListener("click", addNewParam);
// Request Body Type Selection
bodyTypeOptions.forEach((option) => {
option.addEventListener("click", () => {
bodyTypeOptions.forEach((opt) => {
opt.classList.remove("bg-blue-600", "text-white");
});
option.classList.add("bg-blue-600", "text-white");
currentBodyType = option.getAttribute("data-type");
if (currentBodyType === "form") {
bodyEditor.classList.add("hidden");
formDataContainer.classList.remove("hidden");
} else {
bodyEditor.classList.remove("hidden");
formDataContainer.classList.add("hidden");
// Set appropriate placeholder based on body type
switch (currentBodyType) {
case "json":
bodyEditor.placeholder = "Enter JSON body here";
bodyEditor.value =
bodyEditor.value || '{\n "key": "value"\n}';
break;
case "x-www-form-urlencoded":
bodyEditor.placeholder = "key1=value1&key2=value2";
bodyEditor.value = bodyEditor.value || "key=value";
break;
case "text":
bodyEditor.placeholder = "Enter plain text body here";
bodyEditor.value = bodyEditor.value || "";
break;
}
}
});
});
// Form Data Fields
addFormFieldBtn.addEventListener("click", addNewFormField);
// Send Request
sendBtn.addEventListener("click", sendRequest);
// Response Actions
copyResponseBtn.addEventListener("click", copyResponseToClipboard);
/**
* Add a new header row to the headers container
*/
function addNewHeader() {
const headerRow = document.createElement("div");
headerRow.className = "header-row flex mb-2";
headerRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Header name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Header value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
headersContainer.appendChild(headerRow);
// Add event listener to the remove button
headerRow
.querySelector("button")
.addEventListener("click", function () {
this.closest(".header-row").remove();
});
}
/**
* Add a new query parameter row
*/
function addNewParam() {
const paramRow = document.createElement("div");
paramRow.className = "param-row flex mb-2";
paramRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Parameter name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Parameter value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
paramsContainer.appendChild(paramRow);
// Add event listener to the remove button
paramRow.querySelector("button").addEventListener("click", function () {
this.closest(".param-row").remove();
});
}
/**
* Add a new form field row for form data body type
*/
function addNewFormField() {
const formFieldRow = document.createElement("div");
formFieldRow.className = "param-row flex mb-2";
formFieldRow.innerHTML = `
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Field name">
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Field value">
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
`;
formDataContainer.appendChild(formFieldRow);
// Add event listener to the remove button
formFieldRow
.querySelector("button")
.addEventListener("click", function () {
this.closest(".param-row").remove();
});
}
/**
* Collect headers from the UI
* @returns {Object} - Headers object
*/
function getHeaders() {
const headers = {};
const headerRows = headersContainer.querySelectorAll(".header-row");
headerRows.forEach((row) => {
const key = row.querySelector("input:nth-child(1)").value.trim();
const value = row.querySelector("input:nth-child(2)").value.trim();
if (key && value) {
headers[key] = value;
}
});
return headers;
}
/**
* Build query string from parameters
* @returns {string} - Query string
*/
function buildQueryString() {
const params = [];
const paramRows = paramsContainer.querySelectorAll(".param-row");
paramRows.forEach((row) => {
const key = row.querySelector("input:nth-child(1)").value.trim();
const value = row.querySelector("input:nth-child(2)").value.trim();
if (key && value) {
params.push(
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
);
}
});
return params.length > 0 ? `?${params.join("&")}` : "";
}
/**
* Get request body based on the selected body type
* @returns {string|FormData|null} - Request body
*/
function getRequestBody() {
if (methodSelect.value === "GET" || methodSelect.value === "HEAD") {
return null;
}
switch (currentBodyType) {
case "json":
try {
// Try to parse JSON to validate it
JSON.parse(bodyEditor.value);
return bodyEditor.value;
} catch (error) {
showNotification(`Invalid JSON: ${error.message}`, "error");
return null;
}
case "form":
const formData = new FormData();
const formFields = formDataContainer.querySelectorAll(".param-row");
formFields.forEach((field) => {
const key = field
.querySelector("input:nth-child(1)")
.value.trim();
const value = field
.querySelector("input:nth-child(2)")
.value.trim();
if (key) {
formData.append(key, value);
}
});
return formData;
case "x-www-form-urlencoded":
return bodyEditor.value;
case "text":
return bodyEditor.value;
default:
return null;
}
}
/**
* Send the API request
*/
function sendRequest() {
// Get the URL
const url = urlInput.value;
console.log(url);
if (!url) {
showNotification("Please enter a URL", "error");
return;
}
// Add query string if there are query parameters
const queryString = buildQueryString();
const fullUrl = `${url}${queryString}`;
// Get request headers
const headers = getHeaders();
// Get request body based on body type
const body = getRequestBody();
if (body === null && currentBodyType === "json") {
return; // Stop if JSON is invalid
}
console.log(fullUrl);
// Set loading state
responseBody.textContent = "Loading...";
responseStatus.textContent = "Sending request...";
responseStatus.className = "font-bold";
responseSize.textContent = "";
requestStartTime = Date.now();
// Create fetch options
const fetchOptions = {
method: methodSelect.value,
headers: headers,
};
// Add body to request if it's not GET or HEAD
if (
methodSelect.value !== "GET" &&
methodSelect.value !== "HEAD" &&
body
) {
fetchOptions.body = body;
}
// Make the fetch request
fetch(fullUrl, fetchOptions)
.then((response) => {
const endTime = Date.now();
const responseTimeMs = endTime - requestStartTime;
responseTime.textContent = `Time: ${responseTimeMs}ms`;
// Set status
responseStatus.textContent = `Status: ${response.status} ${response.statusText}`;
if (response.ok) {
responseStatus.className = "font-bold text-green-600";
} else {
responseStatus.className = "font-bold text-red-600";
}
// Get response size
const contentLength = response.headers.get("content-length");
if (contentLength) {
const size = parseInt(contentLength, 10);
responseSize.textContent = `Size: ${formatBytes(size)}`;
}
// Store the response object for later use
latestResponse = response;
// Clone the response so we can use it multiple times
return response.text().then((text) => {
try {
// Try to parse as JSON
const json = JSON.parse(text);
return JSON.stringify(json, null, 2);
} catch (e) {
// If not JSON, return as text
return text;
}
});
})
.then((responseText) => {
// Display the response
responseBody.textContent = responseText;
})
.catch((error) => {
responseStatus.textContent = `Error: ${error.message}`;
responseStatus.className = "font-bold text-red-600";
responseBody.textContent = `Failed to fetch: ${error.message}`;
responseSize.textContent = "";
showNotification(`Request failed: ${error.message}`, "error");
});
}
/**
* Format bytes to a human-readable format
* @param {number} bytes - The size in bytes
* @returns {string} - Formatted size
*/
function formatBytes(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* Copy response to clipboard
*/
function copyResponseToClipboard() {
if (
responseBody.textContent.trim() ===
"Send a request to see the response" ||
responseBody.textContent.trim() === "Loading..."
) {
showNotification("No response to copy", "error");
return;
}
navigator.clipboard
.writeText(responseBody.textContent)
.then(() => {
showNotification("Response copied to clipboard", "success");
})
.catch((err) => {
showNotification("Failed to copy response", "error");
});
}
/**
* Show a notification
*/
function showNotification(message, type = "success") {
const notification = document.createElement("div");
notification.className = `mb-3 p-3 rounded-lg shadow-md border-l-4 ${
type === "success"
? "bg-green-100 border-green-500 text-green-800"
: "bg-red-100 border-red-500 text-red-800"
}`;
notification.innerHTML = `
<div class="flex justify-between items-center">
<div>${message}</div>
<button class="ml-4 text-gray-600 hover:text-gray-800">×</button>
</div>
`;
// Add to notifications container
notificationsContainer.appendChild(notification);
// Add event listener to close button
notification.querySelector("button").addEventListener("click", () => {
notification.remove();
});
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
// Set up keyboard shortcuts
document.addEventListener("keydown", (event) => {
// Ctrl+Enter to send request
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
sendRequest();
event.preventDefault();
}
});
// Initialize
document.addEventListener("DOMContentLoaded", () => {
// Open the headers section by default
const headersContent = document.getElementById(
"headers-accordion-content"
);
headersContent.classList.remove("hidden");
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+624
View File
@@ -0,0 +1,624 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>项目管理</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-top: 0;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.control-panel {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
flex-grow: 1;
margin-right: 20px;
}
input[type="text"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
body button {
padding: 8px 16px;
margin-right: 8px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
font-size: 12px;
}
body button:hover {
background-color: #40a9ff;
}
body button.danger {
background-color: #ff4d4f;
}
body button.danger:hover {
background-color: #ff7875;
}
body table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
body th,
body td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
body th {
background-color: #fafafa;
font-weight: 600;
font-size: 12px;
}
body tr:hover {
background-color: #f5f5f5;
}
body .action-buttons {
display: flex;
gap: 8px;
}
body .action-buttons button {
padding: 4px 10px;
font-size: 12px;
}
body .modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
body .modal-content {
background-color: #fff;
margin: 10% auto;
padding: 20px;
border-radius: 8px;
width: 50%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
body .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
body .modal-header h2 {
margin: 0;
}
body .close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
}
body .form-group {
margin-bottom: 15px;
}
body .form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
body .form-group input,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
body .form-group textarea {
min-height: 100px;
resize: vertical;
}
body .modal-footer {
margin-top: 20px;
text-align: right;
}
body .empty-state {
text-align: center;
padding: 40px 0;
color: #999;
}
body .empty-state p {
margin-bottom: 20px;
}
body .description-cell {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
}
body .description-cell:hover {
cursor: pointer;
color: #1890ff;
}
</style>
</head>
<body>
<div class="container">
<h1>项目管理</h1>
<div class="control-panel">
<div class="search-box">
<input type="text" id="search-input" placeholder="搜索项目..." />
</div>
<button id="add-project-btn">添加项目</button>
<!-- <button id="refresh-btn">刷新</button> -->
</div>
<div id="projects-table-container">
<table id="projects-table">
<thead>
<tr>
<th>项目名称</th>
<th>描述</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="projects-list">
<!-- 项目列表将通过JavaScript动态生成 -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none">
<p>暂无项目数据</p>
<button id="create-first-project-btn">创建第一个项目</button>
</div>
</div>
</div>
<!-- 添加/编辑项目的模态框 -->
<div id="project-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">添加项目</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<form id="project-form">
<input type="hidden" id="project-id" />
<div class="form-group">
<label for="project-name">项目名称</label>
<input type="text" id="project-name" required />
</div>
<div class="form-group">
<label for="project-description">项目描述</label>
<textarea id="project-description"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button id="cancel-btn">取消</button>
<button id="save-project-btn">保存</button>
</div>
</div>
</div>
<!-- 确认删除的模态框 -->
<div id="confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>确认删除</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<p>确定要删除这个项目吗?此操作不可撤销。</p>
</div>
<div class="modal-footer">
<button id="cancel-delete-btn">取消</button>
<button id="confirm-delete-btn" class="danger">删除</button>
</div>
</div>
</div>
<script>
// 全局变量
let projects = [];
let currentProjectId = null;
// DOM元素
const projectsListEl = document.getElementById("projects-list");
const emptyStateEl = document.getElementById("empty-state");
const searchInputEl = document.getElementById("search-input");
const projectModal = document.getElementById("project-modal");
const confirmModal = document.getElementById("confirm-modal");
const modalTitle = document.getElementById("modal-title");
const projectForm = document.getElementById("project-form");
const projectIdInput = document.getElementById("project-id");
const projectNameInput = document.getElementById("project-name");
const projectDescInput = document.getElementById("project-description");
// 按钮
const addProjectBtn = document.getElementById("add-project-btn");
const refreshBtn = document.getElementById("refresh-btn");
const createFirstProjectBtn = document.getElementById(
"create-first-project-btn"
);
const saveProjectBtn = document.getElementById("save-project-btn");
const cancelBtn = document.getElementById("cancel-btn");
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
const closeButtons = document.querySelectorAll(".close");
init();
// 初始化
function init() {
fetchProjects();
setupEventListeners();
}
// 设置事件监听器
function setupEventListeners() {
// 添加项目按钮
addProjectBtn.addEventListener("click", () => openProjectModal());
// 刷新按钮
// refreshBtn.addEventListener("click", fetchProjects);
// 创建第一个项目按钮
createFirstProjectBtn.addEventListener("click", () =>
openProjectModal()
);
// 保存项目按钮
saveProjectBtn.addEventListener("click", saveProject);
// 取消按钮
cancelBtn.addEventListener("click", closeProjectModal);
// 确认删除按钮
confirmDeleteBtn.addEventListener("click", deleteProject);
// 取消删除按钮
cancelDeleteBtn.addEventListener("click", closeConfirmModal);
// 关闭按钮
closeButtons.forEach((button) => {
button.addEventListener("click", function () {
projectModal.style.display = "none";
confirmModal.style.display = "none";
});
});
// 搜索输入框
searchInputEl.addEventListener("input", filterProjects);
// 点击模态框外部关闭
window.addEventListener("click", function (event) {
if (event.target === projectModal) {
closeProjectModal();
}
if (event.target === confirmModal) {
closeConfirmModal();
}
});
}
// 获取项目列表
async function fetchProjects() {
try {
const response = await fetch("http://127.0.0.1:5002/api/project/");
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
projects = result.data.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at);
});
renderProjects(projects);
} else {
console.error("获取项目失败:", result.message || "未知错误");
showEmptyState(true, "加载失败,请重试");
}
} catch (error) {
console.error("获取项目错误:", error);
showEmptyState(true, "加载失败,请重试");
}
}
// 渲染项目列表
function renderProjects(projectsToRender) {
if (!projectsToRender || projectsToRender.length === 0) {
showEmptyState(true);
return;
}
showEmptyState(false);
projectsListEl.innerHTML = "";
projectsToRender.forEach((project) => {
const row = document.createElement("tr");
// 格式化日期
const createdAt = new Date(project.created_at);
const formattedDate = createdAt.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
const shortDesc = project.description
? project.description.length > 50
? project.description.substring(0, 50) + "..."
: project.description
: "";
row.innerHTML = `
<td>${project.name}</td>
<td class="description-cell" title="${project.description || ""
}">${shortDesc}</td>
<td>${formattedDate}</td>
<td class="action-buttons">
<button class="view-docs-btn" data-id="${project.id
}">查看文档</button>
<button class="view-demands-btn secondary" data-id="${project.id
}">查看需求树</button>
<button class="view-stages-btn secondary" data-id="${project.id
}">查看场景</button>
<button class="edit-btn" data-id="${project.id}">编辑</button>
<button class="delete-btn danger" data-id="${project.id
}">删除</button>
</td>
`;
projectsListEl.appendChild(row);
});
document.querySelectorAll(".view-demands-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const docId = this.getAttribute("data-id");
window.location.href = `project/demand?project_id=${docId}`;
});
});
document.querySelectorAll(".view-stages-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const projectId = this.getAttribute("data-id");
window.location.href = `project/stage?project_id=${projectId}`;
});
});
// 添加按钮事件
document.querySelectorAll(".view-docs-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const projectId = this.getAttribute("data-id");
window.location.href = `doc_list?project_id=${projectId}`;
});
});
document.querySelectorAll(".edit-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const projectId = this.getAttribute("data-id");
const project = projects.find((p) => p.id === projectId);
openProjectModal(project);
});
});
document.querySelectorAll(".delete-btn").forEach((btn) => {
btn.addEventListener("click", function () {
const projectId = this.getAttribute("data-id");
openConfirmModal(projectId);
});
});
}
// 显示/隐藏空状态
function showEmptyState(show, message = "暂无项目数据") {
if (show) {
document.getElementById("projects-table").style.display = "none";
emptyStateEl.style.display = "block";
emptyStateEl.querySelector("p").textContent = message;
} else {
document.getElementById("projects-table").style.display = "table";
emptyStateEl.style.display = "none";
}
}
// 过滤项目
function filterProjects() {
const searchTerm = searchInputEl.value.toLowerCase();
if (!searchTerm) {
renderProjects(projects);
return;
}
const filteredProjects = projects.filter(
(project) =>
project.name.toLowerCase().includes(searchTerm) ||
(project.description &&
project.description.toLowerCase().includes(searchTerm))
);
renderProjects(filteredProjects);
}
// 打开项目模态框
function openProjectModal(project = null) {
console.log("will open project modal");
modalTitle.textContent = project ? "编辑项目" : "添加项目";
console.log(project);
if (project) {
projectIdInput.value = project.id;
projectNameInput.value = project.name || "";
projectDescInput.value = project.description || "";
} else {
projectForm.reset();
projectIdInput.value = "";
}
projectModal.style.display = "block";
}
// 关闭项目模态框
function closeProjectModal() {
projectModal.style.display = "none";
projectForm.reset();
}
// 打开确认删除模态框
function openConfirmModal(projectId) {
currentProjectId = projectId;
confirmModal.style.display = "block";
}
// 关闭确认删除模态框
function closeConfirmModal() {
confirmModal.style.display = "none";
currentProjectId = null;
}
// 保存项目
async function saveProject() {
const projectId = projectIdInput.value;
const name = projectNameInput.value.trim();
const description = projectDescInput.value.trim();
if (!name) {
alert("请输入项目名称");
return;
}
const projectData = {
name,
description,
};
try {
let url = "http://127.0.0.1:5002/api/project/";
let method = "POST";
if (projectId) {
url += projectId;
method = "PATCH";
}
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(projectData),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
alert(projectId ? "项目更新成功" : "项目创建成功");
closeProjectModal();
fetchProjects();
} else {
alert(`操作失败: ${result.message || "未知错误"}`);
}
} catch (error) {
console.error("保存项目错误:", error);
alert(`操作失败: ${error.message}`);
}
}
// 删除项目
async function deleteProject() {
if (!currentProjectId) return;
try {
const response = await fetch(
`http://127.0.0.1:5002/api/project/${currentProjectId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
alert("项目删除成功");
closeConfirmModal();
fetchProjects();
} else {
alert(`删除失败: ${result.message || "未知错误"}`);
}
} catch (error) {
console.error("删除项目错误:", error);
alert(`删除失败: ${error.message}`);
}
}
</script>
</body>
</html>
+636
View File
@@ -0,0 +1,636 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Stage管理</title>
<link rel="stylesheet" href="/ui/assets/common.css" />
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
}
.btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #40a9ff;
}
.btn-danger {
background-color: #ff4d4f;
}
.btn-danger:hover {
background-color: #ff7875;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 500;
}
.table tr:hover {
background-color: #f5f5f5;
}
.action-cell {
display: flex;
gap: 8px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 24px;
border-radius: 4px;
width: 400px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.close {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 class="title">Stage管理</h1>
<button class="btn" id="add-stage-btn">添加Stage</button>
</div>
<div id="stage-list">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>关联树节点</th>
<!-- <th>描述</th> -->
<th>操作</th>
</tr>
</thead>
<tbody id="stage-table-body">
<!-- 数据将通过JavaScript动态加载 -->
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none">
<p>暂无Stage数据,请添加新Stage</p>
</div>
</div>
</div>
<!-- 添加/编辑Stage的模态框 -->
<div class="modal" id="stage-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modal-title">添加Stage</h2>
<span class="close" id="close-modal">&times;</span>
</div>
<form id="stage-form">
<input type="hidden" id="stage-id" value="" />
<div class="form-group">
<label class="form-label" for="stage-name">名称</label>
<input
type="text"
class="form-input"
id="stage-name"
value=""
required
/>
</div>
<div class="form-group">
<label class="form-label" for="stage-tree-id">关联树节点</label>
<select class="form-input" id="stage-tree-id" required>
<option value="">请选择树节点</option>
<!-- 树节点选项将通过JavaScript动态加载 -->
</select>
</div>
<div class="form-actions">
<button type="button" class="btn" id="cancel-btn">取消</button>
<button type="submit" class="btn" id="save-btn">保存</button>
</div>
</form>
</div>
</div>
<!-- 确认删除的模态框 -->
<div class="modal" id="confirm-modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">确认删除</h2>
<span class="close" id="close-confirm-modal">&times;</span>
</div>
<p>
确定要删除此Stage吗?此操作将同时删除该Stage下的所有图形节点和连接,且不可恢复。
</p>
<div class="form-actions">
<button type="button" class="btn" id="cancel-delete-btn">取消</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn">
删除
</button>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div class="loading" id="loading" style="display: none">
<div class="spinner"></div>
</div>
<script>
// 基础URL
const API_BASE_URL = "http://127.0.0.1:5002/api";
// DOM元素
const stageTableBody = document.getElementById("stage-table-body");
const emptyState = document.getElementById("empty-state");
const addStageBtn = document.getElementById("add-stage-btn");
const stageModal = document.getElementById("stage-modal");
const modalTitle = document.getElementById("modal-title");
const closeModalBtn = document.getElementById("close-modal");
const stageForm = document.getElementById("stage-form");
const stageIdInput = document.getElementById("stage-id");
const stageNameInput = document.getElementById("stage-name");
const stageTreeIdSelect = document.getElementById("stage-tree-id");
const cancelBtn = document.getElementById("cancel-btn");
const confirmModal = document.getElementById("confirm-modal");
const closeConfirmModal = document.getElementById("close-confirm-modal");
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
const loading = document.getElementById("loading");
const urlParams = new URLSearchParams(window.location.search);
// 当前要删除的StageID
let currentDeleteId = null;
// 存储树节点数据
let treeNodes = [];
let projectId = urlParams.get("project_id");
// 页面加载时获取Stage列表和树节点列表
function init() {
fetchTreeNodes();
fetchStages();
}
init();
// 添加Stage按钮点击事件
addStageBtn.addEventListener("click", () => {
modalTitle.textContent = "添加Stage";
stageForm.reset();
stageIdInput.value = "";
openModal(stageModal);
});
// 关闭模态框
closeModalBtn.addEventListener("click", () => closeModal(stageModal));
cancelBtn.addEventListener("click", () => closeModal(stageModal));
// 关闭确认删除模态框
closeConfirmModal.addEventListener("click", () =>
closeModal(confirmModal)
);
cancelDeleteBtn.addEventListener("click", () => closeModal(confirmModal));
// 确认删除
confirmDeleteBtn.addEventListener("click", deleteStage);
// 表单提交
stageForm.addEventListener("submit", handleFormSubmit);
// 获取所有Stage
async function fetchStages() {
showLoading();
try {
const response = await fetch(`${API_BASE_URL}/stage/${projectId}/`);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
renderStageList(result.data);
} else {
showError("获取Stage列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage列表错误:", error);
showError("获取Stage列表错误: " + error.message);
} finally {
hideLoading();
}
}
async function fetchTreeNodes() {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/project/demand/${projectId}`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("fetch nodes", response);
const result = await response.json();
if (result.code === 0 && Array.isArray(result.data)) {
treeNodes = result.data;
populateTreeSelect();
} else {
showError("获取树节点列表失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取树节点列表错误:", error);
showError("获取树节点列表错误: " + error.message);
} finally {
hideLoading();
}
}
// 加载Stage数据用于编辑
// 填充树节点选择框
function populateTreeSelect() {
stageTreeIdSelect.innerHTML = '<option value="">请选择树节点</option>';
// 添加虚拟根节点选项
/* const rootOption = document.createElement("option");
rootOption.value = "root"; // 使用特殊值"root"表示虚拟根节点
rootOption.textContent = "所有节点 (虚拟根节点)";
stageTreeIdSelect.appendChild(rootOption);
*/
treeNodes.forEach((node) => {
const option = document.createElement("option");
option.value = node.id;
option.textContent = `${node.name} (Level: ${node.level})`;
stageTreeIdSelect.appendChild(option);
});
}
function renderStageList(stages) {
stageTableBody.innerHTML = "";
if (stages.length === 0) {
emptyState.style.display = "block";
return;
}
emptyState.style.display = "none";
stages.forEach((stage) => {
const row = document.createElement("tr");
// 截断ID以便显示
const shortId = stage.id.substring(0, 8) + "...";
// 查找关联的树节点名称
let treeNodeName = "未关联";
if (stage.tree_id) {
if (stage.tree_id === "root") {
treeNodeName = "所有节点 (虚拟根节点)";
} else {
console.log("tree_id: ", stage.tree_id);
console.log(treeNodes);
const treeNode = treeNodes.find(
(node) => node.id === stage.tree_id
);
if (treeNode) {
treeNodeName = `${treeNode.name} (Level: ${treeNode.level})`;
} else {
treeNodeName = `ID: ${stage.tree_id.substring(0, 8)}...`;
}
}
}
row.innerHTML = `
<td title="${stage.id}">${shortId}</td>
<td>${stage.name || "未命名"}</td>
<td>${treeNodeName}</td>
<td class="action-cell">
<button class="btn" data-id="${
stage.id
}" data-action="view-graph">查看Graph</button>
<button class="btn" data-id="${
stage.id
}" data-action="edit">编辑</button>
<button class="btn btn-danger" data-id="${
stage.id
}" data-action="delete">删除</button>
</td>
`;
stageTableBody.appendChild(row);
});
// 添加按钮事件监听
document.querySelectorAll("[data-action]").forEach((button) => {
button.addEventListener("click", handleActionClick);
});
}
// 处理操作按钮点击
async function handleActionClick(event) {
console.log("action click");
const action = event.target.getAttribute("data-action");
const stageId = event.target.getAttribute("data-id");
switch (action) {
case "view-graph":
// 跳转到图形页面
window.location.href = `/ui/page/stageGraph.html?id=${stageId}`;
break;
case "edit":
await loadStageForEdit(stageId);
break;
case "delete":
openDeleteConfirmation(stageId);
break;
}
}
// 加载Stage数据用于编辑
async function loadStageForEdit(stageId) {
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/stage/${project_id}/${stageId}/`
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
console.log("get");
const result = await response.json();
if (result.code === 0 && result.data) {
const stage = result.data.stage;
// 填充表单
console.log(stageIdInput);
stageIdInput.value = stage.id;
stageNameInput.value = stage.name || "";
stageTreeIdSelect.value = stage.tree_id || "";
//stageLevelInput.value = stage.level || 0;
//stageDescriptionInput.innerHTML = stage.description || "";
// 更新模态框标题
modalTitle.textContent = "编辑Stage";
// 打开模态框
openModal(stageModal);
} else {
showError("获取Stage详情失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("获取Stage详情错误:", error);
showError("获取Stage详情错误: " + error.message);
} finally {
hideLoading();
}
}
async function handleFormSubmit(event) {
event.preventDefault();
const stageId = stageIdInput.value;
const isEdit = !!stageId;
const stageData = {
name: stageNameInput.value,
project_id: projectId,
tree_id: stageTreeIdSelect.value,
};
showLoading();
try {
const url = isEdit
? `${API_BASE_URL}/stage/${projectId}/${stageId}/`
: `${API_BASE_URL}/stage/${projectId}/`;
const method = isEdit ? "PATCH" : "POST";
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(stageData),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage(isEdit ? "Stage更新成功" : "Stage创建成功");
closeModal(stageModal);
fetchStages(); // 刷新列表
} else {
showError(
(isEdit ? "更新" : "创建") +
"Stage失败: " +
(result.message || "未知错误")
);
}
} catch (error) {
console.error(isEdit ? "更新Stage错误:" : "创建Stage错误:", error);
showError((isEdit ? "更新" : "创建") + "Stage错误: " + error.message);
} finally {
hideLoading();
}
}
// 打开删除确认对话框
function openDeleteConfirmation(stageId) {
currentDeleteId = stageId;
openModal(confirmModal);
}
// 删除Stage
async function deleteStage() {
if (!currentDeleteId) return;
showLoading();
try {
const response = await fetch(
`${API_BASE_URL}/stage/${project_id}/${currentDeleteId}/`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
showMessage("Stage删除成功");
closeModal(confirmModal);
fetchStages(); // 刷新列表
} else {
showError("删除Stage失败: " + (result.message || "未知错误"));
}
} catch (error) {
console.error("删除Stage错误:", error);
showError("删除Stage错误: " + error.message);
} finally {
hideLoading();
currentDeleteId = null;
}
}
// 工具函数:打开模态框
function openModal(modal) {
modal.style.display = "flex";
}
// 工具函数:关闭模态框
function closeModal(modal) {
modal.style.display = "none";
}
// 工具函数:显示加载中
function showLoading() {
loading.style.display = "flex";
}
// 工具函数:隐藏加载中
function hideLoading() {
loading.style.display = "none";
}
// 工具函数:显示消息
function showMessage(message) {
alert(message); // 简单实现,可以替换为更友好的通知
}
// 工具函数:显示错误
function showError(message) {
alert("错误: " + message); // 简单实现,可以替换为更友好的通知
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+1014
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>D3</title>
<script src="/ui/assets/d3.js"></script>
<script src="/ui/assets/echarts.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
// Declare the chart dimensions and margins.
const width = 640;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale.
console.log(window.d3);
console.log(window.echarts);
const x = window.d3
.scaleUtc()
.domain([new Date("2023-01-01"), new Date("2024-01-01")])
.range([marginLeft, width - marginRight]);
// Declare the y (vertical position) scale.
const y = window.d3
.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Create the SVG container.
const svg = window.d3
.create("svg")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(window.d3.axisBottom(x));
// Add the y-axis.
svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(window.d3.axisLeft(y));
// Append the SVG element.
$node.children[0].append(svg.node());
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="/ui/assets/echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 1100px;height:800px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
let myChart = window.echarts.init($node.children[0]);
function convertToTree(data) {
const map = {};
const roots = [];
// 首先创建所有节点的映射
data.forEach(item => {
map[item.id] = {
name: item.name,
value:item.id,
children: []
};
});
// 构建树形结构
data.forEach(item => {
const node = map[item.id];
if (!item.parent_id || item.parent_id === ''|| item.parent_id === '-1') {
roots.push(node);
} else {
const parent = map[item.parent_id];
if (parent) {
parent.children.push(node);
}
}
});
return roots;
}
// 获取数据并展示图表
async function fetchAndShowChart() {
myChart.showLoading();
try {
const response = await fetch('http://127.0.0.1:5002/api/tree/', {
method: 'GET',
});
//console.log(response)
const result = await response.json();
if (result.code === 0) {
const treeData = convertToTree(result.data);
console.log(treeData)
const option = {
tooltip: {
trigger: 'item',
triggerOn: 'mousemove'
},
series: treeData.map((root, index) => ({
type: 'tree',
name: `tree${index + 1}`,
data: [root],
top: '5%',
orient: 'vertical',
left: `${index * 30 + 5}%`,
bottom: '2%',
right: `${(treeData.length - index - 1) * 30 + 5}%`,
symbolSize: 7,
label: {
position: 'left',
verticalAlign: 'middle',
align: 'right'
},
leaves: {
label: {
position: 'right',
verticalAlign: 'middle',
align: 'left'
}
},
emphasis: {
focus: 'descendant'
},
expandAndCollapse: true,
animationDuration: 550,
animationDurationUpdate: 750
}))
};
myChart.hideLoading();
myChart.setOption(option);
}
} catch (error) {
console.error('获取数据失败:', error);
myChart.hideLoading();
}
}
// 执行获取数据和展示图表
fetchAndShowChart();
</script>
</body>
</html>
+314
View File
@@ -0,0 +1,314 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>G6 Tree View</title>
<script src="/ui/assets/g6.js"></script>
<style>
#tree_container {
width: 1100px;
height: 800px;
margin: 0 auto;
border: 1px solid #ddd;
}
.g6-tooltip {
border: 1px solid #e2e2e2;
border-radius: 4px;
font-size: 12px;
color: #545454;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 8px;
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
}
.control-panel {
padding: 10px;
margin-bottom: 10px;
}
button {
margin: 5px;
padding: 5px 10px;
}
</style>
</head>
<body>
<div class="control-panel">
<button id="add-btn">添加节点</button>
<button id="edit-btn">编辑选中节点</button>
<button id="delete-btn">删除选中节点</button>
<button id="refresh-btn">刷新树</button>
</div>
<div id="tree_container"></div>
<script>
// 全局变量
let graph = null;
let selectedNode = null;
// 将后端数据转换为G6可用的格式
function convertToG6Format(data) {
const map = {};
const roots = [];
// 创建所有节点
data.forEach(item => {
map[item.id] = {
id: item.id,
label: item.name,
style: {
fill: '#91d5ff',
stroke: '#40a9ff'
},
children: []
};
});
// 构建树结构
data.forEach(item => {
const node = map[item.id];
if (!item.parent_id || item.parent_id === '' || item.parent_id === '-1') {
roots.push(node);
} else {
const parent = map[item.parent_id];
if (parent) {
if (!parent.children) parent.children = [];
parent.children.push(node);
}
}
});
// 返回根节点数组的第一个作为主树
return roots.length > 0 ? { id: 'root', label: 'Root', children: roots } : {};
}
// 初始化G6图形
function initGraph() {
// 注册自定义节点,美化显示效果
window.G6.registerNode('tree-node', {
draw(cfg, group) {
const keyShape = group.addShape('rect', {
attrs: {
x: 0,
y: -15,
width: 120,
height: 30,
radius: 4,
fill: cfg.style ? cfg.style.fill : '#91d5ff',
stroke: cfg.style ? cfg.style.stroke : '#40a9ff',
cursor: 'pointer'
},
name: 'node-keyshape',
});
group.addShape('text', {
attrs: {
text: cfg.label,
x: 10,
y: 0,
fontSize: 14,
fill: '#333',
textAlign: 'left',
textBaseline: 'middle',
cursor: 'pointer'
},
name: 'node-label'
});
return keyShape;
}
});
// 创建G6实例
graph = new window.G6.Graph({
container: 'tree_container',
width: 1100,
height: 800,
fitView: true,
animate: true,
modes: {
default: ['drag-canvas', 'zoom-canvas', {
type: 'drag-node',
enableDelegate: true
}]
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 40;
},
getHGap: function getHGap() {
return 100;
}
},
defaultNode: {
type: 'tree-node'
},
nodeStateStyles: {
selected: {
fill: '#d9f7be',
stroke: '#73d13d'
}
}
});
// 节点点击事件
graph.on('node:click', evt => {
const { item } = evt;
const nodeId = item._cfg.id;
// 取消之前选中状态
if (selectedNode) {
graph.setItemState(selectedNode, 'selected', false);
}
// 设置新选中状态
graph.setItemState(item, 'selected', true);
selectedNode = item;
console.log('选中节点:', nodeId);
});
// 注册按钮事件
document.getElementById('add-btn').addEventListener('click', addNode);
document.getElementById('edit-btn').addEventListener('click', editNode);
document.getElementById('delete-btn').addEventListener('click', deleteNode);
document.getElementById('refresh-btn').addEventListener('click', fetchAndRenderTree);
return graph;
}
// 获取数据并渲染树
async function fetchAndRenderTree() {
try {
const response = await fetch('http://127.0.0.1:5002/api/tree/');
const result = await response.json();
if (result.code === 0) {
const treeData = convertToG6Format(result.data);
graph.data(treeData);
graph.render();
// 自动适应画布
setTimeout(() => {
graph.fitView();
}, 500);
} else {
console.error('获取数据失败:', result.message);
}
} catch (error) {
console.error('获取数据错误:', error);
}
}
// 添加节点
function addNode() {
const parentId = selectedNode ? selectedNode._cfg.id : '-1';
const newNodeName = prompt('请输入新节点名称:');
if (newNodeName) {
fetch('http://127.0.0.1:5002/api/tree/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: newNodeName,
parent_id: parentId
})
})
.then(response => response.json())
.then(data => {
alert('节点添加成功');
fetchAndRenderTree();
})
.catch(error => {
console.error('添加节点失败:', error);
});
}
}
// 编辑节点
function editNode() {
if (!selectedNode) {
alert('请先选择一个节点');
return;
}
const nodeId = selectedNode._cfg.id;
const newName = prompt('请输入新的节点名称:', selectedNode._cfg.model.label);
if (newName) {
fetch(`http://127.0.0.1:5002/api/tree/${nodeId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: newName
})
})
.then(response => response.json())
.then(data => {
alert('节点更新成功');
fetchAndRenderTree();
})
.catch(error => {
console.error('更新节点失败:', error);
});
}
}
// 删除节点
function deleteNode() {
if (!selectedNode) {
alert('请先选择一个节点');
return;
}
const nodeId = selectedNode._cfg.id;
const confirmDelete = confirm('确定删除此节点吗?');
if (confirmDelete) {
fetch(`http://127.0.0.1:5002/api/tree/${nodeId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cascade: false
})
})
.then(response => response.json())
.then(data => {
alert('节点删除成功');
selectedNode = null;
fetchAndRenderTree();
})
.catch(error => {
console.error('删除节点失败:', error);
});
}
}
// 页面加载完成后初始化图形并加载数据
$node.addEventListener('DOMContentLoaded', () => {
graph = initGraph();
fetchAndRenderTree();
});
</script>
</body>
</html>
+1017
View File
File diff suppressed because it is too large Load Diff