811 lines
44 KiB
Plaintext
811 lines
44 KiB
Plaintext
<%@ 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/INPUT,Struts 渲染 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/INPUT,Struts 渲染 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>
|