2026-03-24 09:18:13 +08:00
|
|
|
|
<%@ 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">
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<title>Struts2 学习门户</title>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
<style>
|
|
|
|
|
|
:root {
|
2026-03-24 16:00:44 +08:00
|
|
|
|
--bg: #eff2f5;
|
|
|
|
|
|
--panel: rgba(255,255,255,0.97);
|
|
|
|
|
|
--line: #d4d9e6;
|
|
|
|
|
|
--text: #1f2b3d;
|
|
|
|
|
|
--muted: #52607a;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
--brand: #1464c7;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
--accent: #0d9488;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
min-height: 100vh;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
|
|
|
|
|
background: linear-gradient(135deg, #e3ebff 0%, #f1f5f9 55%, #effaf5 100%);
|
2026-03-24 09:18:13 +08:00
|
|
|
|
color: var(--text);
|
|
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.shell {
|
|
|
|
|
|
width: min(1200px, 100%);
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 24px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.card {
|
|
|
|
|
|
background: var(--panel);
|
|
|
|
|
|
border-radius: 26px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
padding: 28px;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
box-shadow: 0 25px 50px rgba(15, 23, 42, 0.1);
|
|
|
|
|
|
border: 1px solid var(--line);
|
2026-03-24 09:18:13 +08:00
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.eyebrow {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
letter-spacing: 0.12em;
|
|
|
|
|
|
text-transform: uppercase;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
font-weight: 700;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
h1, h2, h3 {
|
|
|
|
|
|
margin: 8px 0 12px;
|
|
|
|
|
|
font-weight: 700;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
p {
|
2026-03-24 09:18:13 +08:00
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--muted);
|
2026-03-24 16:00:44 +08:00
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
.hero-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
.hero-actions {
|
|
|
|
|
|
display: flex;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
gap: 12px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn, .btn-soft, .btn-ghost {
|
|
|
|
|
|
padding: 12px 18px;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
border-radius: 999px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
font-weight: 700;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
border: none;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.btn {
|
|
|
|
|
|
background: linear-gradient(135deg, var(--brand), #3d6fe7);
|
|
|
|
|
|
color: white;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.btn-soft {
|
|
|
|
|
|
background: #f4f7ff;
|
|
|
|
|
|
color: var(--brand);
|
2026-03-24 09:18:13 +08:00
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.btn-ghost {
|
2026-03-24 09:18:13 +08:00
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text);
|
|
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.lang-warning {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--brand);
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.portal-grid {
|
2026-03-24 09:18:13 +08:00
|
|
|
|
display: grid;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
2026-03-24 09:18:13 +08:00
|
|
|
|
gap: 16px;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
margin-top: 16px;
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.portal-card {
|
2026-03-24 09:18:13 +08:00
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: 18px;
|
2026-03-24 16:00:44 +08:00
|
|
|
|
background: rgba(255,255,255,0.85);
|
|
|
|
|
|
padding: 16px;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.portal-links {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.portal-links .link-btn {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 12px;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
border: 1px solid var(--line);
|
2026-03-24 16:00:44 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: var(--text);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
font-weight: 600;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
.insight-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.insight-card {
|
|
|
|
|
|
border: 1px solid var(--line);
|
2026-03-24 16:00:44 +08:00
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: rgba(255,255,255,0.95);
|
|
|
|
|
|
min-height: 150px;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.chain-pill {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
background: #f4f7ff;
|
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
|
font-size: 12px;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
}
|
2026-03-24 16:00:44 +08:00
|
|
|
|
.session-note {
|
|
|
|
|
|
font-size: 14px;
|
2026-03-24 12:28:16 +08:00
|
|
|
|
color: var(--muted);
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="shell">
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<section class="card hero-card">
|
|
|
|
|
|
<div class="eyebrow" id="heroEyebrow">Struts2 学习门户</div>
|
|
|
|
|
|
<h1 id="heroTitle">登录之后才能继续学习</h1>
|
|
|
|
|
|
<p id="heroDesc">所有课程模块(首页/实验/JSON 等)都由 AuthInterceptor 保护,未登录访问会自动跳转。请先登录再回来。</p>
|
|
|
|
|
|
<div class="lang-warning" id="heroWarning">登录后才能看到完整课程导航与实验入口。</div>
|
|
|
|
|
|
<div class="hero-actions">
|
|
|
|
|
|
<a class="btn" href="dashboard.action" id="heroActionPrimary">进入仪表盘</a>
|
2026-03-24 17:07:40 +08:00
|
|
|
|
<a class="btn-soft" href="logout.action" id="heroActionSecondary">退出登录</a>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<button class="btn-ghost" type="button" id="languageBtnHero">EN</button>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<div class="session-note" id="heroSessionNote">
|
|
|
|
|
|
<s:if test="#session.demoUser != null">
|
|
|
|
|
|
当前:<s:property value="#session.demoUser"/>(<s:property value="#session.demoRole"/>)
|
|
|
|
|
|
</s:if>
|
|
|
|
|
|
<s:else>
|
|
|
|
|
|
当前尚未登录,请先完成登录再浏览实验。
|
|
|
|
|
|
</s:else>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<section class="card portal-section">
|
|
|
|
|
|
<div class="eyebrow" id="portalEyebrow">学习模块导航</div>
|
|
|
|
|
|
<h2 id="portalTitle">分步骤理解 Struts2</h2>
|
|
|
|
|
|
<p id="portalDesc">每个模块都通过 `.action` 路由触达,由统一的 Session 鉴权链路保护。</p>
|
|
|
|
|
|
<div class="portal-grid">
|
|
|
|
|
|
<article class="portal-card">
|
|
|
|
|
|
<h3 id="portalCard1Title">请求生命周期</h3>
|
|
|
|
|
|
<p id="portalCard1Desc">Dispatcher → Action → Interceptor → Result,严格按链路走向。</p>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="chain-pill">Dispatcher</span>
|
|
|
|
|
|
<span class="chain-pill">Action</span>
|
|
|
|
|
|
<span class="chain-pill">Interceptors</span>
|
|
|
|
|
|
<span class="chain-pill">Result</span>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
<article class="portal-card">
|
|
|
|
|
|
<h3 id="portalCard2Title">登录保护</h3>
|
|
|
|
|
|
<p id="portalCard2Desc">`LoginAction` 写入 Session,`AuthInterceptor` 阻止任何未登录访问。</p>
|
|
|
|
|
|
<div class="portal-links">
|
|
|
|
|
|
<a class="link-btn" href="loginPage.action">登录页</a>
|
|
|
|
|
|
<a class="link-btn" href="dashboard.action">仪表盘</a>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
<article class="portal-card">
|
|
|
|
|
|
<h3 id="portalCard3Title">表单与校验</h3>
|
|
|
|
|
|
<p id="portalCard3Desc">表单字段直接绑定到 Action,ValidationAction 负责完整校验流程。</p>
|
|
|
|
|
|
<div class="portal-links">
|
|
|
|
|
|
<a class="link-btn" href="userFormPage.action">表单</a>
|
|
|
|
|
|
<a class="link-btn" href="validationPage.action">校验</a>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
<article class="portal-card">
|
|
|
|
|
|
<h3 id="portalCard4Title">AJAX / REST 对照</h3>
|
|
|
|
|
|
<p id="portalCard4Desc">对比 `ajax.action` 与 `api/users.action` 的 JSON 输出。</p>
|
|
|
|
|
|
<div class="portal-links">
|
|
|
|
|
|
<a class="link-btn" href="ajax.action">AJAX</a>
|
|
|
|
|
|
<a class="link-btn" href="api/users.action">REST</a>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-03-24 12:28:16 +08:00
|
|
|
|
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<section class="card insight-section">
|
|
|
|
|
|
<div class="eyebrow">Insight</div>
|
|
|
|
|
|
<h2>安全学习链路速览</h2>
|
|
|
|
|
|
<div class="insight-grid">
|
|
|
|
|
|
<article class="insight-card">
|
|
|
|
|
|
<h3>登录才能使用</h3>
|
|
|
|
|
|
<p>未登录访问 `dashboard.action` 等任何端点都会被 AuthInterceptor 重定向至 `loginPage.action`。</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
<article class="insight-card">
|
|
|
|
|
|
<h3>表单绑定观察</h3>
|
|
|
|
|
|
<p>`UserAction` 按字段绑定,`ValidationAction` 造成的字段错误会带回当前 JSP。</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
<article class="insight-card">
|
|
|
|
|
|
<h3>AJAX vs REST</h3>
|
|
|
|
|
|
<p>`ajax.action` 返回对话式 JSON,`api/users.action` 呈现 REST 样式,便于教学对照。</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-24 16:00:44 +08:00
|
|
|
|
<script src="../assets/struts-lab.js"></script>
|
2026-03-24 09:18:13 +08:00
|
|
|
|
<script>
|
2026-03-24 16:00:44 +08:00
|
|
|
|
strutsLab.mount({
|
|
|
|
|
|
buttonId: "languageBtnHero",
|
2026-03-24 09:18:13 +08:00
|
|
|
|
messages: {
|
|
|
|
|
|
zh: {
|
2026-03-24 16:00:44 +08:00
|
|
|
|
pageTitle: "Struts2 学习门户",
|
2026-03-24 09:18:13 +08:00
|
|
|
|
text: {
|
2026-03-24 16:00:44 +08:00
|
|
|
|
heroEyebrow: "Struts2 学习门户",
|
|
|
|
|
|
heroTitle: "登录之后才能继续学习",
|
2026-03-24 17:07:40 +08:00
|
|
|
|
heroDesc: "所有入口都由 Session + AuthInterceptor 保护,未登录将自动跳回登录页;登录成功后会先回到这个统一门户。",
|
|
|
|
|
|
heroWarning: "当前门户本身也在登录保护之下,适合用来总览各实验模块。",
|
2026-03-24 16:00:44 +08:00
|
|
|
|
heroActionPrimary: "进入仪表盘",
|
2026-03-24 17:07:40 +08:00
|
|
|
|
heroActionSecondary: "退出登录",
|
2026-03-24 16:00:44 +08:00
|
|
|
|
portalEyebrow: "学习模块导航",
|
|
|
|
|
|
portalTitle: "分步骤理解 Struts2",
|
|
|
|
|
|
portalDesc: "每个 `.action` 路径都在登录后才能访问,Session 鉴权串联整个体验。",
|
|
|
|
|
|
portalCard1Title: "请求生命周期",
|
|
|
|
|
|
portalCard1Desc: "Dispatcher → Action → Interceptor → Result,典型执行路径。",
|
|
|
|
|
|
portalCard2Title: "登录保护",
|
|
|
|
|
|
portalCard2Desc: "`LoginAction` 写入 Session,`AuthInterceptor` 拦截未登录。",
|
|
|
|
|
|
portalCard3Title: "表单与校验",
|
|
|
|
|
|
portalCard3Desc: "Action 字段绑定 + ValidationAction 校验,成功后进入汇总。",
|
|
|
|
|
|
portalCard4Title: "AJAX / REST 对照",
|
|
|
|
|
|
portalCard4Desc: "对比 `ajax.action`(AJAX)与 `api/users.action`(REST)。"
|
|
|
|
|
|
}
|
2026-03-24 09:18:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
en: {
|
2026-03-24 16:00:44 +08:00
|
|
|
|
pageTitle: "Struts2 Learning Portal",
|
2026-03-24 09:18:13 +08:00
|
|
|
|
text: {
|
2026-03-24 16:00:44 +08:00
|
|
|
|
heroEyebrow: "Struts2 Learning Portal",
|
|
|
|
|
|
heroTitle: "Login before you explore",
|
2026-03-24 17:07:40 +08:00
|
|
|
|
heroDesc: "All modules are protected by Session + AuthInterceptor. After login, this protected portal becomes the unified entry.",
|
|
|
|
|
|
heroWarning: "The portal itself is protected and serves as the overview for every lab module.",
|
2026-03-24 16:00:44 +08:00
|
|
|
|
heroActionPrimary: "Go to dashboard",
|
2026-03-24 17:07:40 +08:00
|
|
|
|
heroActionSecondary: "Log out",
|
2026-03-24 16:00:44 +08:00
|
|
|
|
portalEyebrow: "Module guide",
|
|
|
|
|
|
portalTitle: "Step-by-step Struts2",
|
|
|
|
|
|
portalDesc: "Every `.action` route sits behind login; the session chain secures the experience.",
|
|
|
|
|
|
portalCard1Title: "Request lifecycle",
|
|
|
|
|
|
portalCard1Desc: "Dispatcher → Action → Interceptor → Result shows the execution path.",
|
|
|
|
|
|
portalCard2Title: "Login protection",
|
|
|
|
|
|
portalCard2Desc: "`LoginAction` writes Session, `AuthInterceptor` blocks unauthenticated hits.",
|
|
|
|
|
|
portalCard3Title: "Forms & validation",
|
|
|
|
|
|
portalCard3Desc: "Field binding plus `ValidationAction` determine success/failure views.",
|
|
|
|
|
|
portalCard4Title: "AJAX / REST contrast",
|
|
|
|
|
|
portalCard4Desc: "Compare `ajax.action` (AJAX) with `api/users.action` (REST) outputs."
|
|
|
|
|
|
}
|
2026-03-24 09:18:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|