feat: 项目自动选中、重命名接口与长配置值展开;页面显示版本号 v1.0.<commit> build <时间>
build-and-deploy / build-and-deploy (push) Successful in 28s

This commit is contained in:
jif2.zhang
2026-09-14 09:47:59 +08:00
parent 1765caba50
commit 37c781e6b9
14 changed files with 383 additions and 66 deletions
+6 -2
View File
@@ -130,8 +130,11 @@
</div>
<footer class="statusbar">
<span id="status-text">系统管理 · 用户管理</span>
<span id="status-user"></span>
<div class="statusbar-left">
<span id="status-text">系统管理 · 用户管理</span>
<span id="status-user"></span>
</div>
<span class="statusbar-version" id="version-text"></span>
</footer>
<!-- 新增/编辑用户模态框 -->
@@ -271,6 +274,7 @@
</div>
<script src="/js/api.js"></script>
<script src="/js/version.js"></script>
<script>
var currentUser = null;
var confirmCallback = null;
+49 -14
View File
@@ -281,9 +281,32 @@ body {
color: #0451a5;
}
.config-value-cell {
font-family: Consolas, monospace;
word-break: break-all;
.config-value-cell {
font-family: Consolas, monospace;
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 {
@@ -375,17 +398,29 @@ body {
}
/* 状态栏 */
.statusbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
padding: 0 12px;
background: #f0f0f0;
border-top: 1px solid #c0c0c0;
font-size: 12px;
color: #666;
flex-shrink: 0;
.statusbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
padding: 0 12px;
background: #f0f0f0;
border-top: 1px solid #c0c0c0;
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.statusbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.statusbar-version {
white-space: nowrap;
font-family: Consolas, monospace;
color: #888;
}
/* 模态框 */
+8 -4
View File
@@ -123,9 +123,12 @@
</div>
<!-- 状态栏 -->
<footer class="statusbar">
<span id="status-text">就绪</span>
<span id="status-mode">当前模式:编辑</span>
<footer class="statusbar">
<div class="statusbar-left">
<span id="status-text">就绪</span>
<span id="status-mode">当前模式:编辑</span>
</div>
<span class="statusbar-version" id="version-text"></span>
</footer>
<!-- 模态框:新建项目 -->
@@ -348,6 +351,7 @@
window.location.href = "/login.html";
});
</script>
<script src="/js/app.js"></script>
<script src="/js/app.js"></script>
<script src="/js/version.js"></script>
</body>
</html>
+4
View File
@@ -71,6 +71,10 @@ const API = {
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) {
+67 -22
View File
@@ -290,17 +290,48 @@ function renderConfigTable(configs, path) {
* @param {boolean} exists - 是否存在
* @returns {string} 值列 HTML
*/
function buildValueCell(cfg, exists) {
if (!exists) return '<span class="value-missing">—</span>';
const value = String(cfg.value);
if (value.trim() !== "") return escapeHtml(value);
const hasDefault =
cfg.defaultValue !== undefined && cfg.defaultValue !== null && String(cfg.defaultValue).trim() !== "";
if (hasDefault) {
return `<span class="value-default" title="配置值为空,回退展示默认值">默认 ${escapeHtml(String(cfg.defaultValue))}</span>`;
}
return "";
}
function buildValueCell(cfg, exists) {
if (!exists) return '<span class="value-missing">—</span>';
const value = String(cfg.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 =
cfg.defaultValue !== undefined && cfg.defaultValue !== null && String(cfg.defaultValue).trim() !== "";
if (hasDefault) {
return `<span class="value-default" title="配置值为空,回退展示默认值">默认 ${escapeHtml(String(cfg.defaultValue))}</span>`;
}
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);
/**
* 根据当前项目维度开关,控制配置模态框中维度字段的显示
@@ -849,17 +880,31 @@ function bindResizer() {
* 应用初始化入口
* @returns {Promise<void>}
*/
async function init() {
bindEvents();
try {
const modeData = await API.getMode();
state.mode = modeData.mode;
} catch {
state.mode = "edit";
}
applyMode();
await loadProjects();
setStatus("就绪");
async function init() {
bindEvents();
try {
const modeData = await API.getMode();
state.mode = modeData.mode;
} catch {
state.mode = "edit";
}
applyMode();
await loadProjects();
// 从项目列表跳转进入时,依据 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("就绪");
}
}
// 页面加载完成后启动
+26
View File
@@ -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
View File
@@ -51,8 +51,19 @@
</form>
</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>
<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>
// 已登录则直接跳转
if (localStorage.getItem("registry_token")) {
+66 -3
View File
@@ -26,6 +26,8 @@
.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: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-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; }
@@ -80,8 +82,11 @@
</div>
<footer class="statusbar">
<span id="status-text">加载中...</span>
<span id="status-user"></span>
<div class="statusbar-left">
<span id="status-text">加载中...</span>
<span id="status-user"></span>
</div>
<span class="statusbar-version" id="version-text"></span>
</footer>
<!-- 新建项目模态框(复用 index.html 中的结构) -->
@@ -138,7 +143,28 @@
</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/version.js"></script>
<script>
var currentUser = null;
var confirmCallback = null;
@@ -203,6 +229,9 @@
return;
}
var selectedId = null;
try { selectedId = localStorage.getItem("registry_selected_project") || null; } catch (e) {}
var dims = [];
grid.innerHTML = projects.map(function (p) {
dims = [];
@@ -211,7 +240,8 @@
if (p.enableGroup) dims.push("🗂 分组");
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") : "";
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-icon">📦</div>' +
'<div><div class="project-card-title">' + esc(p.name) + '</div>' +
@@ -223,6 +253,7 @@
"<span>" + time + "</span>" +
'<div class="project-card-actions">' +
'<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>" +
"</div>" +
"</div>" +
@@ -240,9 +271,26 @@
}
function openProject(id) {
try { localStorage.setItem("registry_selected_project", id); } catch (e) {}
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) {
openConfirm("确认删除项目「" + name + "」?此操作将同时删除该项目下的所有配置,且不可恢复。", async function () {
try {
@@ -259,6 +307,21 @@
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-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 () {
var name = document.getElementById("project-name").value.trim();
var desc = document.getElementById("project-description").value.trim();