Files
struts2-demo/web/WEB-INF/views/index.jsp
2026-03-24 12:28:16 +08:00

811 lines
44 KiB
Plaintext
Raw 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.
<%@ 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: #f4f6fb;
--panel: rgba(255,255,255,0.94);
--line: #d7e0ea;
--text: #122033;
--muted: #5c6f84;
--brand: #1464c7;
--brand-2: #f68b1f;
--soft: #e8f2ff;
--shadow: 0 20px 44px rgba(18, 32, 51, 0.14);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top right, rgba(20, 100, 199, 0.15), transparent 28%),
radial-gradient(circle at bottom left, rgba(246, 139, 31, 0.12), transparent 24%),
var(--bg);
}
a { color: inherit; text-decoration: none; }
.shell { max-width: 1480px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero {
padding: 28px;
margin-bottom: 18px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 800;
color: var(--brand);
}
.hero-head {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-start;
}
.hero h1 {
margin: 10px 0 12px;
font-size: 42px;
line-height: 1.1;
}
.hero p {
margin: 0;
color: var(--muted);
line-height: 1.9;
max-width: 900px;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft, .btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
border: 0;
cursor: pointer;
}
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: white; }
.btn-soft { background: var(--soft); color: var(--brand); }
.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--line); }
.metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 20px;
}
.metric {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.56);
}
.metric span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; }
.metric strong { font-size: 26px; }
.layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
.card { padding: 22px; }
.search {
display: flex;
gap: 10px;
margin-top: 14px;
}
.search input {
flex: 1;
border: 1px solid var(--line);
border-radius: 16px;
padding: 13px 14px;
font: inherit;
background: transparent;
color: var(--text);
outline: none;
}
.helper-list {
margin: 14px 0 0;
padding-left: 18px;
color: var(--muted);
line-height: 1.9;
}
.status-card {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.56);
color: var(--muted);
line-height: 1.85;
}
.status-card strong {
display: block;
margin-bottom: 8px;
color: var(--text);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.demo-card {
border: 1px solid var(--line);
border-radius: 22px;
padding: 18px;
background: rgba(255,255,255,0.45);
transition: transform 0.18s ease, border-color 0.18s ease;
}
.demo-card:hover {
transform: translateY(-3px);
border-color: rgba(20, 100, 199, 0.28);
}
.demo-card h3 {
margin: 10px 0 8px;
font-size: 20px;
}
.demo-card p {
margin: 0 0 14px;
color: var(--muted);
line-height: 1.8;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.tag {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: var(--soft);
color: var(--brand);
font-size: 12px;
font-weight: 700;
}
.demo-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.link-btn {
display: inline-flex;
padding: 9px 14px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--text);
background: white;
font-size: 13px;
font-weight: 700;
}
.pipeline {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.step {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.52);
}
.step strong { display: block; margin-bottom: 8px; }
.step p { margin: 0; color: var(--muted); line-height: 1.75; }
.empty {
padding: 14px;
border: 1px dashed var(--line);
border-radius: 16px;
color: var(--muted);
text-align: center;
}
.learning-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-top: 18px;
}
.learning-panel {
border: 1px solid var(--line);
border-radius: 20px;
padding: 18px;
background: rgba(255,255,255,0.65);
box-shadow: inset 0 4px 12px rgba(18,32,51,0.06);
}
.learning-panel h3 {
margin: 0 0 6px;
font-size: 18px;
}
.learning-panel p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
.flow-steps {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.flow-step {
padding: 10px;
border-radius: 14px;
border: 1px solid rgba(20,100,199,0.25);
background: rgba(13,143,124,0.06);
}
.flow-step strong {
display: block;
font-size: 14px;
}
.chain-pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--line);
background: #ffffff;
font-size: 12px;
margin-right: 6px;
margin-top: 6px;
}
.insight-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
margin-top: 16px;
}
.insight-card {
border-radius: 20px;
padding: 18px;
border: 1px solid var(--line);
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(255,255,255,0.7));
min-height: 200px;
position: relative;
}
.insight-card strong {
display: block;
margin-top: 10px;
font-size: 22px;
color: var(--brand);
}
.insight-card ul {
margin: 10px 0 0 16px;
color: var(--muted);
line-height: 1.6;
}
@media (max-width: 1100px) {
.layout { grid-template-columns: 1fr; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
.shell { padding: 14px; }
.hero-head, .search { flex-direction: column; align-items: stretch; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell">
<section class="hero">
<div class="hero-head">
<div>
<div class="eyebrow" id="heroEyebrow">Struts2 学习实验台</div>
<h1 id="heroTitle">把零散示例整理成一条可讲解、可演示、可验证的学习路线</h1>
<p id="heroText">这个入口页现在不再只是样例链接集合,而是把经典 Struts2 的动作映射、参数绑定、Session 登录、表单校验和文件上传串成一个可循序学习的 Demo 门户。</p>
</div>
<div class="hero-actions">
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
<a class="btn" href="loginPage.action" id="primaryAction">进入登录章节</a>
<a class="btn-soft" href="hello.action?name=Platform%20Team" id="secondaryAction">运行 Hello Action</a>
</div>
</div>
<div class="metrics">
<div class="metric"><span id="metricRoutesLabel">关键动作</span><strong>8</strong></div>
<div class="metric"><span id="metricSecureLabel">鉴权节点</span><strong>4</strong></div>
<div class="metric"><span id="metricFormsLabel">表单实验</span><strong>3</strong></div>
<div class="metric"><span id="metricApiLabel">JSON 示例</span><strong>2</strong></div>
</div>
</section>
<div class="layout">
<aside class="sidebar">
<section class="card">
<div class="eyebrow" id="searchEyebrow">快速检索</div>
<h2 id="searchTitle">按关键词筛选实验</h2>
<div class="search">
<input id="searchInput" type="text" placeholder="试试 login、session、validation、upload、json" />
</div>
<ul class="helper-list" id="helperList">
<li>先看登录和仪表盘,理解最经典的 Session 鉴权链路。</li>
<li>再看表单、校验、上传,理解 Action 如何接收与处理输入。</li>
<li>最后再看 AJAX 和 REST 风格返回,理解 Struts2 的扩展能力。</li>
</ul>
</section>
<section class="card">
<div class="eyebrow" id="statusEyebrow">当前会话</div>
<h2 id="statusTitle">登录状态与访问建议</h2>
<div class="status-card">
<s:if test="#session.demoUser != null">
<strong id="sessionStateTitle">当前已登录</strong>
<div id="sessionStateBody"
data-authenticated="true"
data-user="<s:property value='#session.demoUser'/>"
data-role="<s:property value='#session.demoRole'/>"></div>
<div class="demo-links" style="margin-top: 14px;">
<a class="link-btn" href="dashboard.action" id="dashboardLink">打开仪表盘</a>
<a class="link-btn" href="logout.action" id="logoutLink">退出登录</a>
</div>
</s:if>
<s:else>
<strong id="sessionStateTitle">当前未登录</strong>
<div id="sessionStateBody" data-authenticated="false"></div>
<div class="demo-links" style="margin-top: 14px;">
<a class="link-btn" href="loginPage.action" id="loginLink">打开登录页</a>
</div>
</s:else>
</div>
</section>
<section class="card">
<div class="eyebrow" id="routeEyebrow">学习顺序</div>
<h2 id="routeTitle">建议你按这个顺序看</h2>
<div class="pipeline">
<div class="step">
<strong id="routeStep1Title">1. Hello Action</strong>
<p id="routeStep1Text">先看最小动作映射,建立 request -> action -> result 的基本心智模型。</p>
</div>
<div class="step">
<strong id="routeStep2Title">2. Session 登录</strong>
<p id="routeStep2Text">看登录动作如何写入 Session并由拦截器保护后续实验页。</p>
</div>
<div class="step">
<strong id="routeStep3Title">3. 表单与校验</strong>
<p id="routeStep3Text">看字段绑定、校验错误和成功页汇总。</p>
</div>
<div class="step">
<strong id="routeStep4Title">4. 上传与 JSON</strong>
<p id="routeStep4Text">最后再看文件上传和 JSON 返回,理解经典 MVC 如何扩展。</p>
</div>
</div>
</section>
</aside>
<main class="content">
<section class="card">
<div class="eyebrow" id="catalogEyebrow">实验目录</div>
<h2 id="catalogTitle">核心实验入口</h2>
<div class="grid" id="demoGrid">
<article class="demo-card" data-keywords="hello action request parameter basics 基础 动作 映射">
<div class="tag-list">
<span class="tag" id="tagHello1">动作</span>
<span class="tag" id="tagHello2">基础</span>
</div>
<h3 id="cardHelloTitle">Hello Action</h3>
<p id="cardHelloText">运行最小 Struts2 动作,观察请求参数如何进入 Action再由结果页返回浏览器。</p>
<div class="demo-links">
<a class="link-btn" href="hello.action?name=Platform%20Team" id="cardHelloRun">运行示例</a>
</div>
</article>
<article class="demo-card" data-keywords="login session auth interceptor dashboard 登录 会话 鉴权 拦截器">
<div class="tag-list">
<span class="tag" id="tagLogin1">登录</span>
<span class="tag" id="tagLogin2">Session</span>
</div>
<h3 id="cardLoginTitle">登录与仪表盘</h3>
<p id="cardLoginText">这部分是本项目的核心改造点:用经典 Session 登录和拦截器串起后续实验页。</p>
<div class="demo-links">
<a class="link-btn" href="loginPage.action" id="cardLoginOpen">打开登录</a>
<a class="link-btn" href="dashboard.action" id="cardLoginDashboard">打开仪表盘</a>
</div>
</article>
<article class="demo-card" data-keywords="user form submit binding profile 用户 表单 绑定">
<div class="tag-list">
<span class="tag" id="tagUser1">表单</span>
<span class="tag" id="tagUser2">绑定</span>
</div>
<h3 id="cardUserTitle">用户资料提交</h3>
<p id="cardUserText">演示字段绑定、错误回显和成功汇总页,也是最适合讲参数注入的例子。</p>
<div class="demo-links">
<a class="link-btn" href="userFormPage.action" id="cardUserOpen">打开用户表单</a>
</div>
</article>
<article class="demo-card" data-keywords="validation field errors age email 校验 错误 表单">
<div class="tag-list">
<span class="tag" id="tagValidation1">校验</span>
<span class="tag" id="tagValidation2">输入规则</span>
</div>
<h3 id="cardValidationTitle">字段校验实验</h3>
<p id="cardValidationText">对比校验失败和成功页面,理解为什么 Struts2 会在业务逻辑之前先跑 validate。</p>
<div class="demo-links">
<a class="link-btn" href="validationPage.action" id="cardValidationOpen">打开校验页</a>
</div>
</article>
<article class="demo-card" data-keywords="upload multipart metadata 文件 上传 multipart">
<div class="tag-list">
<span class="tag" id="tagUpload1">上传</span>
<span class="tag" id="tagUpload2">安全演示</span>
</div>
<h3 id="cardUploadTitle">文件上传元数据</h3>
<p id="cardUploadText">保留 multipart 绑定教学价值,但不真正落盘,适合本地和 VPS 环境安全演示。</p>
<div class="demo-links">
<a class="link-btn" href="uploadPage.action" id="cardUploadOpen">打开上传页</a>
</div>
</article>
<article class="demo-card" data-keywords="ajax json rest api 接口 json ajax">
<div class="tag-list">
<span class="tag" id="tagJson1">JSON</span>
<span class="tag" id="tagJson2">接口风格</span>
</div>
<h3 id="cardJsonTitle">AJAX 与 REST 风格返回</h3>
<p id="cardJsonText">保留 JSON 动作和 REST 风格示例,用来说明经典 MVC 项目如何逐步演进到接口输出。</p>
<div class="demo-links">
<a class="link-btn" href="ajax.action" id="cardJsonAjax">打开 AJAX JSON</a>
<a class="link-btn" href="api/users.action" id="cardJsonRest">打开 REST JSON</a>
</div>
</article>
</div>
<div class="empty" id="emptyState" style="display: none; margin-top: 16px;">当前关键词没有匹配到实验。</div>
</section>
<section class="card learning-panel-wrapper">
<div class="eyebrow" id="learningBoardEyebrow">可视化学习</div>
<h2 id="learningBoardTitle">Struts2 请求与鉴权流程图</h2>
<p id="learningBoardDesc">通过时序卡片展示请求到达、拦截、执行和结果之间的关系,并把登录保护链路具体化,增强讲解能力。</p>
<div class="learning-grid">
<article class="learning-panel">
<div class="eyebrow" id="timelineEyebrow">请求生命周期</div>
<h3 id="timelineTitle">请求进入 → Action → 结果</h3>
<p id="timelineDesc">Struts2 将 URL 映射到 Action执行之前先跑拦截器再决定返回的 JSP 或 JSON。</p>
<div class="flow-steps">
<div class="flow-step">
<strong id="timelineStep1">1. 请求进来</strong>
<p id="timelineStep1Text">Dispatcher 解析 namespace 与 action构造参数并执行</p>
</div>
<div class="flow-step">
<strong id="timelineStep2">2. 拦截器链</strong>
<p id="timelineStep2Text">`AuthInterceptor` 检测 Session其他拦截器做参数/校验/文件准备</p>
</div>
<div class="flow-step">
<strong id="timelineStep3">3. 结果输出</strong>
<p id="timelineStep3Text">Action 返回 SUCCESS/INPUTStruts 渲染 JSP 或 JSON浏览器读取响应</p>
</div>
</div>
</article>
<article class="learning-panel">
<div class="eyebrow" id="chainEyebrow">Action → Interceptor → Result</div>
<h3 id="chainTitle">链式控制:操作 → 鉴权 → 渲染</h3>
<p id="chainDesc">每个请求都必须穿过这个三段式,理解它能帮助你解释 Struts2 的核心执行模型。</p>
<div class="chain-pill" id="chainAction">Action</div>
<div class="chain-pill" id="chainInterceptor">Interceptor</div>
<div class="chain-pill" id="chainResult">Result</div>
<p id="chainExplain">Action 负责业务,拦截器负责验证/鉴权Result 负责视图渲染或 JSON 返回。</p>
</article>
</div>
</section>
<section class="card">
<div class="eyebrow" id="insightEyebrow">学习洞察</div>
<h2 id="insightTitle">关键链路 & 实验对照</h2>
<div class="insight-grid">
<article class="insight-card">
<div class="eyebrow">登录保护链路</div>
<h3 id="loginCardTitle">Session 登录 + 拦截器</h3>
<p id="loginCardDesc">运用 `LoginAction`、`AuthInterceptor` 和 `DashboardAction` 来完成端到端登录和受保护导航。</p>
<ul>
<li id="loginCardItem1">输入 admin / 123456 写入 Session</li>
<li id="loginCardItem2">`AuthInterceptor` 拦截未登录访问</li>
<li id="loginCardItem3">登录后跳转到专属仪表盘再进实验</li>
</ul>
</article>
<article class="insight-card">
<div class="eyebrow">表单绑定 & 校验</div>
<h3 id="bindingCardTitle">数据绑定观察卡片</h3>
<p id="bindingCardDesc">从 `userFormPage` 到 `submitUser`,展示字段绑定、校验触发和成功结果的行为差异。</p>
<ul>
<li id="bindingCardItem1">字段名Action 属性与表单 `name` 一一对应</li>
<li id="bindingCardItem2">验证逻辑:`UserAction#submit` 里点亮不同结果</li>
<li id="bindingCardItem3">成功后跳转 `user/success.jsp` 显示绑定概览</li>
</ul>
</article>
<article class="insight-card">
<div class="eyebrow">JSON 对照模块</div>
<h3 id="jsonCardTitle">AJAX vs REST</h3>
<p id="jsonCardDesc">并列演示 `ajax.action`AJAX JSON和 `api/users.action`REST JSON的输出差异便于讲解 Struts 如何扩展接口。</p>
<ul>
<li id="jsonCardItem1">`ajax.action` 面向浏览器交互,返回 `success` + data</li>
<li id="jsonCardItem2">`api/users.action` 保持 REST 语义,便于讲解 JSON 策略</li>
</ul>
</article>
</div>
</section>
</main>
</div>
</div>
<script src="assets/struts-lab.js"></script>
<script>
const searchInput = document.getElementById("searchInput");
const cards = Array.from(document.querySelectorAll(".demo-card"));
const emptyState = document.getElementById("emptyState");
let ui = null;
function runSearch() {
const keyword = searchInput.value.trim().toLowerCase();
let visible = 0;
cards.forEach((card) => {
const match = !keyword || card.dataset.keywords.includes(keyword);
card.style.display = match ? "block" : "none";
if (match) {
visible += 1;
}
});
emptyState.style.display = visible ? "none" : "block";
if (ui) {
emptyState.textContent = ui.messages.emptyState;
}
}
searchInput.addEventListener("input", runSearch);
ui = strutsLab.mount({
messages: {
zh: {
pageTitle: "Struts2 学习实验台",
text: {
heroEyebrow: "Struts2 学习实验台",
heroTitle: "把零散示例整理成一条可讲解、可演示、可验证的学习路线",
heroText: "这个入口页现在不再只是样例链接集合,而是把经典 Struts2 的动作映射、参数绑定、Session 登录、表单校验和文件上传串成一个可循序学习的 Demo 门户。",
primaryAction: "进入登录章节",
secondaryAction: "运行 Hello Action",
metricRoutesLabel: "关键动作",
metricSecureLabel: "鉴权节点",
metricFormsLabel: "表单实验",
metricApiLabel: "JSON 示例",
searchEyebrow: "快速检索",
searchTitle: "按关键词筛选实验",
statusEyebrow: "当前会话",
statusTitle: "登录状态与访问建议",
routeEyebrow: "学习顺序",
routeTitle: "建议你按这个顺序看",
sessionStateTitle: "当前已登录",
dashboardLink: "打开仪表盘",
logoutLink: "退出登录",
loginLink: "打开登录页",
routeStep1Title: "1. Hello Action",
routeStep1Text: "先看最小动作映射,建立 request -> action -> result 的基本心智模型。",
routeStep2Title: "2. Session 登录",
routeStep2Text: "看登录动作如何写入 Session并由拦截器保护后续实验页。",
routeStep3Title: "3. 表单与校验",
routeStep3Text: "看字段绑定、校验错误和成功页汇总。",
routeStep4Title: "4. 上传与 JSON",
routeStep4Text: "最后再看文件上传和 JSON 返回,理解经典 MVC 如何扩展。",
catalogEyebrow: "实验目录",
catalogTitle: "核心实验入口",
tagHello1: "动作",
tagHello2: "基础",
cardHelloTitle: "Hello Action",
cardHelloText: "运行最小 Struts2 动作,观察请求参数如何进入 Action再由结果页返回浏览器。",
cardHelloRun: "运行示例",
tagLogin1: "登录",
tagLogin2: "Session",
cardLoginTitle: "登录与仪表盘",
cardLoginText: "这部分是本项目的核心改造点:用经典 Session 登录和拦截器串起后续实验页。",
cardLoginOpen: "打开登录",
cardLoginDashboard: "打开仪表盘",
tagUser1: "表单",
tagUser2: "绑定",
cardUserTitle: "用户资料提交",
cardUserText: "演示字段绑定、错误回显和成功汇总页,也是最适合讲参数注入的例子。",
cardUserOpen: "打开用户表单",
tagValidation1: "校验",
tagValidation2: "输入规则",
cardValidationTitle: "字段校验实验",
cardValidationText: "对比校验失败和成功页面,理解为什么 Struts2 会在业务逻辑之前先跑 validate。",
cardValidationOpen: "打开校验页",
tagUpload1: "上传",
tagUpload2: "安全演示",
cardUploadTitle: "文件上传元数据",
cardUploadText: "保留 multipart 绑定教学价值,但不真正落盘,适合本地和 VPS 环境安全演示。",
cardUploadOpen: "打开上传页",
tagJson1: "JSON",
tagJson2: "接口风格",
cardJsonTitle: "AJAX 与 REST 风格返回",
cardJsonText: "保留 JSON 动作和 REST 风格示例,用来说明经典 MVC 项目如何逐步演进到接口输出。",
cardJsonAjax: "打开 AJAX JSON",
cardJsonRest: "打开 REST JSON",
learningBoardEyebrow: "可视化学习",
learningBoardTitle: "Struts2 请求与鉴权流程图",
learningBoardDesc: "用时序卡片展示请求穿越 Action、拦截器和 Result 的全链路,让讲解更直观。",
timelineEyebrow: "请求生命周期",
timelineTitle: "请求进入 → Action → 结果",
timelineDesc: "Struts2 先解析 action再跑拦截器最后决定 JSP 或 JSON 的渲染。",
timelineStep1: "1. 请求进来",
timelineStep1Text: "Dispatcher 解析 namespace、action注入参数并准备执行。",
timelineStep2: "2. 拦截器链",
timelineStep2Text: "`AuthInterceptor` 检查 Session其它拦截器做校验/上传处理。",
timelineStep3: "3. 结果输出",
timelineStep3Text: "Action 返回 SUCCESS/INPUTStruts 渲染 JSP 或 JSON 回应。",
chainEyebrow: "Action → Interceptor → Result",
chainTitle: "链式控制:操作 → 鉴权 → 渲染",
chainDesc: "中间拦截器决定请求是否通过,结果决定渲染视图或接口响应。",
chainExplain: "Action 负责业务Interceptor 掌握鉴权细节Result 才真正交给浏览器。",
insightEyebrow: "学习洞察",
insightTitle: "关键链路与实验对照",
loginCardTitle: "Session 登录 + 拦截器",
loginCardDesc: "用 `LoginAction`、`AuthInterceptor` 和仪表盘串起端到端链路。",
loginCardItem1: "输入 admin / 123456 写入 Session",
loginCardItem2: "`AuthInterceptor` 拦截未登录访问",
loginCardItem3: "登录后跳转仪表盘再进入实验页",
bindingCardTitle: "表单绑定观察卡片",
bindingCardDesc: "从用户表单到 `submitUser`,看字段绑定与校验行为如何变化。",
bindingCardItem1: "字段名与 Action 属性一一对应",
bindingCardItem2: "`UserAction#submit` 根据校验返回不同结果",
bindingCardItem3: "成功后跳转 `user/success.jsp` 显示汇总",
jsonCardTitle: "AJAX vs REST JSON",
jsonCardDesc: "对照 `ajax.action` 与 `api/users.action` 的输出,讲解 Struts JSON 扩展。",
jsonCardItem1: "`ajax.action` 面向浏览器交互,返回 success + 数据",
jsonCardItem2: "`api/users.action` 保持 REST 格式,便于接口讲解"
},
html: {
helperList: "<li>先看登录和仪表盘,理解最经典的 Session 鉴权链路。</li><li>再看表单、校验、上传,理解 Action 如何接收与处理输入。</li><li>最后再看 AJAX 和 REST 风格返回,理解 Struts2 的扩展能力。</li>"
},
placeholders: {
searchInput: "试试 login、session、validation、upload、json"
},
emptyState: "当前关键词没有匹配到实验。"
},
en: {
pageTitle: "Struts2 Learning Lab",
text: {
heroEyebrow: "Struts2 Learning Lab",
heroTitle: "Turn scattered examples into one explainable and verifiable learning route",
heroText: "This entry page is now a structured learning portal that connects action mapping, parameter binding, session login, form validation, and file upload into one guided Struts2 demo.",
primaryAction: "Open login chapter",
secondaryAction: "Run hello action",
metricRoutesLabel: "Key actions",
metricSecureLabel: "Protected nodes",
metricFormsLabel: "Form labs",
metricApiLabel: "JSON samples",
searchEyebrow: "Quick search",
searchTitle: "Filter labs by keyword",
statusEyebrow: "Current session",
statusTitle: "Login state and access guidance",
routeEyebrow: "Study order",
routeTitle: "Suggested learning order",
sessionStateTitle: "Logged in",
dashboardLink: "Open dashboard",
logoutLink: "Log out",
loginLink: "Open login page",
routeStep1Title: "1. Hello action",
routeStep1Text: "Start with the smallest action mapping and build the request -> action -> result mental model.",
routeStep2Title: "2. Session login",
routeStep2Text: "See how login writes into session and how an interceptor protects later pages.",
routeStep3Title: "3. Forms and validation",
routeStep3Text: "Review field binding, validation errors, and success summaries.",
routeStep4Title: "4. Upload and JSON",
routeStep4Text: "Finish with file upload and JSON responses to see how classic MVC extends outward.",
catalogEyebrow: "Catalog",
catalogTitle: "Core lab entries",
tagHello1: "Action",
tagHello2: "Basics",
cardHelloTitle: "Hello action",
cardHelloText: "Run the smallest Struts2 action and inspect how request parameters reach the action and return through a result page.",
cardHelloRun: "Run demo",
tagLogin1: "Login",
tagLogin2: "Session",
cardLoginTitle: "Login and dashboard",
cardLoginText: "This is the main upgrade in this project: a classic session login plus interceptor-protected learning pages.",
cardLoginOpen: "Open login",
cardLoginDashboard: "Open dashboard",
tagUser1: "Form",
tagUser2: "Binding",
cardUserTitle: "User profile submit",
cardUserText: "Demonstrates field binding, error echoing, and a success summary page.",
cardUserOpen: "Open user form",
tagValidation1: "Validation",
tagValidation2: "Rules",
cardValidationTitle: "Field validation lab",
cardValidationText: "Compare failed and successful validation states and explain why validate() runs before business output.",
cardValidationOpen: "Open validation page",
tagUpload1: "Upload",
tagUpload2: "Safe demo",
cardUploadTitle: "Upload metadata lab",
cardUploadText: "Keeps multipart binding visible but avoids writing files to disk, which is safer for local and VPS demos.",
cardUploadOpen: "Open upload page",
tagJson1: "JSON",
tagJson2: "API style",
cardJsonTitle: "AJAX and REST responses",
cardJsonText: "Keeps JSON and REST-style samples to show how a classic MVC project can evolve into API output.",
cardJsonAjax: "Open AJAX JSON",
cardJsonRest: "Open REST JSON",
learningBoardEyebrow: "Visual learning",
learningBoardTitle: "Struts2 request & auth flow",
learningBoardDesc: "Timeline cards highlight how requests traverse Action, Interceptor and Result so the flow is easier to explain.",
timelineEyebrow: "Request lifecycle",
timelineTitle: "Inbound → Action → Result",
timelineDesc: "The dispatcher resolves the action, runs interceptors, then chooses which JSP or JSON should render.",
timelineStep1: "1. Request arrives",
timelineStep1Text: "Dispatcher maps namespace/action, binds parameters, prepares execution.",
timelineStep2: "2. Interceptor chain",
timelineStep2Text: "`AuthInterceptor` enforces session while others handle validation/uploads.",
timelineStep3: "3. Result output",
timelineStep3Text: "Action returns SUCCESS/INPUT and Struts renders JSP or JSON back to the browser.",
chainEyebrow: "Action → Interceptor → Result",
chainTitle: "Control chain: ops → auth → render",
chainDesc: "Interceptors decide if the request passes; the result determines view or API output.",
chainExplain: "Action owns business logic, Interceptor handles guardrails, Result delivers the final payload.",
insightEyebrow: "Learning insights",
insightTitle: "Key links & lab comparison",
loginCardTitle: "Session login + interceptor",
loginCardDesc: "`LoginAction`, `AuthInterceptor`, and the dashboard string together a safe auth chapter.",
loginCardItem1: "Submit admin / 123456 to write session",
loginCardItem2: "`AuthInterceptor` blocks unauthenticated access",
loginCardItem3: "Successful login lands on dashboard before labs",
bindingCardTitle: "Form binding snapshot",
bindingCardDesc: "Observe how the user form, validation, and result respond to field binding and checks.",
bindingCardItem1: "Form field names mirror Action properties",
bindingCardItem2: "`UserAction#submit` routes based on validation",
bindingCardItem3: "Success page shows a structured summary",
jsonCardTitle: "AJAX vs REST JSON",
jsonCardDesc: "Line up `ajax.action` and `api/users.action` to explain Struts JSON modes.",
jsonCardItem1: "`ajax.action` targets browser interactions with success + payload",
jsonCardItem2: "`api/users.action` stays RESTy for API teaching"
},
html: {
helperList: "<li>Start with login and the dashboard to understand the classic session-auth path.</li><li>Move to forms, validation, and upload to see how actions receive and process input.</li><li>Finish with AJAX and REST-style responses to explain Struts2 extension points.</li>"
},
placeholders: {
searchInput: "Try login, session, validation, upload, json"
},
emptyState: "No lab matched the current keyword."
}
},
render: function (uiState) {
const sessionTitle = document.getElementById("sessionStateTitle");
const sessionBody = document.getElementById("sessionStateBody");
if (!sessionTitle || !sessionBody) {
return;
}
const authenticated = sessionBody.dataset.authenticated === "true";
if (authenticated) {
const roleLabel = uiState.language === "zh" ? "演示管理员" : "Demo administrator";
sessionTitle.textContent = uiState.language === "zh" ? "当前已登录" : "Logged in";
sessionBody.innerHTML = uiState.language === "zh"
? `用户:${sessionBody.dataset.user}<br/>角色:${roleLabel}<br/>建议先打开仪表盘,再进入受保护表单页。`
: `User: ${sessionBody.dataset.user}<br/>Role: ${roleLabel}<br/>Open the dashboard first, then continue to the protected form pages.`;
} else {
sessionTitle.textContent = uiState.language === "zh" ? "当前未登录" : "Not logged in";
sessionBody.innerHTML = uiState.language === "zh"
? "建议先进入登录页。用户表单、校验页和上传页都已经接入 Session 鉴权链路。"
: "Start with the login page. The user form, validation page, and upload page now sit behind the session-auth path.";
}
}
});
runSearch();
</script>
</body>
</html>