feat: finish bilingual session auth learning lab
This commit is contained in:
341
web/WEB-INF/views/user/dashboard.jsp
Normal file
341
web/WEB-INF/views/user/dashboard.jsp
Normal file
@@ -0,0 +1,341 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录后仪表盘 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eff7f5;
|
||||
--panel: rgba(255,255,255,0.95);
|
||||
--line: #d5ebe3;
|
||||
--text: #123028;
|
||||
--muted: #4a655c;
|
||||
--brand: #0d8f7c;
|
||||
--soft: #e8f8f2;
|
||||
--shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(13, 143, 124, 0.15), transparent 24%),
|
||||
radial-gradient(circle at bottom left, rgba(49, 196, 141, 0.12), transparent 22%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.shell {
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.hero-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand);
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2, h3 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: var(--muted); line-height: 1.85; }
|
||||
.actions, .links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn, .btn-soft, .btn-ghost, .link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--line);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn { background: linear-gradient(135deg, var(--brand), #31c48d); color: white; border: 0; }
|
||||
.btn-soft { background: var(--soft); color: var(--brand); }
|
||||
.btn-ghost, .link-btn { background: white; color: var(--text); }
|
||||
.stats, .grid, .steps {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.steps { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.stat, .panel, .step {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.68);
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||||
.stat strong { font-size: 22px; }
|
||||
.tag-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--soft);
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.panel ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.85;
|
||||
}
|
||||
.step strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.step p { margin: 0; }
|
||||
@media (max-width: 960px) {
|
||||
.stats, .grid, .steps { grid-template-columns: 1fr; }
|
||||
.hero-head { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card">
|
||||
<div class="hero-head">
|
||||
<div>
|
||||
<div class="eyebrow" id="heroEyebrow">登录后仪表盘</div>
|
||||
<h1 id="heroTitle">Session 已建立,后续实验页现在通过拦截器受保护</h1>
|
||||
<p id="heroText">这一页的作用不是展示“登录成功”四个字,而是把你接下来的学习路线展开:继续看用户表单、校验、上传,或者回到门户解释这条 Session 鉴权链路是如何工作的。</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
|
||||
<a class="btn" href="../userFormPage.action" id="primaryAction">进入用户表单</a>
|
||||
<a class="btn-soft" href="../logout.action" id="logoutAction">退出登录</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span id="statUserLabel">当前用户</span>
|
||||
<strong><s:property value="displayName"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="statRoleLabel">当前角色</span>
|
||||
<strong id="roleValue" data-role-code="<s:property value='role'/>"><s:property value="role"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="statTimeLabel">登录时间</span>
|
||||
<strong><s:property value="loginTime"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow" id="secureEyebrow">受保护实验</div>
|
||||
<h2 id="secureTitle">现在你可以进入这些需要登录的页面</h2>
|
||||
<div class="grid">
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagUser">字段绑定</span>
|
||||
<span class="tag" id="tagUserSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelUserTitle">用户资料表单</h3>
|
||||
<p id="panelUserText">继续看请求参数如何绑定到 Action 属性,并在成功页生成一份汇总结果。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../userFormPage.action" id="panelUserLink">打开用户表单</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagValidation">validate()</span>
|
||||
<span class="tag" id="tagValidationSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelValidationTitle">字段校验页</h3>
|
||||
<p id="panelValidationText">对比校验失败和成功状态,理解为什么 Struts2 会在业务逻辑前先执行校验方法。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../validationPage.action" id="panelValidationLink">打开校验页</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagUpload">multipart</span>
|
||||
<span class="tag" id="tagUploadSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelUploadTitle">上传元数据页</h3>
|
||||
<p id="panelUploadText">这里保留文件上传的教学价值,但不把文件真正写入磁盘,适合安全演示。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../uploadPage.action" id="panelUploadLink">打开上传页</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagPortal">解释链路</span>
|
||||
<span class="tag" id="tagPortalTrace">回看入口</span>
|
||||
</div>
|
||||
<h3 id="panelPortalTitle">回门户讲完整链路</h3>
|
||||
<p id="panelPortalText">回到入口页,可以从公开入口、登录动作、Session 写入、拦截器保护这条路线整体复盘。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../index.action" id="panelPortalLink">返回门户</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow" id="flowEyebrow">鉴权流程</div>
|
||||
<h2 id="flowTitle">这次你实际走过的 Struts2 登录链路</h2>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<strong id="flowStep1Title">1. 表单提交到 login Action</strong>
|
||||
<p id="flowStep1Text">用户名和密码先进入 Action,校验失败会直接回到登录页。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep2Title">2. Action 写入 Session</strong>
|
||||
<p id="flowStep2Text">账号通过后,用户、角色和登录时间被放进 Session。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep3Title">3. 拦截器保护页面动作</strong>
|
||||
<p id="flowStep3Text">用户表单、校验页、上传页和仪表盘都先经过 AuthInterceptor。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep4Title">4. 未登录会被打回登录页</strong>
|
||||
<p id="flowStep4Text">如果没有 Session,安全动作不会继续执行,而是直接跳回登录入口。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="../assets/struts-lab.js"></script>
|
||||
<script>
|
||||
strutsLab.mount({
|
||||
messages: {
|
||||
zh: {
|
||||
pageTitle: "登录后仪表盘 - Struts2 学习实验台",
|
||||
text: {
|
||||
heroEyebrow: "登录后仪表盘",
|
||||
heroTitle: "Session 已建立,后续实验页现在通过拦截器受保护",
|
||||
heroText: "这一页的作用不是展示“登录成功”四个字,而是把你接下来的学习路线展开:继续看用户表单、校验、上传,或者回到门户解释这条 Session 鉴权链路是如何工作的。",
|
||||
primaryAction: "进入用户表单",
|
||||
logoutAction: "退出登录",
|
||||
statUserLabel: "当前用户",
|
||||
statRoleLabel: "当前角色",
|
||||
statTimeLabel: "登录时间",
|
||||
secureEyebrow: "受保护实验",
|
||||
secureTitle: "现在你可以进入这些需要登录的页面",
|
||||
tagUser: "字段绑定",
|
||||
tagUserSecure: "受保护",
|
||||
panelUserTitle: "用户资料表单",
|
||||
panelUserText: "继续看请求参数如何绑定到 Action 属性,并在成功页生成一份汇总结果。",
|
||||
panelUserLink: "打开用户表单",
|
||||
tagValidation: "validate()",
|
||||
tagValidationSecure: "受保护",
|
||||
panelValidationTitle: "字段校验页",
|
||||
panelValidationText: "对比校验失败和成功状态,理解为什么 Struts2 会在业务逻辑前先执行校验方法。",
|
||||
panelValidationLink: "打开校验页",
|
||||
tagUpload: "multipart",
|
||||
tagUploadSecure: "受保护",
|
||||
panelUploadTitle: "上传元数据页",
|
||||
panelUploadText: "这里保留文件上传的教学价值,但不把文件真正写入磁盘,适合安全演示。",
|
||||
panelUploadLink: "打开上传页",
|
||||
tagPortal: "解释链路",
|
||||
tagPortalTrace: "回看入口",
|
||||
panelPortalTitle: "回门户讲完整链路",
|
||||
panelPortalText: "回到入口页,可以从公开入口、登录动作、Session 写入、拦截器保护这条路线整体复盘。",
|
||||
panelPortalLink: "返回门户",
|
||||
flowEyebrow: "鉴权流程",
|
||||
flowTitle: "这次你实际走过的 Struts2 登录链路",
|
||||
flowStep1Title: "1. 表单提交到 login Action",
|
||||
flowStep1Text: "用户名和密码先进入 Action,校验失败会直接回到登录页。",
|
||||
flowStep2Title: "2. Action 写入 Session",
|
||||
flowStep2Text: "账号通过后,用户、角色和登录时间被放进 Session。",
|
||||
flowStep3Title: "3. 拦截器保护页面动作",
|
||||
flowStep3Text: "用户表单、校验页、上传页和仪表盘都先经过 AuthInterceptor。",
|
||||
flowStep4Title: "4. 未登录会被打回登录页",
|
||||
flowStep4Text: "如果没有 Session,安全动作不会继续执行,而是直接跳回登录入口。"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Post-login Dashboard - Struts2 Learning Lab",
|
||||
text: {
|
||||
heroEyebrow: "Post-login dashboard",
|
||||
heroTitle: "A session now exists, and later lab pages are protected by an interceptor",
|
||||
heroText: "This page is not just a success notice. It expands the next learning route: continue to user forms, validation, and upload, or go back to the portal and explain the full session-auth path.",
|
||||
primaryAction: "Open user form",
|
||||
logoutAction: "Log out",
|
||||
statUserLabel: "Current user",
|
||||
statRoleLabel: "Current role",
|
||||
statTimeLabel: "Login time",
|
||||
secureEyebrow: "Protected labs",
|
||||
secureTitle: "These pages now require a valid login session",
|
||||
tagUser: "Binding",
|
||||
tagUserSecure: "Protected",
|
||||
panelUserTitle: "User profile form",
|
||||
panelUserText: "Continue with request parameter binding into action properties and a structured success summary.",
|
||||
panelUserLink: "Open user form",
|
||||
tagValidation: "validate()",
|
||||
tagValidationSecure: "Protected",
|
||||
panelValidationTitle: "Validation page",
|
||||
panelValidationText: "Compare invalid and successful states and explain why Struts2 runs validate() before business output.",
|
||||
panelValidationLink: "Open validation page",
|
||||
tagUpload: "multipart",
|
||||
tagUploadSecure: "Protected",
|
||||
panelUploadTitle: "Upload metadata page",
|
||||
panelUploadText: "Keeps the teaching value of file upload while avoiding real disk writes, which is safer for demo environments.",
|
||||
panelUploadLink: "Open upload page",
|
||||
tagPortal: "Trace the path",
|
||||
tagPortalTrace: "Back to entry",
|
||||
panelPortalTitle: "Return to the portal",
|
||||
panelPortalText: "Go back and explain the full route from public entry to login action, session write, and interceptor protection.",
|
||||
panelPortalLink: "Back to portal",
|
||||
flowEyebrow: "Auth flow",
|
||||
flowTitle: "The Struts2 login path you just completed",
|
||||
flowStep1Title: "1. Form submits to the login action",
|
||||
flowStep1Text: "Username and password reach the action first; validation failure returns to the login page.",
|
||||
flowStep2Title: "2. The action writes into session",
|
||||
flowStep2Text: "After success, user, role, and login time are stored in session.",
|
||||
flowStep3Title: "3. The interceptor protects page actions",
|
||||
flowStep3Text: "The user form, validation page, upload page, and dashboard all pass through AuthInterceptor first.",
|
||||
flowStep4Title: "4. Unauthenticated access goes back to login",
|
||||
flowStep4Text: "If no session exists, secure actions stop immediately and redirect to the login entry."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: function (ui) {
|
||||
const roleNode = document.getElementById("roleValue");
|
||||
if (roleNode && roleNode.dataset.roleCode === "admin") {
|
||||
roleNode.textContent = ui.language === "zh" ? "演示管理员" : "Demo administrator";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
126
web/WEB-INF/views/user/form.jsp
Normal file
126
web/WEB-INF/views/user/form.jsp
Normal file
@@ -0,0 +1,126 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户资料表单 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #f97316 0%, #fb923c 50%, #ffedd5 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.96);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #ea580c;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6c5545; line-height: 1.85; }
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4c3422; }
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #ead8ca;
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #ea580c, #fb923c);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error { color: #c2410c; font-size: 13px; margin-top: 6px; }
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #fff5ef;
|
||||
border: 1px solid #f2d9ca;
|
||||
}
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff1e8;
|
||||
color: #c2410c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">受保护表单</div>
|
||||
<h1>通过 Action 属性绑定提交一份用户资料</h1>
|
||||
<p>这个页面现在放在登录保护链路里。重点不是字段本身,而是观察请求参数如何进入 Action,再在成功页汇总成一份结构化结果。</p>
|
||||
|
||||
<s:if test="#session.demoUser == null">
|
||||
<div class="note">
|
||||
<strong>当前未登录</strong>
|
||||
<p style="margin-top: 8px;">这个实验页已经接入 Session 保护。请先登录,再回来看字段绑定和错误回显。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../loginPage.action">先去登录</a>
|
||||
<a class="link-btn" href="../index.action">返回门户</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<div class="note">
|
||||
<strong>建议观察点</strong>
|
||||
<p style="margin-top: 8px;">依次观察:字段名如何对应 Action 属性、校验错误如何回显、成功页如何读取 Action 结果。</p>
|
||||
</div>
|
||||
|
||||
<s:form action="submitUser" method="post" namespace="/">
|
||||
<div class="field">
|
||||
<label for="username">用户名</label>
|
||||
<s:textfield id="username" name="username" placeholder="platform-owner"/>
|
||||
<div class="error"><s:fielderror fieldName="username"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<s:textfield id="email" name="email" placeholder="platform@example.com"/>
|
||||
<div class="error"><s:fielderror fieldName="email"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="phone">手机号</label>
|
||||
<s:textfield id="phone" name="phone" placeholder="13800000000"/>
|
||||
<div class="error"><s:fielderror fieldName="phone"/></div>
|
||||
</div>
|
||||
|
||||
<button type="submit">提交资料并生成汇总页</button>
|
||||
</s:form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../validationPage.action">下一步看校验页</a>
|
||||
</div>
|
||||
</s:else>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
274
web/WEB-INF/views/user/login.jsp
Normal file
274
web/WEB-INF/views/user/login.jsp
Normal file
@@ -0,0 +1,274 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session 登录实验 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #122c63 0%, #1464c7 52%, #4db5ff 100%);
|
||||
}
|
||||
.shell {
|
||||
width: min(1120px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 430px;
|
||||
gap: 18px;
|
||||
}
|
||||
.panel {
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
|
||||
}
|
||||
.top-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #1464c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #566a80; line-height: 1.85; }
|
||||
.btn-ghost, .link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d7e5f7;
|
||||
background: #ffffff;
|
||||
color: #1464c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #edf5ff;
|
||||
border: 1px solid #d7e5f7;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d7e5f7;
|
||||
background: #f8fbff;
|
||||
}
|
||||
.stat strong { display: block; margin-bottom: 6px; }
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
color: #1f3650;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #d6e1ed;
|
||||
font: inherit;
|
||||
}
|
||||
button.submit-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #1464c7, #3d8ef6);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error {
|
||||
color: #c53d3d;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.action-error {
|
||||
margin: 0 0 16px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff1f1;
|
||||
color: #b63a3a;
|
||||
border: 1px solid #f1c4c4;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="panel">
|
||||
<div class="top-actions">
|
||||
<div class="eyebrow" id="leftEyebrow">Session 登录章节</div>
|
||||
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
|
||||
</div>
|
||||
<h1 id="leftTitle">用最经典的 Struts2 Session 登录,把后续实验页保护起来</h1>
|
||||
<p id="leftText">这个页面不只是演示登录表单,而是把 Action 校验、写入 Session、拦截器校验和登录后仪表盘串成一个完整章节,适合你系统掌握老项目最常见的鉴权思路。</p>
|
||||
|
||||
<div class="note">
|
||||
<strong id="credentialTitle">演示账号</strong>
|
||||
<p id="credentialBody" style="margin-top: 8px;">用户名:<code>admin</code><br/>密码:<code>123456</code></p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<strong id="statOneTitle">这一页讲什么</strong>
|
||||
<span id="statOneText">Action 校验、错误回显、Session 写入、登录后跳转。</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong id="statTwoTitle">下一步看什么</strong>
|
||||
<span id="statTwoText">登录成功后先看仪表盘,再进入用户表单、校验页和上传页。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.action" id="backPortalLink">返回门户</a>
|
||||
<s:if test="#session.demoUser != null">
|
||||
<a class="link-btn" href="../dashboard.action" id="dashboardLink">进入仪表盘</a>
|
||||
<a class="link-btn" href="../logout.action" id="logoutLink">退出登录</a>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<a class="link-btn" href="../hello.action?name=Team" id="helloLink">运行 Hello Action</a>
|
||||
</s:else>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="eyebrow" id="formEyebrow">登录表单</div>
|
||||
<h2 id="formTitle">输入演示账号</h2>
|
||||
|
||||
<s:if test="#session.demoUser != null">
|
||||
<div class="note">
|
||||
<strong id="loggedInTitle">当前已登录</strong>
|
||||
<p id="loggedInText" style="margin-top: 8px;">你已经拥有可访问的 Session,可以直接进入仪表盘或退出后重新体验登录过程。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../dashboard.action" id="loggedInDashboardLink">打开仪表盘</a>
|
||||
<a class="link-btn" href="../logout.action" id="loggedInLogoutLink">退出后重试</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:if test="hasActionErrors()">
|
||||
<div class="action-error"><s:actionerror/></div>
|
||||
</s:if>
|
||||
|
||||
<s:form action="login" method="post" namespace="/">
|
||||
<div class="field">
|
||||
<label for="username" id="usernameLabel">用户名</label>
|
||||
<s:textfield id="username" name="username" placeholder="admin"/>
|
||||
<div class="error"><s:fielderror fieldName="username"/></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password" id="passwordLabel">密码</label>
|
||||
<s:password id="password" name="password" placeholder="123456" showPassword="true"/>
|
||||
<div class="error"><s:fielderror fieldName="password"/></div>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" type="submit" id="submitBtn">写入 Session 并进入仪表盘</button>
|
||||
</s:form>
|
||||
</s:else>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="../assets/struts-lab.js"></script>
|
||||
<script>
|
||||
strutsLab.mount({
|
||||
messages: {
|
||||
zh: {
|
||||
pageTitle: "Session 登录实验 - Struts2 学习实验台",
|
||||
text: {
|
||||
leftEyebrow: "Session 登录章节",
|
||||
leftTitle: "用最经典的 Struts2 Session 登录,把后续实验页保护起来",
|
||||
leftText: "这个页面不只是演示登录表单,而是把 Action 校验、写入 Session、拦截器校验和登录后仪表盘串成一个完整章节,适合你系统掌握老项目最常见的鉴权思路。",
|
||||
credentialTitle: "演示账号",
|
||||
statOneTitle: "这一页讲什么",
|
||||
statOneText: "Action 校验、错误回显、Session 写入、登录后跳转。",
|
||||
statTwoTitle: "下一步看什么",
|
||||
statTwoText: "登录成功后先看仪表盘,再进入用户表单、校验页和上传页。",
|
||||
backPortalLink: "返回门户",
|
||||
dashboardLink: "进入仪表盘",
|
||||
logoutLink: "退出登录",
|
||||
formEyebrow: "登录表单",
|
||||
formTitle: "输入演示账号",
|
||||
loggedInTitle: "当前已登录",
|
||||
loggedInText: "你已经拥有可访问的 Session,可以直接进入仪表盘或退出后重新体验登录过程。",
|
||||
loggedInDashboardLink: "打开仪表盘",
|
||||
loggedInLogoutLink: "退出后重试",
|
||||
helloLink: "运行 Hello Action",
|
||||
usernameLabel: "用户名",
|
||||
passwordLabel: "密码",
|
||||
submitBtn: "写入 Session 并进入仪表盘"
|
||||
},
|
||||
html: {
|
||||
credentialBody: "用户名:<code>admin</code><br/>密码:<code>123456</code>"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Session Login Lab - Struts2 Learning Lab",
|
||||
text: {
|
||||
leftEyebrow: "Session login chapter",
|
||||
leftTitle: "Use the classic Struts2 session login pattern to protect later lab pages",
|
||||
leftText: "This page is not just a login form. It connects action validation, session writes, interceptor checks, and a post-login dashboard into one complete learning chapter.",
|
||||
credentialTitle: "Demo credentials",
|
||||
statOneTitle: "What this page teaches",
|
||||
statOneText: "Action validation, error echoing, session writes, and post-login routing.",
|
||||
statTwoTitle: "What to open next",
|
||||
statTwoText: "After login, open the dashboard first, then continue to the user form, validation page, and upload page.",
|
||||
backPortalLink: "Back to portal",
|
||||
dashboardLink: "Open dashboard",
|
||||
logoutLink: "Log out",
|
||||
formEyebrow: "Login form",
|
||||
formTitle: "Enter the demo account",
|
||||
loggedInTitle: "Already logged in",
|
||||
loggedInText: "A valid session already exists. You can open the dashboard directly or log out and replay the login flow.",
|
||||
loggedInDashboardLink: "Open dashboard",
|
||||
loggedInLogoutLink: "Log out and retry",
|
||||
helloLink: "Run hello action",
|
||||
usernameLabel: "Username",
|
||||
passwordLabel: "Password",
|
||||
submitBtn: "Write session and enter dashboard"
|
||||
},
|
||||
html: {
|
||||
credentialBody: "Username: <code>admin</code><br/>Password: <code>123456</code>"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
132
web/WEB-INF/views/user/success.jsp
Normal file
132
web/WEB-INF/views/user/success.jsp
Normal file
@@ -0,0 +1,132 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户资料汇总 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0d8f7c 0%, #31c48d 55%, #d4f7e7 100%);
|
||||
color: #123028;
|
||||
}
|
||||
.shell {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #0d8f7c;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #446056; line-height: 1.85; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d7eee6;
|
||||
background: #f7fcfa;
|
||||
}
|
||||
.stat span { display: block; color: #5f7f75; font-size: 12px; margin-bottom: 6px; }
|
||||
.stat strong { font-size: 22px; }
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #e8f8f2;
|
||||
color: #0d8f7c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card">
|
||||
<div class="eyebrow">提交成功</div>
|
||||
<h1>用户资料已经通过 Action 处理并汇总完成</h1>
|
||||
<p>这页现在只负责展示用户资料提交的结果,不再和登录成功页面混用,便于你单独理解“表单提交成功页”这一类经典 Struts2 结果页。</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">当前提交</div>
|
||||
<h2><s:property value="username"/></h2>
|
||||
<p>
|
||||
<s:if test="profileReady">
|
||||
这份资料已经达到“可继续演示”的状态,可以继续进入校验页或上传页。
|
||||
</s:if>
|
||||
<s:else>
|
||||
资料已经提交成功,但手机号仍偏弱。你可以回表单页再试一次,观察不同输入对结果的影响。
|
||||
</s:else>
|
||||
</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>用户名</span>
|
||||
<strong><s:property value="username"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>邮箱</span>
|
||||
<strong><s:property value="email"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>手机号</span>
|
||||
<strong><s:property value="phone"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>资料状态</span>
|
||||
<strong>
|
||||
<s:if test="profileReady">可继续实验</s:if>
|
||||
<s:else>建议复查</s:else>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>提交时间</span>
|
||||
<strong><s:property value="submittedAt"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>结果类型</span>
|
||||
<strong>/WEB-INF/views/user/success.jsp</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../userFormPage.action">再试一次</a>
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../validationPage.action">继续看校验页</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user