All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
- 后端: Auth(登录/登出/me + session 中间件)、Users、Tokens 路由与 Service, scrypt 哈希+timingSafeEqual 校验、requireAdmin 保护、admin 用户自动初始化 - 前端: login.html(登录页, 无账号密码提示)、admin.html(用户+Token 管理, 令牌明文显隐/复制/吊销/重置密码)、标题栏用户信息+系统管理入口 - 持久化: jsonAdapter 支持 users/api-tokens,运行时数据忽略入库 - 文档: 需求.md/原型设计.md 同步
316 lines
15 KiB
HTML
316 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>项目管理 - Registry 配置管理中心</title>
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
/* 标题栏右侧 */
|
||
.titlebar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 12px; }
|
||
.titlebar-user { color: #e8f1fb; display: flex; align-items: center; gap: 6px; }
|
||
.role-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.35); }
|
||
.role-badge.admin { background: #ffb900; border-color: #ffd666; color: #4a3200; font-weight: 600; }
|
||
.titlebar-link { color: #cfe3f7; text-decoration: none; cursor: pointer; padding-left: 14px; border-left: 1px solid rgba(255,255,255,0.3); }
|
||
.titlebar-link:hover { color: #fff; text-decoration: underline; }
|
||
/* 内容区 */
|
||
.page-content { flex: 1; overflow: auto; padding: 18px; }
|
||
.page-heading { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
|
||
.page-heading h2 { font-size: 16px; color: #003a70; }
|
||
.page-heading .sub { font-size: 12px; color: #888; }
|
||
.content-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
|
||
.content-toolbar .spacer { flex: 1; }
|
||
.search-input { height: 30px; width: 240px; padding: 0 10px; border: 1px solid #adadad; border-radius: 3px; font-size: 13px; font-family: inherit; }
|
||
.filter-select { height: 30px; padding: 0 8px; border: 1px solid #adadad; border-radius: 3px; background: #fff; font-size: 12px; }
|
||
/* 项目卡片 */
|
||
.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-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; }
|
||
.project-card-id { font-family: Consolas, monospace; font-size: 11px; color: #999; margin-top: 2px; }
|
||
.project-card-desc { margin-top: 10px; font-size: 12px; color: #666; line-height: 1.5; min-height: 36px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.project-card-badges { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
||
.dim-chip { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; background: #eef4fb; color: #0a4a86; border: 1px solid #d0dced; }
|
||
.project-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding-top: 10px; border-top: 1px solid #f0f0f0; font-size: 11px; color: #999; }
|
||
.project-card-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; }
|
||
.project-card:hover .project-card-actions { opacity: 1; }
|
||
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
|
||
.empty-state .empty-icon { font-size: 36px; opacity: 0.4; }
|
||
.empty-state p { margin-top: 8px; font-size: 13px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="titlebar">
|
||
<span class="app-title">📋 Registry 配置管理中心</span>
|
||
<span class="current-project">项目管理</span>
|
||
<div class="titlebar-right">
|
||
<span class="titlebar-user">
|
||
👤 <span id="user-name"></span>
|
||
<span class="role-badge" id="user-role"></span>
|
||
</span>
|
||
<a class="titlebar-link" id="admin-entry" href="/admin.html" style="display:none">⚙ 系统管理</a>
|
||
<a class="titlebar-link" id="logout-link" href="#">退出登录</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="toolbar">
|
||
<div class="tool-group">
|
||
<span class="group-label">项目</span>
|
||
<button class="toolbar-btn" id="btn-refresh" title="刷新列表">🔄 刷新</button>
|
||
<button class="toolbar-btn" id="btn-new-project" title="新建项目">📁 新建</button>
|
||
</div>
|
||
<div class="tool-group">
|
||
<span class="group-label" id="project-count">共 0 个项目</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-content">
|
||
<div class="page-heading">
|
||
<h2>我的项目</h2>
|
||
<span class="sub">选择一个项目进入配置管理</span>
|
||
</div>
|
||
<div class="content-toolbar">
|
||
<input type="text" class="search-input" id="project-search" placeholder="🔍 搜索项目名称或描述">
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-primary" style="height:30px" id="btn-new-project-2">➕ 新建项目</button>
|
||
</div>
|
||
<div class="projects-grid" id="projects-grid"></div>
|
||
</div>
|
||
|
||
<footer class="statusbar">
|
||
<span id="status-text">加载中...</span>
|
||
<span id="status-user"></span>
|
||
</footer>
|
||
|
||
<!-- 新建项目模态框(复用 index.html 中的结构) -->
|
||
<div class="modal-overlay" id="modal-project" style="display:none">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>新建项目</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-project')">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>项目名称 <span class="required">*</span></label>
|
||
<input type="text" id="project-name" placeholder="例如:用户服务">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>项目描述</label>
|
||
<textarea id="project-description" rows="2" placeholder="项目配置说明"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>维度配置 <span class="hint">(创建后不可修改)</span></label>
|
||
<div class="dim-config-row">
|
||
<label class="checkbox-label"><input type="checkbox" id="project-enable-idc" checked> 机房</label>
|
||
<select id="project-idc-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
|
||
</div>
|
||
<div class="dim-config-row">
|
||
<label class="checkbox-label"><input type="checkbox" id="project-enable-env" checked> 环境</label>
|
||
<select id="project-env-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
|
||
</div>
|
||
<div class="dim-config-row">
|
||
<label class="checkbox-label"><input type="checkbox" id="project-enable-group" checked> 分组</label>
|
||
<select id="project-group-mapping" class="mapping-select"><option value="path">映射为路径</option><option value="label">仅作标签</option></select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('modal-project')">取消</button>
|
||
<button class="btn btn-primary" id="btn-save-project">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认删除模态框 -->
|
||
<div class="modal-overlay" id="modal-confirm" style="display:none">
|
||
<div class="modal" style="width:360px">
|
||
<div class="modal-header">
|
||
<h3>确认操作</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-confirm')">✕</button>
|
||
</div>
|
||
<div class="modal-body"><p id="confirm-message"></p></div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('modal-confirm')">取消</button>
|
||
<button class="btn btn-danger" id="btn-confirm-ok">确认</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script>
|
||
var currentUser = null;
|
||
var confirmCallback = null;
|
||
|
||
function toast(message, type) {
|
||
var el = document.createElement("div");
|
||
el.className = "toast toast-" + (type || "info");
|
||
el.textContent = message;
|
||
document.body.appendChild(el);
|
||
setTimeout(function () { el.remove(); }, 2500);
|
||
}
|
||
|
||
function openModal(id) { document.getElementById(id).style.display = "flex"; }
|
||
function closeModal(id) { document.getElementById(id).style.display = "none"; }
|
||
|
||
function openConfirm(message, callback) {
|
||
document.getElementById("confirm-message").textContent = message;
|
||
confirmCallback = callback;
|
||
openModal("modal-confirm");
|
||
}
|
||
document.getElementById("btn-confirm-ok").addEventListener("click", function () {
|
||
closeModal("modal-confirm");
|
||
if (confirmCallback) confirmCallback();
|
||
confirmCallback = null;
|
||
});
|
||
|
||
// 初始化
|
||
async function init() {
|
||
try {
|
||
var data = await API.getMe();
|
||
currentUser = data;
|
||
} catch (e) {
|
||
window.location.href = "/login.html";
|
||
return;
|
||
}
|
||
document.getElementById("user-name").textContent = currentUser.displayName || currentUser.username;
|
||
var roleEl = document.getElementById("user-role");
|
||
if (currentUser.role === "admin") {
|
||
roleEl.textContent = "管理员";
|
||
roleEl.className = "role-badge admin";
|
||
document.getElementById("admin-entry").style.display = "";
|
||
}
|
||
document.getElementById("status-user").textContent = currentUser.username;
|
||
await loadProjects();
|
||
}
|
||
|
||
async function loadProjects() {
|
||
try {
|
||
var projects = await API.getProjects();
|
||
renderProjects(projects);
|
||
} catch (err) {
|
||
toast("加载项目失败: " + err.message, "error");
|
||
}
|
||
}
|
||
|
||
function renderProjects(projects) {
|
||
var grid = document.getElementById("projects-grid");
|
||
if (!projects.length) {
|
||
grid.innerHTML = '<div class="empty-state"><div class="empty-icon">📁</div><p>暂无项目,点击「新建项目」创建第一个</p></div>';
|
||
document.getElementById("project-count").textContent = "共 0 个项目";
|
||
document.getElementById("status-text").textContent = "已加载 0 个项目";
|
||
return;
|
||
}
|
||
|
||
var dims = [];
|
||
grid.innerHTML = projects.map(function (p) {
|
||
dims = [];
|
||
if (p.enableIdc) dims.push("🏢 机房");
|
||
if (p.enableEnvironment) dims.push("🌐 环境");
|
||
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 + "')\">" +
|
||
'<div class="project-card-top">' +
|
||
'<div class="project-icon">📦</div>' +
|
||
'<div><div class="project-card-title">' + esc(p.name) + '</div>' +
|
||
'<div class="project-card-id">' + esc(p.id) + "</div></div>" +
|
||
"</div>" +
|
||
'<div class="project-card-desc">' + esc(p.description || "") + "</div>" +
|
||
'<div class="project-card-badges">' + badges + "</div>" +
|
||
'<div class="project-card-footer">' +
|
||
"<span>" + time + "</span>" +
|
||
'<div class="project-card-actions">' +
|
||
'<button class="row-btn" onclick="event.stopPropagation();openProject(\'' + p.id + "')\">进入</button>" +
|
||
'<button class="row-btn danger" onclick="event.stopPropagation();confirmDelete(\'' + p.id + "', '" + esc(p.name) + "')\">删除</button>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}).join("");
|
||
|
||
document.getElementById("project-count").textContent = "共 " + projects.length + " 个项目";
|
||
document.getElementById("status-text").textContent = "已加载 " + projects.length + " 个项目";
|
||
}
|
||
|
||
function esc(s) {
|
||
var d = document.createElement("div");
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function openProject(id) {
|
||
window.location.href = "/index.html?project=" + encodeURIComponent(id);
|
||
}
|
||
|
||
function confirmDelete(id, name) {
|
||
openConfirm("确认删除项目「" + name + "」?此操作将同时删除该项目下的所有配置,且不可恢复。", async function () {
|
||
try {
|
||
await API.deleteProject(id);
|
||
toast("项目已删除", "success");
|
||
loadProjects();
|
||
} catch (err) {
|
||
toast("删除失败: " + err.message, "error");
|
||
}
|
||
});
|
||
}
|
||
|
||
// 新建项目
|
||
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-project").addEventListener("click", async function () {
|
||
var name = document.getElementById("project-name").value.trim();
|
||
var desc = document.getElementById("project-description").value.trim();
|
||
if (!name) { toast("请输入项目名称", "error"); return; }
|
||
try {
|
||
await API.createProject({
|
||
name: name,
|
||
description: desc,
|
||
enableIdc: document.getElementById("project-enable-idc").checked,
|
||
enableEnvironment: document.getElementById("project-enable-env").checked,
|
||
enableGroup: document.getElementById("project-enable-group").checked,
|
||
idcMapping: document.getElementById("project-idc-mapping").value,
|
||
environmentMapping: document.getElementById("project-env-mapping").value,
|
||
groupMapping: document.getElementById("project-group-mapping").value,
|
||
});
|
||
closeModal("modal-project");
|
||
document.getElementById("project-name").value = "";
|
||
document.getElementById("project-description").value = "";
|
||
toast("项目创建成功", "success");
|
||
loadProjects();
|
||
} catch (err) {
|
||
toast("创建失败: " + err.message, "error");
|
||
}
|
||
});
|
||
|
||
// 搜索
|
||
document.getElementById("project-search").addEventListener("input", function () {
|
||
var kw = this.value.trim().toLowerCase();
|
||
var cards = document.querySelectorAll(".project-card");
|
||
var visible = 0;
|
||
cards.forEach(function (card) {
|
||
var text = card.textContent.toLowerCase();
|
||
var show = !kw || text.indexOf(kw) !== -1;
|
||
card.style.display = show ? "" : "none";
|
||
if (show) visible++;
|
||
});
|
||
document.getElementById("project-count").textContent = "共 " + visible + " 个项目";
|
||
});
|
||
|
||
// 退出登录
|
||
document.getElementById("logout-link").addEventListener("click", async function (e) {
|
||
e.preventDefault();
|
||
try { await API.logout(); } catch (e) {}
|
||
localStorage.removeItem("registry_token");
|
||
localStorage.removeItem("registry_user");
|
||
window.location.href = "/login.html";
|
||
});
|
||
|
||
document.getElementById("btn-refresh").addEventListener("click", loadProjects);
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|