feat: finish bilingual session auth learning lab

This commit is contained in:
Codex
2026-03-24 09:18:13 +08:00
parent 4cc4c26f2b
commit 5e318cb7f4
27 changed files with 1911 additions and 1079 deletions

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

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

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

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