feat: 项目自动选中、重命名接口与长配置值展开;页面显示版本号 v1.0.<commit> build <时间>
build-and-deploy / build-and-deploy (push) Successful in 28s
build-and-deploy / build-and-deploy (push) Successful in 28s
This commit is contained in:
@@ -32,6 +32,8 @@ jobs:
|
|||||||
- name: 构建 Docker 镜像
|
- name: 构建 Docker 镜像
|
||||||
run: |
|
run: |
|
||||||
docker build \
|
docker build \
|
||||||
|
--build-arg GIT_COMMIT=${GITHUB_SHA} \
|
||||||
|
--build-arg BUILD_TIME=$(date +%Y%m%d%H%M%S) \
|
||||||
-t "${IMAGE_NAME}:${{ steps.meta.outputs.tag }}" \
|
-t "${IMAGE_NAME}:${{ steps.meta.outputs.tag }}" \
|
||||||
-t "${IMAGE_NAME}:latest" \
|
-t "${IMAGE_NAME}:latest" \
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ FROM node:22-alpine
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 构建元数据(由 CI 传入;未传入时回退到运行时本地 git 信息)
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
ARG BUILD_TIME=unknown
|
||||||
|
ENV GIT_COMMIT=${GIT_COMMIT}
|
||||||
|
ENV BUILD_TIME=${BUILD_TIME}
|
||||||
|
|
||||||
# 先复制清单以利用镜像层缓存
|
# 先复制清单以利用镜像层缓存
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -130,8 +130,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="statusbar">
|
<footer class="statusbar">
|
||||||
<span id="status-text">系统管理 · 用户管理</span>
|
<div class="statusbar-left">
|
||||||
<span id="status-user"></span>
|
<span id="status-text">系统管理 · 用户管理</span>
|
||||||
|
<span id="status-user"></span>
|
||||||
|
</div>
|
||||||
|
<span class="statusbar-version" id="version-text"></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- 新增/编辑用户模态框 -->
|
<!-- 新增/编辑用户模态框 -->
|
||||||
@@ -271,6 +274,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
|
<script src="/js/version.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var currentUser = null;
|
var currentUser = null;
|
||||||
var confirmCallback = null;
|
var confirmCallback = null;
|
||||||
|
|||||||
@@ -286,6 +286,29 @@ body {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value-truncated {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-more-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
border: 1px solid #5b9bd5;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #e5f1fb;
|
||||||
|
color: #0078d4;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-more-btn:hover {
|
||||||
|
background: #d0e5f7;
|
||||||
|
}
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
@@ -388,6 +411,18 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar-version {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
/* 模态框 */
|
/* 模态框 */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
+7
-3
@@ -123,9 +123,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 状态栏 -->
|
<!-- 状态栏 -->
|
||||||
<footer class="statusbar">
|
<footer class="statusbar">
|
||||||
<span id="status-text">就绪</span>
|
<div class="statusbar-left">
|
||||||
<span id="status-mode">当前模式:编辑</span>
|
<span id="status-text">就绪</span>
|
||||||
|
<span id="status-mode">当前模式:编辑</span>
|
||||||
|
</div>
|
||||||
|
<span class="statusbar-version" id="version-text"></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- 模态框:新建项目 -->
|
<!-- 模态框:新建项目 -->
|
||||||
@@ -349,5 +352,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
<script src="/js/version.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ const API = {
|
|||||||
return this.request(`/api/projects/${id}/mappings`, { method: "PUT", body: mappings });
|
return this.request(`/api/projects/${id}/mappings`, { method: "PUT", body: mappings });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProject(id, data) {
|
||||||
|
return this.request(`/api/projects/${id}`, { method: "PUT", body: data });
|
||||||
|
},
|
||||||
|
|
||||||
// ========== 配置 ==========
|
// ========== 配置 ==========
|
||||||
|
|
||||||
getConfigs(projectId) {
|
getConfigs(projectId) {
|
||||||
|
|||||||
+47
-2
@@ -293,7 +293,15 @@ function renderConfigTable(configs, path) {
|
|||||||
function buildValueCell(cfg, exists) {
|
function buildValueCell(cfg, exists) {
|
||||||
if (!exists) return '<span class="value-missing">—</span>';
|
if (!exists) return '<span class="value-missing">—</span>';
|
||||||
const value = String(cfg.value);
|
const value = String(cfg.value);
|
||||||
if (value.trim() !== "") return escapeHtml(value);
|
if (value.trim() !== "") {
|
||||||
|
if (value.length > 40) {
|
||||||
|
return (
|
||||||
|
`<span class="value-truncated" data-full="${escapeHtml(value)}">${escapeHtml(value.slice(0, 40))}…</span>` +
|
||||||
|
`<button type="button" class="value-more-btn" title="显示完整值">更多</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return escapeHtml(value);
|
||||||
|
}
|
||||||
const hasDefault =
|
const hasDefault =
|
||||||
cfg.defaultValue !== undefined && cfg.defaultValue !== null && String(cfg.defaultValue).trim() !== "";
|
cfg.defaultValue !== undefined && cfg.defaultValue !== null && String(cfg.defaultValue).trim() !== "";
|
||||||
if (hasDefault) {
|
if (hasDefault) {
|
||||||
@@ -302,6 +310,29 @@ function buildValueCell(cfg, exists) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 值列"更多"按钮:展开/收起完整值(事件委托,仅在长值单元格内生效)
|
||||||
|
* @param {Event} e - 点击事件
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onValueMoreClick(e) {
|
||||||
|
const btn = e.target.closest(".value-more-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
const span = btn.previousElementSibling;
|
||||||
|
if (!span || !span.classList.contains("value-truncated")) return;
|
||||||
|
|
||||||
|
if (btn.dataset.expanded === "1") {
|
||||||
|
span.innerHTML = escapeHtml(span.dataset.full.slice(0, 40)) + "…";
|
||||||
|
btn.textContent = "更多";
|
||||||
|
btn.dataset.expanded = "";
|
||||||
|
} else {
|
||||||
|
span.innerHTML = escapeHtml(span.dataset.full);
|
||||||
|
btn.textContent = "收起";
|
||||||
|
btn.dataset.expanded = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("click", onValueMoreClick);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据当前项目维度开关,控制配置模态框中维度字段的显示
|
* 根据当前项目维度开关,控制配置模态框中维度字段的显示
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
@@ -859,7 +890,21 @@ async function init() {
|
|||||||
}
|
}
|
||||||
applyMode();
|
applyMode();
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
setStatus("就绪");
|
|
||||||
|
// 从项目列表跳转进入时,依据 URL 参数或上次选中项自动选中项目
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
let projectId = params.get("project");
|
||||||
|
if (!projectId) {
|
||||||
|
try { projectId = localStorage.getItem("registry_selected_project"); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (projectId && state.projects.some((p) => p.id === projectId)) {
|
||||||
|
const selector = document.getElementById("project-selector");
|
||||||
|
if (selector) selector.value = projectId;
|
||||||
|
try { localStorage.setItem("registry_selected_project", projectId); } catch (e) {}
|
||||||
|
await selectProject(projectId);
|
||||||
|
} else {
|
||||||
|
setStatus("就绪");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载完成后启动
|
// 页面加载完成后启动
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 版本号加载器
|
||||||
|
* 读取 /api/version 并将版本号填充到页面元素的 #version-text(或 .statusbar-version)
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
function fill(text) {
|
||||||
|
var el = document.getElementById("version-text");
|
||||||
|
if (!el) el = document.querySelector(".statusbar-version");
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetch("/api/version")
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok) throw new Error("bad status");
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (data && data.success && data.data) {
|
||||||
|
fill(data.data.version || "");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {});
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
+12
-1
@@ -51,8 +51,19 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-footer">Registry Config Center</div>
|
<div class="login-footer">Registry Config Center <span id="login-version" class="login-version"></span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
// 加载版本号
|
||||||
|
fetch("/api/version")
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data && data.success && data.data && data.data.version) {
|
||||||
|
document.getElementById("login-version").textContent = data.data.version;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {});
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// 已登录则直接跳转
|
// 已登录则直接跳转
|
||||||
if (localStorage.getItem("registry_token")) {
|
if (localStorage.getItem("registry_token")) {
|
||||||
|
|||||||
+66
-3
@@ -26,6 +26,8 @@
|
|||||||
.projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
|
.projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
|
||||||
.project-card { background: #fff; border: 1px solid #dcdcdc; border-radius: 6px; padding: 14px 16px 12px; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; display: flex; flex-direction: column; }
|
.project-card { background: #fff; border: 1px solid #dcdcdc; border-radius: 6px; padding: 14px 16px 12px; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; display: flex; flex-direction: column; }
|
||||||
.project-card:hover { border-color: #5b9bd5; box-shadow: 0 4px 14px rgba(0,120,212,0.15); transform: translateY(-1px); }
|
.project-card:hover { border-color: #5b9bd5; box-shadow: 0 4px 14px rgba(0,120,212,0.15); transform: translateY(-1px); }
|
||||||
|
.project-card.selected { border-color: #0078d4; box-shadow: 0 0 0 1px #0078d4 inset, 0 4px 14px rgba(0,120,212,0.2); }
|
||||||
|
.project-card.selected .project-card-title::after { content: "✓"; margin-left: 6px; font-size: 13px; color: #0078d4; font-weight: 700; }
|
||||||
.project-card-top { display: flex; align-items: flex-start; gap: 10px; }
|
.project-card-top { display: flex; align-items: flex-start; gap: 10px; }
|
||||||
.project-icon { width: 34px; height: 34px; border-radius: 6px; background: #e5f1fb; color: #0078d4; display: flex; align-items: center; justify-content: center; font-size: 17px; flex-shrink: 0; }
|
.project-icon { width: 34px; height: 34px; border-radius: 6px; background: #e5f1fb; color: #0078d4; display: flex; align-items: center; justify-content: center; font-size: 17px; flex-shrink: 0; }
|
||||||
.project-card-title { font-size: 15px; font-weight: 600; color: #003a70; line-height: 1.3; }
|
.project-card-title { font-size: 15px; font-weight: 600; color: #003a70; line-height: 1.3; }
|
||||||
@@ -80,8 +82,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="statusbar">
|
<footer class="statusbar">
|
||||||
<span id="status-text">加载中...</span>
|
<div class="statusbar-left">
|
||||||
<span id="status-user"></span>
|
<span id="status-text">加载中...</span>
|
||||||
|
<span id="status-user"></span>
|
||||||
|
</div>
|
||||||
|
<span class="statusbar-version" id="version-text"></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- 新建项目模态框(复用 index.html 中的结构) -->
|
<!-- 新建项目模态框(复用 index.html 中的结构) -->
|
||||||
@@ -138,7 +143,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 重命名项目模态框 -->
|
||||||
|
<div class="modal-overlay" id="modal-rename" style="display:none">
|
||||||
|
<div class="modal" style="width:360px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>重命名项目</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('modal-rename')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>项目名称 <span class="required">*</span></label>
|
||||||
|
<input type="text" id="rename-project-name" placeholder="新的项目名称">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('modal-rename')">取消</button>
|
||||||
|
<button class="btn btn-primary" id="btn-save-rename">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
|
<script src="/js/version.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var currentUser = null;
|
var currentUser = null;
|
||||||
var confirmCallback = null;
|
var confirmCallback = null;
|
||||||
@@ -203,6 +229,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectedId = null;
|
||||||
|
try { selectedId = localStorage.getItem("registry_selected_project") || null; } catch (e) {}
|
||||||
|
|
||||||
var dims = [];
|
var dims = [];
|
||||||
grid.innerHTML = projects.map(function (p) {
|
grid.innerHTML = projects.map(function (p) {
|
||||||
dims = [];
|
dims = [];
|
||||||
@@ -211,7 +240,8 @@
|
|||||||
if (p.enableGroup) dims.push("🗂 分组");
|
if (p.enableGroup) dims.push("🗂 分组");
|
||||||
var badges = dims.map(function (d) { return '<span class="dim-chip">' + d + "</span>"; }).join("");
|
var badges = dims.map(function (d) { return '<span class="dim-chip">' + d + "</span>"; }).join("");
|
||||||
var time = p.updatedAt ? new Date(p.updatedAt).toLocaleDateString("zh-CN") : "";
|
var time = p.updatedAt ? new Date(p.updatedAt).toLocaleDateString("zh-CN") : "";
|
||||||
return '<div class="project-card" data-id="' + p.id + '" onclick="openProject(\'' + p.id + "')\">" +
|
var selectedCls = p.id === selectedId ? " selected" : "";
|
||||||
|
return '<div class="project-card' + selectedCls + '" data-id="' + p.id + '" onclick="selectProject(\'' + p.id + "')\">" +
|
||||||
'<div class="project-card-top">' +
|
'<div class="project-card-top">' +
|
||||||
'<div class="project-icon">📦</div>' +
|
'<div class="project-icon">📦</div>' +
|
||||||
'<div><div class="project-card-title">' + esc(p.name) + '</div>' +
|
'<div><div class="project-card-title">' + esc(p.name) + '</div>' +
|
||||||
@@ -223,6 +253,7 @@
|
|||||||
"<span>" + time + "</span>" +
|
"<span>" + time + "</span>" +
|
||||||
'<div class="project-card-actions">' +
|
'<div class="project-card-actions">' +
|
||||||
'<button class="row-btn" onclick="event.stopPropagation();openProject(\'' + p.id + "')\">进入</button>" +
|
'<button class="row-btn" onclick="event.stopPropagation();openProject(\'' + p.id + "')\">进入</button>" +
|
||||||
|
'<button class="row-btn" onclick="event.stopPropagation();openRename(\'' + p.id + "', '" + esc(p.name) + "')\">重命名</button>" +
|
||||||
'<button class="row-btn danger" onclick="event.stopPropagation();confirmDelete(\'' + p.id + "', '" + esc(p.name) + "')\">删除</button>" +
|
'<button class="row-btn danger" onclick="event.stopPropagation();confirmDelete(\'' + p.id + "', '" + esc(p.name) + "')\">删除</button>" +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
@@ -240,9 +271,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openProject(id) {
|
function openProject(id) {
|
||||||
|
try { localStorage.setItem("registry_selected_project", id); } catch (e) {}
|
||||||
window.location.href = "/index.html?project=" + encodeURIComponent(id);
|
window.location.href = "/index.html?project=" + encodeURIComponent(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectProject(id) {
|
||||||
|
try { localStorage.setItem("registry_selected_project", id); } catch (e) {}
|
||||||
|
var cards = document.querySelectorAll(".project-card");
|
||||||
|
cards.forEach(function (c) { c.classList.remove("selected"); });
|
||||||
|
var card = document.querySelector('.project-card[data-id="' + id + '"]');
|
||||||
|
if (card) card.classList.add("selected");
|
||||||
|
openProject(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var renameProjectId = null;
|
||||||
|
function openRename(id, name) {
|
||||||
|
renameProjectId = id;
|
||||||
|
document.getElementById("rename-project-name").value = name;
|
||||||
|
openModal("modal-rename");
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete(id, name) {
|
function confirmDelete(id, name) {
|
||||||
openConfirm("确认删除项目「" + name + "」?此操作将同时删除该项目下的所有配置,且不可恢复。", async function () {
|
openConfirm("确认删除项目「" + name + "」?此操作将同时删除该项目下的所有配置,且不可恢复。", async function () {
|
||||||
try {
|
try {
|
||||||
@@ -259,6 +307,21 @@
|
|||||||
document.getElementById("btn-new-project").addEventListener("click", function () { openModal("modal-project"); });
|
document.getElementById("btn-new-project").addEventListener("click", function () { openModal("modal-project"); });
|
||||||
document.getElementById("btn-new-project-2").addEventListener("click", function () { openModal("modal-project"); });
|
document.getElementById("btn-new-project-2").addEventListener("click", function () { openModal("modal-project"); });
|
||||||
|
|
||||||
|
document.getElementById("btn-save-rename").addEventListener("click", async function () {
|
||||||
|
var name = document.getElementById("rename-project-name").value.trim();
|
||||||
|
if (!name) { toast("请输入项目名称", "error"); return; }
|
||||||
|
if (!renameProjectId) { toast("缺少项目信息", "error"); return; }
|
||||||
|
try {
|
||||||
|
await API.updateProject(renameProjectId, { name: name });
|
||||||
|
closeModal("modal-rename");
|
||||||
|
renameProjectId = null;
|
||||||
|
toast("项目已重命名", "success");
|
||||||
|
loadProjects();
|
||||||
|
} catch (err) {
|
||||||
|
toast("重命名失败: " + err.message, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("btn-save-project").addEventListener("click", async function () {
|
document.getElementById("btn-save-project").addEventListener("click", async function () {
|
||||||
var name = document.getElementById("project-name").value.trim();
|
var name = document.getElementById("project-name").value.trim();
|
||||||
var desc = document.getElementById("project-description").value.trim();
|
var desc = document.getElementById("project-description").value.trim();
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ router.post("/", (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/projects/:id
|
||||||
|
* 更新项目基本信息(重命名/修改描述)
|
||||||
|
* Body: { name, description }
|
||||||
|
*/
|
||||||
|
router.put("/:id", (req, res) => {
|
||||||
|
try {
|
||||||
|
const project = projectService.updateProject(req.params.id, req.body);
|
||||||
|
res.json({ success: true, data: project });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/projects/:id/mappings
|
* PUT /api/projects/:id/mappings
|
||||||
* 更新项目的维度映射方式(path/label)
|
* 更新项目的维度映射方式(path/label)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const userRoutes = require("./routes/users");
|
|||||||
const tokenRoutes = require("./routes/tokens");
|
const tokenRoutes = require("./routes/tokens");
|
||||||
const { loadSystem, saveSystem } = require("./persistence/jsonAdapter");
|
const { loadSystem, saveSystem } = require("./persistence/jsonAdapter");
|
||||||
const userService = require("./services/userService");
|
const userService = require("./services/userService");
|
||||||
|
const versionInfo = require("./version");
|
||||||
|
|
||||||
// MCP 服务(Model Context Protocol,供 AI 自动操作)
|
// MCP 服务(Model Context Protocol,供 AI 自动操作)
|
||||||
const mcpRoutes = require("./mcp");
|
const mcpRoutes = require("./mcp");
|
||||||
@@ -49,6 +50,18 @@ app.get("/", (req, res) => {
|
|||||||
res.redirect("/login.html");
|
res.redirect("/login.html");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/version
|
||||||
|
* 获取系统版本号(v1.0.<commit hash> build <yyyyMMdd HHmmss>)
|
||||||
|
*/
|
||||||
|
app.get("/api/version", (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json({ success: true, data: versionInfo.getVersionInfo() });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/mode
|
* GET /api/mode
|
||||||
* 获取当前系统模式
|
* 获取当前系统模式
|
||||||
|
|||||||
@@ -84,6 +84,34 @@ function createProject(data) {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新项目基本信息(重命名/修改描述)
|
||||||
|
* 仅允许修改 name 与 description,维度开关与映射方式不受影响
|
||||||
|
* @param {string} id - 项目 ID
|
||||||
|
* @param {Object} data - 待更新字段
|
||||||
|
* @param {string} [data.name] - 新项目名称
|
||||||
|
* @param {string} [data.description] - 新项目描述
|
||||||
|
* @returns {Object} 更新后的项目对象
|
||||||
|
*/
|
||||||
|
function updateProject(id, data) {
|
||||||
|
const projects = loadProjects();
|
||||||
|
const index = projects.findIndex((p) => p.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error("项目不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = (data.name === undefined ? projects[index].name : data.name || "");
|
||||||
|
if (!name.trim()) {
|
||||||
|
throw new Error("项目名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
projects[index].name = name.trim();
|
||||||
|
projects[index].description = (data.description === undefined ? projects[index].description : data.description || "").trim();
|
||||||
|
projects[index].updatedAt = new Date().toISOString();
|
||||||
|
saveProjects(projects);
|
||||||
|
return projects[index];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新项目的维度映射方式
|
* 更新项目的维度映射方式
|
||||||
* 仅允许修改已启用维度的映射方式,维度开关本身不可变
|
* 仅允许修改已启用维度的映射方式,维度开关本身不可变
|
||||||
@@ -137,6 +165,7 @@ module.exports = {
|
|||||||
getAllProjects,
|
getAllProjects,
|
||||||
getProjectById,
|
getProjectById,
|
||||||
createProject,
|
createProject,
|
||||||
|
updateProject,
|
||||||
updateMappings,
|
updateMappings,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 版本信息
|
||||||
|
* 版本号格式: v1.0.<git commit hash> build <yyyyMMdd HHmmss>
|
||||||
|
* - commit hash: 优先取环境变量 GIT_COMMIT(Docker 构建时传入),否则本地 git rev-parse
|
||||||
|
* - build time: 优先取环境变量 BUILD_TIME(Docker 构建时传入),否则为进程启动时刻
|
||||||
|
*/
|
||||||
|
const { execFileSync } = require("child_process");
|
||||||
|
|
||||||
|
function pad(n) {
|
||||||
|
return String(n).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(d) {
|
||||||
|
return (
|
||||||
|
d.getFullYear() +
|
||||||
|
pad(d.getMonth() + 1) +
|
||||||
|
pad(d.getDate()) +
|
||||||
|
" " +
|
||||||
|
pad(d.getHours()) +
|
||||||
|
pad(d.getMinutes()) +
|
||||||
|
pad(d.getSeconds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCommitHash() {
|
||||||
|
if (process.env.GIT_COMMIT && process.env.GIT_COMMIT !== "unknown" && process.env.GIT_COMMIT !== "") {
|
||||||
|
return process.env.GIT_COMMIT;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const hash = execFileSync("git", ["rev-parse", "--short=8", "HEAD"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
return hash || "unknown";
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBuildTime() {
|
||||||
|
if (process.env.BUILD_TIME && process.env.BUILD_TIME !== "unknown" && process.env.BUILD_TIME !== "") {
|
||||||
|
return process.env.BUILD_TIME;
|
||||||
|
}
|
||||||
|
return formatDateTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitHash = resolveCommitHash();
|
||||||
|
const buildTime = resolveBuildTime();
|
||||||
|
const versionString = `v1.0.${commitHash} build ${buildTime}`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: versionString,
|
||||||
|
commit: commitHash,
|
||||||
|
buildTime,
|
||||||
|
getVersion() {
|
||||||
|
return versionString;
|
||||||
|
},
|
||||||
|
getVersionInfo() {
|
||||||
|
return { version: versionString, commit: commitHash, buildTime };
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user