Files
struts2-demo/web/WEB-INF/views/index.jsp

602 lines
31 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;
}
@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>
</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"
},
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"
},
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>