registry/public/projects.html
jif2.zhang 1765caba50
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
feat: 账号认证、用户管理与 API Token 管理
- 后端: Auth(登录/登出/me + session 中间件)、Users、Tokens 路由与 Service,
  scrypt 哈希+timingSafeEqual 校验、requireAdmin 保护、admin 用户自动初始化
- 前端: login.html(登录页, 无账号密码提示)、admin.html(用户+Token 管理,
  令牌明文显隐/复制/吊销/重置密码)、标题栏用户信息+系统管理入口
- 持久化: jsonAdapter 支持 users/api-tokens,运行时数据忽略入库
- 文档: 需求.md/原型设计.md 同步
2026-09-11 17:12:37 +08:00

316 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>