update admin

This commit is contained in:
2026-05-06 19:10:27 +08:00
parent e24b41f53c
commit e8f36222e7
25 changed files with 1103 additions and 354 deletions
@@ -0,0 +1,54 @@
<#macro page title active adminName="admin">
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<title>${title}</title>
<#include "admin-commjs.ftl"/>
<style>
body { background: #f6f7fb; }
.admin-header { background: #1f2937; color: #fff; padding: 12px 18px; }
.admin-grid { display: grid; grid-template-columns: 230px 1fr; gap: 16px; padding: 16px; }
.admin-sidebar { background: #fff; border-radius: 8px; padding: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.admin-main { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.admin-sidebar a.item.active { background: #1d4ed8 !important; color: #fff !important; border-radius: 6px; }
.metric-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }
.small-muted { color: #6b7280; font-size: 12px; }
@media (max-width: 900px) {
.admin-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="admin-header">
<div class="ui grid">
<div class="ten wide column">Jiscuss 后台管理</div>
<div class="six wide right aligned column">当前管理员: ${adminName} | <a style="color:#93c5fd" href="/swagger-ui.html" target="_blank">接口文档</a> | <a style="color:#93c5fd" href="/">前台首页</a></div>
</div>
</div>
<div class="admin-grid">
<div class="admin-sidebar">
<div class="ui vertical fluid menu">
<a class="item <#if active=='home'>active</#if>" href="/admin/home">总览与运维</a>
<a class="item <#if active=='users'>active</#if>" href="/admin/users">用户与角色</a>
<a class="item <#if active=='discussions'>active</#if>" href="/admin/discussions">文章管理</a>
<a class="item <#if active=='posts'>active</#if>" href="/admin/posts">评论管理</a>
<a class="item <#if active=='upgradeLogs'>active</#if>" href="/admin/upgrade-logs">版本升级日志</a>
<a class="item <#if active=='auditLogs'>active</#if>" href="/admin/audit-logs">操作审计日志</a>
<div class="item small-muted">预留扩展</div>
<a class="item <#if active=='plugins'>active</#if>" href="/admin/plugins">插件管理(预留)</a>
<a class="item <#if active=='themes'>active</#if>" href="/admin/themes">主题管理(预留)</a>
</div>
</div>
<div class="admin-main">
<#nested>
</div>
</div>
</body>
</html>
</#macro>
@@ -0,0 +1,48 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 操作审计日志" active=active adminName=adminName>
<h2 class="ui header">操作审计日志</h2>
<p class="small-muted">记录所有管理员的后台操作,包括角色管理、用户管理、内容管理等。</p>
<table class="ui striped celled table">
<thead>
<tr>
<th>ID</th>
<th>管理员</th>
<th>操作类型</th>
<th>目标类型</th>
<th>目标ID</th>
<th>描述</th>
<th>状态</th>
<th>操作时间</th>
</tr>
</thead>
<tbody>
<#list logs as l>
<tr>
<td>${l.id}</td>
<td>${l.adminName!""}</td>
<td><span class="ui label">${l.actionType!""}</span></td>
<td>${l.targetType!""}</td>
<td>${l.targetId!""}</td>
<td>
<div style="max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${l.description!""}">
${l.description!""}
</div>
</td>
<td>
<#if l.status == "success">
<span class="ui label green">成功</span>
<#elseif l.status == "blocked">
<span class="ui label orange">阻止</span>
<#elseif l.status == "ignored">
<span class="ui label yellow">忽略</span>
<#else>
<span class="ui label">${l.status!""}</span>
</#if>
</td>
<td>${l.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
</tr>
</#list>
</tbody>
</table>
</@shell.page>
@@ -0,0 +1,32 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 文章管理" active=active adminName=adminName>
<h2 class="ui header">文章管理</h2>
<table class="ui striped celled table">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>创建人ID</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<#list discussions as d>
<tr>
<td>${d.id}</td>
<td>${d.title!""}</td>
<td>${d.createId!""}</td>
<td>${d.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
<td>
<a class="ui tiny button" href="/getdiscussionsbyid?id=${d.id}" target="_blank">查看</a>
<form style="display:inline" method="post" action="/admin/discussions/${d.id}/delete" onsubmit="return confirm('确认删除该文章及其评论吗?');">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button class="ui tiny red button" type="submit">删除</button>
</form>
</td>
</tr>
</#list>
</tbody>
</table>
</@shell.page>
+42 -330
View File
@@ -1,338 +1,50 @@
<!DOCTYPE html>
<html>
<title>后台管理首页</title>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 总览" active=active adminName=adminName>
<h2 class="ui header">系统总览</h2>
<#--jquery-->
<script crossorigin="anonymous" integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
src="https://lib.baomitu.com/jquery/3.4.1/jquery.min.js"></script>
<#--semantic-ui-->
<link rel="stylesheet" type="text/css" href="/static/semanticui/semantic.css">
<script type="text/javascript" src="/static/semanticui/semantic.js"></script>
<link rel="stylesheet" type="text/css" href="/static/semanticui/my.css">
<#--tinymce-->
<script crossorigin="anonymous" integrity="sha384-CpsBIlOAWHuSRRN235sCBzEeKN6hLT6SpOGRkGadKpYj0gDP7+s3Q8pC38z8uGHH"
src="https://lib.baomitu.com/tinymce/5.1.1/tinymce.min.js"></script>
<#--layx-->
<link href="/static/layx/layx.min.css?b14794a8a3baf3e8b58e" rel="stylesheet">
<script type="text/javascript" src="/static/layx/layx.min.js?b14794a8a3baf3e8b58e"></script>
<script type="text/javascript" charset="UTF-8" src="/static/js/comm/util.js"></script>
<style type="text/css">
body {
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: grayscale;
}
#sidebar {
position: fixed;
height: 100vh;
background-color: #f5f5f5;
padding-top: 68px;
padding-left: 0;
padding-right: 0;
}
#sidebar .ui.menu > a.item {
padding: 10px 20px;
line-height: 20px;
color: #337ab7;
border-radius: 0 !important;
margin-top: 0;
margin-bottom: 0;
}
#sidebar .ui.menu > a.item.active {
background-color: #337ab7;
color: white;
border: none !important;
}
#sidebar .ui.menu > a.item:hover {
background-color: #eee;
color: #23527c;
}
#content {
padding-top: 56px;
padding-left: 20px;
padding-right: 20px;
}
#content h1 {
font-size: 36px;
}
#content .ui.dividing.header {
width: 100%;
}
.ui.centered.small.circular.image {
margin-top: 14px;
margin-bottom: 14px;
}
.ui.borderless.menu {
box-shadow: none;
flex-wrap: wrap;
border: none;
padding-left: 0;
padding-right: 0;
}
.ui.mobile.only.grid .ui.menu .ui.vertical.menu {
display: none;
}
</style>
</head>
<body id="root">
<div class="ui tablet computer only padded grid">
<div class="ui inverted borderless top fixed fluid menu">
<a class="header item">Jiscuss后台管理</a>
<div class="right menu">
<div class="item">
<div class="ui small input"><input placeholder="Search..." /></div>
</div>
<a class="item">常用管理</a> <a class="item">设置</a>
<a class="item">用户</a> <a class="item">主题</a> <a class="item">注销</a>
</div>
<div class="ui four stackable cards">
<div class="card"><div class="content"><div class="header">用户总数</div><div class="description">${userCount}</div></div></div>
<div class="card"><div class="content"><div class="header">文章总数</div><div class="description">${discussionCount}</div></div></div>
<div class="card"><div class="content"><div class="header">评论总数</div><div class="description">${postCount}</div></div></div>
<div class="card"><div class="content"><div class="header">角色总数</div><div class="description">${roleCount}</div></div></div>
</div>
</div>
<div class="ui mobile only padded grid">
<div class="ui top fixed borderless fluid inverted menu">
<a class="header item">Project Name</a>
<div class="right menu">
<div class="item">
<button class="ui icon toggle basic inverted button">
<i class="content icon"></i>
</button>
<h3 class="ui dividing header">服务器与运行时</h3>
<div class="ui two stackable cards">
<div class="card">
<div class="content">
<div class="header">CPU / 内存</div>
<div class="description">
<p>CPU 负载: ${sys.cpuLoad}</p>
<p>JVM 已用内存: ${sys.usedMb} MB / ${sys.totalMb} MB</p>
<p>JVM 最大内存: ${sys.maxMb} MB</p>
<p>Heap 已用: ${sys.heapUsedMb} MB</p>
<p>应用运行时长: ${sys.uptimeMs} ms</p>
</div>
</div>
</div>
<div class="ui vertical borderless inverted fluid menu">
<a class="item">Dashboard</a> <a class="item">Settings</a>
<a class="item">Profile</a> <a class="item">Help</a>
<div class="ui fitted divider"></div>
<div class="item">
<div class="ui small input"><input placeholder="Search..." /></div>
<div class="card">
<div class="content">
<div class="header">关键依赖版本</div>
<div class="description">
<p>Spring Boot: ${deps.springBoot}</p>
<p>Java: ${deps.java}</p>
<p>Spring: ${deps.spring}</p>
<p>Hibernate: ${deps.hibernate}</p>
<p>Flyway: ${deps.flyway}</p>
<p>Druid: ${deps.druid}</p>
<p>Ehcache: ${deps.ehcache}</p>
<p>SpringDoc: ${deps.springdoc}</p>
</div>
</div>
</div>
</div>
</div>
<div class="ui padded grid">
<div
class="three wide tablet only three wide computer only column"
id="sidebar"
>
<div class="ui vertical borderless fluid text menu">
<a class="active item">常用管理</a> <a class="item">设置</a>
<div class="ui hidden divider"></div>
<a class="item">用户</a> <a class="item">主题</a>
<div class="ui hidden divider"></div>
<a class="item">Jiscuss文档</a>
<a class="item" href="/">返回首页</a>
</div>
<h3 class="ui dividing header">快捷入口</h3>
<div class="ui buttons">
<a class="ui primary button" href="/admin/users">用户与角色</a>
<a class="ui button" href="/admin/discussions">文章管理</a>
<a class="ui button" href="/admin/posts">评论管理</a>
<a class="ui button" href="/swagger-ui.html" target="_blank">接口文档</a>
</div>
<div
class="sixteen wide mobile thirteen wide tablet thirteen wide computer right floated column"
id="content"
>
<div class="ui padded grid">
<div class="row">
<h1 class="ui huge dividing header">Dashboard</h1>
</div>
<div class="center aligned row">
<div
class="eight wide mobile four wide tablet four wide computer column"
>
<img
class="ui centered small circular image"
src="./static/images/wireframe/square-image.png"
/>
<div class="ui large basic label">Label</div>
<p>Something else</p>
</div>
<div
class="eight wide mobile four wide tablet four wide computer column"
>
<img
class="ui centered small circular image"
src="./static/images/wireframe/square-image.png"
/>
<div class="ui large basic label">Label</div>
<p>Something else</p>
</div>
<div
class="eight wide mobile four wide tablet four wide computer column"
>
<img
class="ui centered small circular image"
src="./static/images/wireframe/square-image.png"
/>
<div class="ui large basic label">Label</div>
<p>Something else</p>
</div>
<div
class="eight wide mobile four wide tablet four wide computer column"
>
<img
class="ui centered small circular image"
src="./static/images/wireframe/square-image.png"
/>
<div class="ui large basic label">Label</div>
<p>Something else</p>
</div>
</div>
<div class="ui hidden section divider"></div>
<div class="row">
<h1 class="ui huge dividing header">Section title</h1>
</div>
<div class="row">
<table class="ui single line striped selectable unstackable table">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
<tr>
<td>1,003</td>
<td>libero</td>
<td>Sed</td>
<td>cursus</td>
<td>ante</td>
</tr>
<tr>
<td>1,004</td>
<td>dapibus</td>
<td>diam</td>
<td>Sed</td>
<td>nisi</td>
</tr>
<tr>
<td>1,005</td>
<td>Nulla</td>
<td>quis</td>
<td>sem</td>
<td>at</td>
</tr>
<tr>
<td>1,006</td>
<td>nibh</td>
<td>elementum</td>
<td>imperdiet</td>
<td>Duis</td>
</tr>
<tr>
<td>1,007</td>
<td>sagittis</td>
<td>ipsum</td>
<td>Praesent</td>
<td>mauris</td>
</tr>
<tr>
<td>1,008</td>
<td>Fusce</td>
<td>nec</td>
<td>tellus</td>
<td>sed</td>
</tr>
<tr>
<td>1,009</td>
<td>augue</td>
<td>semper</td>
<td>porta</td>
<td>Mauris</td>
</tr>
<tr>
<td>1,010</td>
<td>massa</td>
<td>Vestibulum</td>
<td>lacinia</td>
<td>arcu</td>
</tr>
<tr>
<td>1,011</td>
<td>eget</td>
<td>nulla</td>
<td>Class</td>
<td>aptent</td>
</tr>
<tr>
<td>1,012</td>
<td>taciti</td>
<td>sociosqu</td>
<td>ad</td>
<td>litora</td>
</tr>
<tr>
<td>1,013</td>
<td>torquent</td>
<td>per</td>
<td>conubia</td>
<td>nostra</td>
</tr>
<tr>
<td>1,014</td>
<td>per</td>
<td>inceptos</td>
<td>himenaeos</td>
<td>Curabitur</td>
</tr>
<tr>
<td>1,015</td>
<td>sodales</td>
<td>ligula</td>
<td>in</td>
<td>libero</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$(".ui.toggle.button").click(function() {
$(".mobile.only.grid .ui.vertical.menu").toggle(100);
});
});
</script>
</body>
</html>
</@shell.page>
@@ -0,0 +1,8 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 插件管理(预留)" active=active adminName=adminName>
<h2 class="ui header">插件管理(预留)</h2>
<div class="ui info message">
<div class="header">预留扩展点</div>
<p>后续可在此接入插件生命周期管理:安装、启停、配置、版本升级、依赖校验。</p>
</div>
</@shell.page>
@@ -0,0 +1,33 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 评论管理" active=active adminName=adminName>
<h2 class="ui header">评论管理</h2>
<table class="ui compact celled table">
<thead>
<tr>
<th>ID</th>
<th>文章</th>
<th>作者</th>
<th>内容</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<#list posts as p>
<tr>
<td>${p.id}</td>
<td>${discussionNames[p.discussionId]!('#'+p.discussionId?c)}</td>
<td>${userNames[p.createId]!('#'+p.createId?c)}</td>
<td><div style="max-width:420px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${p.content!""}</div></td>
<td>${p.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
<td>
<form style="display:inline" method="post" action="/admin/posts/${p.id}/delete" onsubmit="return confirm('确认删除该评论吗?');">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button class="ui tiny red button" type="submit">删除</button>
</form>
</td>
</tr>
</#list>
</tbody>
</table>
</@shell.page>
@@ -0,0 +1,8 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 主题管理(预留)" active=active adminName=adminName>
<h2 class="ui header">主题管理(预留)</h2>
<div class="ui info message">
<div class="header">预留扩展点</div>
<p>后续可在此增加主题包管理、配色切换、模板覆盖与预览发布。</p>
</div>
</@shell.page>
@@ -0,0 +1,65 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 版本升级日志" active=active adminName=adminName>
<h2 class="ui header">版本功能升级日志</h2>
<p class="small-muted">记录每次升级新增与修复内容,便于发布追踪与回滚评估。</p>
<form class="ui form" method="post" action="/admin/upgrade-logs">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<div class="three fields">
<div class="field">
<label>版本号</label>
<input name="version" placeholder="例如 v3.5.13" required>
</div>
<div class="field">
<label>类型</label>
<select class="ui dropdown" name="type">
<option value="feature">feature</option>
<option value="fix">fix</option>
<option value="security">security</option>
<option value="refactor">refactor</option>
</select>
</div>
<div class="field">
<label>标题</label>
<input name="title" placeholder="本次升级摘要" required>
</div>
</div>
<div class="field">
<label>详细内容</label>
<textarea name="content" rows="4" placeholder="描述新增、修复、兼容性说明等"></textarea>
</div>
<button class="ui primary button" type="submit">新增日志</button>
</form>
<h3 class="ui dividing header">历史记录</h3>
<table class="ui celled table">
<thead>
<tr>
<th>ID</th>
<th>版本</th>
<th>类型</th>
<th>标题</th>
<th>内容</th>
<th>创建人</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<#list logs as l>
<tr>
<td>${l.id}</td>
<td>${l.version!""}</td>
<td>${l.type!""}</td>
<td>${l.title!""}</td>
<td>${l.content!""}</td>
<td>${l.createdBy!""}</td>
<td>${l.createTime?string('yyyy-MM-dd HH:mm:ss')!""}</td>
</tr>
</#list>
</tbody>
</table>
<script>
$('.ui.dropdown').dropdown();
</script>
</@shell.page>
@@ -0,0 +1,82 @@
<#import "admin-shell.ftl" as shell>
<@shell.page title="后台管理 - 用户与角色" active=active adminName=adminName>
<h2 class="ui header">用户与角色管理</h2>
<p class="small-muted">简单 RBAC:用户可绑定多个角色,拥有 ADMIN 角色的用户可以进入后台管理。</p>
<h3 class="ui dividing header">角色管理</h3>
<form class="ui form" method="post" action="/admin/roles">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<div class="three fields">
<div class="field"><label>角色编码</label><input name="code" placeholder="如 ADMIN_AUDIT" required></div>
<div class="field"><label>角色名称</label><input name="name" placeholder="如 审计管理员" required></div>
<div class="field"><label>描述</label><input name="description" placeholder="角色用途说明"></div>
</div>
<button class="ui small primary button" type="submit">新增角色</button>
</form>
<table class="ui celled compact table">
<thead><tr><th>ID</th><th>编码</th><th>名称</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
<#list roles as r>
<tr>
<td>${r.id}</td>
<td>${r.code!""}</td>
<td>${r.name!""}</td>
<td><#if r.enabled?? && r.enabled==1>启用<#else>停用</#if></td>
<td>
<form style="display:inline" method="post" action="/admin/roles/${r.id}/toggle">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button class="ui tiny button" type="submit">切换启停</button>
</form>
</td>
</tr>
</#list>
</tbody>
</table>
<h3 class="ui dividing header">用户角色绑定</h3>
<table class="ui celled table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色绑定</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<#list users as u>
<tr>
<td>${u.id}</td>
<td>${u.username!""}</td>
<td>${u.email!""}</td>
<td>
<form class="ui form" method="post" action="/admin/users/${u.id}/roles">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<div class="inline fields">
<#list roles as r>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="roleIds" value="${r.id}"
<#if userRoleIds[u.id]?? && userRoleIds[u.id]?seq_contains(r.id)>checked</#if>>
<label>${r.code}</label>
</div>
</div>
</#list>
</div>
<button class="ui tiny primary button" type="submit">保存角色</button>
</form>
</td>
<td>
<a class="ui tiny button" href="/user?id=${u.id}" target="_blank">查看用户页</a>
</td>
</tr>
</#list>
</tbody>
</table>
<script>
$('.ui.checkbox').checkbox();
</script>
</@shell.page>