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,44 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.interceptor.SessionAware;
import java.util.Map;
public class DashboardAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
private String displayName;
private String role;
private String loginTime;
@Override
public String execute() {
displayName = value(LoginAction.SESSION_USER, "ops-admin");
role = value(LoginAction.SESSION_ROLE, "admin");
loginTime = value(LoginAction.SESSION_LOGIN_TIME, "--");
return SUCCESS;
}
private String value(String key, String fallback) {
Object value = session == null ? null : session.get(key);
return value == null ? fallback : String.valueOf(value);
}
public String getDisplayName() {
return displayName;
}
public String getRole() {
return role;
}
public String getLoginTime() {
return loginTime;
}
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
}

View File

@@ -27,11 +27,11 @@ public class FileUploadAction extends ActionSupport {
}
if (fileCount == 0) {
addActionError("Select at least one file before submitting the demo.");
addActionError("请至少选择一个文件再提交。 / Select at least one file before submitting the demo.");
return INPUT;
}
summary = "This demo captures upload metadata only. It does not persist files to disk, which keeps the sample safe for classroom use.";
summary = "metadata-only";
return SUCCESS;
}

View File

@@ -1,20 +1,25 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.interceptor.SessionAware;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
public class LoginAction extends ActionSupport {
public class LoginAction extends ActionSupport implements SessionAware {
public static final String SESSION_USER = "demoUser";
public static final String SESSION_ROLE = "demoRole";
public static final String SESSION_LOGIN_TIME = "demoLoginTime";
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private Map<String, Object> session;
private String username;
private String password;
private String displayName;
private String role;
private String loginTime;
private String recommendation;
@Override
public String execute() {
@@ -26,14 +31,15 @@ public class LoginAction extends ActionSupport {
}
if ("admin".equals(username) && "123456".equals(password)) {
displayName = "System Demo Admin";
role = "Administrator";
displayName = "ops-admin";
loginTime = LocalDateTime.now().format(TIME_FORMATTER);
recommendation = "Continue with the user form, validation sample, or upload flow to explore the rest of the demo.";
session.put(SESSION_USER, displayName);
session.put(SESSION_ROLE, "admin");
session.put(SESSION_LOGIN_TIME, loginTime);
return SUCCESS;
}
addActionError("Invalid demo credentials. Use admin / 123456.");
addActionError("演示账号不正确,请使用 admin / 123456。 / Invalid demo credentials. Use admin / 123456.");
return INPUT;
}
@@ -43,10 +49,10 @@ public class LoginAction extends ActionSupport {
return;
}
if (username == null || username.length() < 3) {
addFieldError("username", "Username must be at least 3 characters.");
addFieldError("username", "用户名至少 3 个字符。 / Username must be at least 3 characters.");
}
if (password == null || password.length() < 6) {
addFieldError("password", "Password must be at least 6 characters.");
addFieldError("password", "密码至少 6 个字符。 / Password must be at least 6 characters.");
}
}
@@ -79,15 +85,12 @@ public class LoginAction extends ActionSupport {
return displayName;
}
public String getRole() {
return role;
}
public String getLoginTime() {
return loginTime;
}
public String getRecommendation() {
return recommendation;
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
}

View File

@@ -0,0 +1,26 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.interceptor.SessionAware;
import java.util.Map;
public class LogoutAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
@Override
public String execute() {
if (session != null) {
session.remove(LoginAction.SESSION_USER);
session.remove(LoginAction.SESSION_ROLE);
session.remove(LoginAction.SESSION_LOGIN_TIME);
}
return SUCCESS;
}
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
}

View File

@@ -14,6 +14,7 @@ public class UserAction extends ActionSupport {
private String phone;
private String submittedAt;
private String profileStage;
private boolean profileReady;
public String submit() {
username = normalize(username);
@@ -25,22 +26,23 @@ public class UserAction extends ActionSupport {
}
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
profileStage = (phone != null && phone.length() >= 7) ? "Profile ready for follow-up demos." : "Profile captured. Add a stronger phone number next time.";
profileReady = phone != null && phone.length() >= 7;
profileStage = profileReady ? "ready" : "review";
return SUCCESS;
}
private boolean isValid() {
boolean valid = true;
if (username == null || username.length() < 3) {
addFieldError("username", "Username must be at least 3 characters.");
addFieldError("username", "用户名至少 3 个字符。 / Username must be at least 3 characters.");
valid = false;
}
if (email == null || !email.contains("@")) {
addFieldError("email", "Enter a valid email address.");
addFieldError("email", "请输入有效邮箱。 / Enter a valid email address.");
valid = false;
}
if (phone == null || phone.replaceAll("[^0-9]", "").length() < 7) {
addFieldError("phone", "Enter at least 7 digits for the phone number.");
addFieldError("phone", "手机号至少 7 位数字。 / Enter at least 7 digits for the phone number.");
valid = false;
}
return valid;
@@ -81,4 +83,8 @@ public class UserAction extends ActionSupport {
public String getProfileStage() {
return profileStage;
}
public boolean isProfileReady() {
return profileReady;
}
}

View File

@@ -15,13 +15,15 @@ public class ValidationAction extends ActionSupport {
private String bio;
private String scoreBand;
private String submittedAt;
private boolean seniorTrack;
@Override
public String execute() {
username = normalize(username);
email = normalize(email);
bio = normalize(bio);
scoreBand = age >= 30 ? "Mid-career operator profile" : "Early-career operator profile";
seniorTrack = age >= 30;
scoreBand = seniorTrack ? "mid" : "early";
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
return SUCCESS;
}
@@ -29,16 +31,16 @@ public class ValidationAction extends ActionSupport {
@Override
public void validate() {
if (username == null || username.trim().length() < 3 || username.trim().length() > 20) {
addFieldError("username", "Username must be between 3 and 20 characters.");
addFieldError("username", "用户名长度需在 3 到 20 之间。 / Username must be between 3 and 20 characters.");
}
if (email == null || !email.contains("@") || email.indexOf('@') == email.length() - 1) {
addFieldError("email", "Enter a valid email address.");
addFieldError("email", "请输入有效邮箱。 / Enter a valid email address.");
}
if (age == null || age < 18 || age > 60) {
addFieldError("age", "Age must be between 18 and 60.");
addFieldError("age", "年龄需在 18 到 60 之间。 / Age must be between 18 and 60.");
}
if (bio != null && bio.trim().length() > 240) {
addFieldError("bio", "Bio must stay under 240 characters.");
addFieldError("bio", "简介不能超过 240 个字符。 / Bio must stay under 240 characters.");
}
}
@@ -85,4 +87,8 @@ public class ValidationAction extends ActionSupport {
public String getSubmittedAt() {
return submittedAt;
}
public boolean isSeniorTrack() {
return seniorTrack;
}
}

View File

@@ -0,0 +1,19 @@
package com.demo.action.interceptor;
import com.demo.action.LoginAction;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import java.util.Map;
public class AuthInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session != null && session.get(LoginAction.SESSION_USER) != null) {
return invocation.invoke();
}
return "login";
}
}

View File

@@ -4,13 +4,30 @@
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<constant name="struts.devMode" value="true"/>
<constant name="struts.devMode" value="false"/>
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.i18n.encoding" value="UTF-8"/>
<constant name="struts.action.extension" value="action"/>
<package name="default" namespace="/" extends="json-default">
<interceptors>
<interceptor name="auth" class="com.demo.action.interceptor.AuthInterceptor"/>
<interceptor-stack name="secureStack">
<interceptor-ref name="auth"/>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<global-results>
<result name="login" type="redirectAction">loginPage</result>
</global-results>
<package name="default" namespace="/" extends="struts-default">
<action name="index">
<result>/index.jsp</result>
<result>/WEB-INF/views/index.jsp</result>
</action>
<action name="loginPage">
<result>/WEB-INF/views/user/login.jsp</result>
</action>
<action name="hello" class="com.demo.action.HelloAction" method="execute">
@@ -18,18 +35,44 @@
</action>
<action name="login" class="com.demo.action.LoginAction" method="execute">
<result name="success">/user/success.jsp</result>
<result name="input">/user/login.jsp</result>
<result name="success" type="redirectAction">dashboard</result>
<result name="input">/WEB-INF/views/user/login.jsp</result>
</action>
<action name="dashboard" class="com.demo.action.DashboardAction" method="execute">
<interceptor-ref name="secureStack"/>
<result>/WEB-INF/views/user/dashboard.jsp</result>
</action>
<action name="logout" class="com.demo.action.LogoutAction" method="execute">
<result type="redirectAction">loginPage</result>
</action>
<action name="userFormPage">
<interceptor-ref name="secureStack"/>
<result>/WEB-INF/views/user/form.jsp</result>
</action>
<action name="validationPage">
<interceptor-ref name="secureStack"/>
<result>/WEB-INF/views/validation/form.jsp</result>
</action>
<action name="uploadPage">
<interceptor-ref name="secureStack"/>
<result>/WEB-INF/views/upload/index.jsp</result>
</action>
<action name="submitUser" class="com.demo.action.UserAction" method="submit">
<result name="success">/user/success.jsp</result>
<result name="input">/user/form.jsp</result>
<interceptor-ref name="secureStack"/>
<result name="success">/WEB-INF/views/user/success.jsp</result>
<result name="input">/WEB-INF/views/user/form.jsp</result>
</action>
<action name="upload" class="com.demo.action.FileUploadAction" method="execute">
<result name="success">/upload/success.jsp</result>
<result name="input">/upload/index.jsp</result>
<interceptor-ref name="secureStack"/>
<result name="success">/WEB-INF/views/upload/success.jsp</result>
<result name="input">/WEB-INF/views/upload/index.jsp</result>
</action>
<action name="ajax" class="com.demo.action.AjaxAction" method="execute">
@@ -37,12 +80,13 @@
</action>
<action name="validate" class="com.demo.action.ValidationAction" method="execute">
<result name="success">/validation/success.jsp</result>
<result name="input">/validation/form.jsp</result>
<interceptor-ref name="secureStack"/>
<result name="success">/WEB-INF/views/validation/success.jsp</result>
<result name="input">/WEB-INF/views/validation/form.jsp</result>
</action>
</package>
<package name="rest" namespace="/api" extends="struts-default">
<package name="rest" namespace="/api" extends="json-default">
<action name="users" class="com.demo.action.rest.UserRestAction" method="execute">
<result type="json"/>
</action>

601
web/WEB-INF/views/index.jsp Normal file
View File

@@ -0,0 +1,601 @@
<%@ 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>

View File

@@ -1,11 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Demo - Struts2 Demo Lab</title>
<title>上传元数据实验 - Struts2 学习实验台</title>
<style>
body {
margin: 0;
@@ -81,13 +81,24 @@
</head>
<body>
<div class="shell">
<div class="eyebrow">Upload demo</div>
<h1>Capture file metadata without writing anything to disk</h1>
<p>This safer upload sample focuses on how Struts binds multipart form fields. The action records metadata only, which makes the demo easier to run in constrained environments.</p>
<div class="eyebrow">受保护上传页</div>
<h1>只采集上传元数据,不把文件真正落盘</h1>
<p>这个页面保留了 multipart 表单绑定的教学意义但避免真实写盘更适合本地、VPS 和课堂演示环境。</p>
<s:if test="#session.demoUser == null">
<div class="note">
<strong>What the action collects</strong>
<p style="margin-top: 8px;">Primary filename, content type, and total selected file count.</p>
<strong>当前未登录</strong>
<p style="margin-top: 8px;">上传页也已经接入 Session 保护。请先登录,再看 Struts2 对文件字段和元数据的绑定结果。</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;">主文件名、内容类型和总文件数量。不会真实保存上传文件。</p>
</div>
<s:if test="hasActionErrors()">
@@ -96,22 +107,23 @@
<s:form action="upload" method="post" enctype="multipart/form-data" namespace="/">
<div class="field">
<label>Primary file</label>
<label>主文件</label>
<s:file name="upload"/>
</div>
<div class="field">
<label>Extra files</label>
<label>附加文件</label>
<s:file name="uploads" multiple="multiple"/>
</div>
<button type="submit">Submit upload metadata</button>
<button type="submit">提交并查看上传元数据</button>
</s:form>
<div class="links">
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="../demo/upload/index.jsp">Open upload guide</a>
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
<a class="link-btn" href="../index.action">回到门户</a>
</div>
</s:else>
</div>
</body>
</html>

View File

@@ -1,11 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Summary - Struts2 Demo Lab</title>
<title>上传元数据结果 - Struts2 学习实验台</title>
<style>
body {
margin: 0;
@@ -64,28 +64,28 @@
</head>
<body>
<div class="card">
<div class="eyebrow">Upload summary</div>
<h1>Upload metadata captured successfully</h1>
<p><s:property value="summary" default="The demo collected upload metadata without persisting files."/></p>
<div class="eyebrow">上传结果</div>
<h1>上传元数据已经被 Action 成功接收</h1>
<p>这个实验只记录元数据,不真正保存文件。重点是理解 Struts2 对 multipart 表单字段的接收方式。</p>
<div class="stats">
<div class="stat">
<span>Selected files</span>
<span>文件数量</span>
<strong><s:property value="fileCount" default="0"/></strong>
</div>
<div class="stat">
<span>Primary filename</span>
<strong><s:property value="uploadFileName" default="Not provided"/></strong>
<span>主文件名</span>
<strong><s:property value="uploadFileName" default="未提供"/></strong>
</div>
<div class="stat">
<span>Content type</span>
<strong><s:property value="uploadContentType" default="Unknown"/></strong>
<span>内容类型</span>
<strong><s:property value="uploadContentType" default="未知"/></strong>
</div>
</div>
<div class="links">
<a class="link-btn" href="index.jsp">Try another upload</a>
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="../uploadPage.action">再做一次上传实验</a>
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
</div>
</div>
</body>

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

@@ -1,11 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Success Dashboard - Struts2 Demo Lab</title>
<title>用户资料汇总 - Struts2 学习实验台</title>
<style>
body {
margin: 0;
@@ -74,58 +74,57 @@
<body>
<div class="shell">
<section class="card">
<div class="eyebrow">Result</div>
<h1>Demo flow completed successfully</h1>
<p>This page now acts as a shared success dashboard for multiple form-based actions. It makes the result screen more useful than a single success line.</p>
<div class="eyebrow">提交成功</div>
<h1>用户资料已经通过 Action 处理并汇总完成</h1>
<p>这页现在只负责展示用户资料提交的结果,不再和登录成功页面混用,便于你单独理解“表单提交成功页”这一类经典 Struts2 结果页。</p>
</section>
<section class="card">
<div class="eyebrow">Current submission</div>
<h2><s:property value="displayName != null ? displayName : username"/></h2>
<p><s:property value="recommendation != null ? recommendation : profileStage"/></p>
<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>Username</span>
<strong><s:property value="username" default="demo-user"/></strong>
<span>用户名</span>
<strong><s:property value="username"/></strong>
</div>
<div class="stat">
<span>Role or stage</span>
<strong><s:property value="role != null ? role : profileStage"/></strong>
<span>邮箱</span>
<strong><s:property value="email"/></strong>
</div>
<div class="stat">
<span>Timestamp</span>
<strong><s:property value="loginTime != null ? loginTime : submittedAt"/></strong>
<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="../index.jsp">Back to portal</a>
<a class="link-btn" href="form.jsp">Open user form</a>
<a class="link-btn" href="../validation/form.jsp">Run validation demo</a>
<a class="link-btn" href="../upload/index.jsp">Open upload demo</a>
</div>
</section>
<section class="card">
<div class="eyebrow">What happened</div>
<h2>Map this result back to the Struts stack</h2>
<div class="stats">
<div class="stat">
<span>Action mapping</span>
<strong>login</strong>
<p>The request matched the <code>login</code> action in <code>struts.xml</code>.</p>
</div>
<div class="stat">
<span>Execute result</span>
<strong>SUCCESS</strong>
<p>The action returned the success result instead of redisplaying the form.</p>
</div>
<div class="stat">
<span>Rendered view</span>
<strong>/user/success.jsp</strong>
<p>The result mapping selected this JSP to present the post-action summary.</p>
</div>
<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>

View File

@@ -0,0 +1,133 @@
<%@ 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, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
}
.shell {
max-width: 900px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
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: #db2777;
font-weight: 800;
}
h1 { margin: 10px 0 12px; }
p { margin: 0; color: #6e5565; line-height: 1.85; }
.field { margin-top: 16px; }
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4a223b; }
input, textarea {
width: 100%;
padding: 13px 14px;
border-radius: 14px;
border: 1px solid #ecd6e0;
font: inherit;
}
textarea { resize: vertical; min-height: 120px; }
button {
width: 100%;
margin-top: 18px;
padding: 14px;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #db2777, #f472b6);
color: white;
font-size: 16px;
font-weight: 800;
cursor: pointer;
}
.error { color: #c0265d; font-size: 13px; margin-top: 6px; }
.note {
margin-top: 18px;
padding: 16px;
border-radius: 18px;
background: #fff3f8;
border: 1px solid #f3d8e6;
}
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.link-btn {
display: inline-flex;
padding: 10px 14px;
border-radius: 999px;
background: #fff0f7;
color: #db2777;
text-decoration: none;
font-weight: 700;
}
</style>
</head>
<body>
<div class="shell">
<div class="eyebrow">受保护校验页</div>
<h1>在业务执行前,先把输入规则讲清楚</h1>
<p>这个实验页专门用来讲 Struts2 的 <code>validate()</code>。你可以故意输错,再看字段错误如何被带回到原页面。</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;">先故意输入一个短用户名、错误邮箱和超范围年龄,再修正后重新提交,对比两次页面反馈。</p>
</div>
<s:form action="validate" method="post" namespace="/">
<div class="field">
<label for="username">用户名3 到 20 字符)</label>
<s:textfield id="username" name="username" placeholder="release-manager"/>
<div class="error"><s:fielderror fieldName="username"/></div>
</div>
<div class="field">
<label for="email">邮箱</label>
<s:textfield id="email" name="email" placeholder="release@example.com"/>
<div class="error"><s:fielderror fieldName="email"/></div>
</div>
<div class="field">
<label for="age">年龄18 到 60</label>
<s:textfield id="age" name="age" placeholder="30"/>
<div class="error"><s:fielderror fieldName="age"/></div>
</div>
<div class="field">
<label for="bio">简介</label>
<s:textarea id="bio" name="bio" placeholder="描述角色、经验或当前负责的平台方向。"/>
<div class="error"><s:fielderror fieldName="bio"/></div>
</div>
<button type="submit">提交并查看校验结果</button>
</s:form>
<div class="links">
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
<a class="link-btn" href="../uploadPage.action">继续看上传页</a>
</div>
</s:else>
</div>
</body>
</html>

View File

@@ -1,11 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Validation Summary - Struts2 Demo Lab</title>
<title>校验结果汇总 - Struts2 学习实验台</title>
<style>
body {
margin: 0;
@@ -18,7 +18,7 @@
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
}
.card {
width: min(820px, 100%);
width: min(860px, 100%);
background: rgba(255,255,255,0.95);
border-radius: 28px;
padding: 28px;
@@ -64,34 +64,37 @@
</head>
<body>
<div class="card">
<div class="eyebrow">Validation summary</div>
<h1>Profile validation passed</h1>
<p>The action accepted the submitted fields and calculated a simple profile band for the demo.</p>
<div class="eyebrow">校验通过</div>
<h1>所有输入都满足当前 Action 的校验规则</h1>
<p>这页的价值在于把字段校验和业务执行分开解释。只有当所有字段都通过时Action 才会进入成功结果页。</p>
<div class="stats">
<div class="stat"><span>Username</span><strong><s:property value="username"/></strong></div>
<div class="stat"><span>Email</span><strong><s:property value="email"/></strong></div>
<div class="stat"><span>Age</span><strong><s:property value="age"/></strong></div>
<div class="stat"><span>Profile band</span><strong><s:property value="scoreBand"/></strong></div>
<div class="stat"><span>Submitted at</span><strong><s:property value="submittedAt"/></strong></div>
<div class="stat"><span>Bio</span><strong><s:property value="bio" default="No bio submitted."/></strong></div>
<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="age"/></strong></div>
<div class="stat">
<span>阶段判断</span>
<strong><s:if test="seniorTrack">中阶段运维画像</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><s:property value="bio" default="未填写简介"/></strong></div>
</div>
<div class="links">
<a class="link-btn" href="form.jsp">Try validation again</a>
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="../validationPage.action">重新做一次校验</a>
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
</div>
<div class="stats">
<div class="stat">
<span>Rule path</span>
<span>规则入口</span>
<strong>validate()</strong>
The action accepted every field, so execution continued to the success result.
先校验,再决定是否允许执行 <code>execute()</code>。
</div>
<div class="stat">
<span>Teaching focus</span>
<strong>Input before logic</strong>
Compare this page with the form error state to explain why validation runs before business output.
<span>教学重点</span>
<strong>输入先于业务</strong>
这正是传统 Struts2 表单页面里最值得讲清楚的部分。
</div>
</div>
</div>

92
web/assets/struts-lab.js Normal file
View File

@@ -0,0 +1,92 @@
(function () {
const LANGUAGE_KEY = "struts_lab_language";
function getLanguage() {
return localStorage.getItem(LANGUAGE_KEY) === "en" ? "en" : "zh";
}
function setLanguage(language) {
const resolved = language === "en" ? "en" : "zh";
localStorage.setItem(LANGUAGE_KEY, resolved);
document.documentElement.lang = resolved === "zh" ? "zh-CN" : "en";
return resolved;
}
function applyText(values) {
Object.entries(values || {}).forEach(([id, value]) => {
const node = document.getElementById(id);
if (node) {
node.textContent = value;
}
});
}
function applyHtml(values) {
Object.entries(values || {}).forEach(([id, value]) => {
const node = document.getElementById(id);
if (node) {
node.innerHTML = value;
}
});
}
function applyPlaceholders(values) {
Object.entries(values || {}).forEach(([id, value]) => {
const node = document.getElementById(id);
if (node) {
node.setAttribute("placeholder", value);
}
});
}
function mount(config) {
const state = {
language: setLanguage(getLanguage())
};
const button = document.getElementById(config.buttonId || "languageBtn");
function messages() {
return config.messages[state.language] || config.messages.zh || {};
}
function render() {
const current = messages();
if (current.pageTitle) {
document.title = current.pageTitle;
}
applyText(current.text);
applyHtml(current.html);
applyPlaceholders(current.placeholders);
if (button) {
button.textContent = state.language === "zh" ? "EN" : "中文";
}
if (typeof config.render === "function") {
config.render({
language: state.language,
messages: current,
setLanguage
});
}
}
if (button) {
button.addEventListener("click", function () {
state.language = setLanguage(state.language === "zh" ? "en" : "zh");
render();
});
}
render();
return {
getLanguage: function () {
return state.language;
}
};
}
window.strutsLab = {
mount: mount,
getLanguage: getLanguage,
setLanguage: setLanguage
};
})();

View File

@@ -42,8 +42,8 @@
}</pre>
<div class="links">
<a class="btn" href="../../ajax">Open action JSON</a>
<a class="btn" href="../../api/users">Open REST JSON</a>
<a class="btn" href="../../ajax.action">Open action JSON</a>
<a class="btn" href="../../api/users.action">Open REST JSON</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
</div>
</div>

View File

@@ -45,7 +45,7 @@
}</pre>
<div class="links">
<a class="btn" href="../../hello?name=Platform%20Team">Run hello action</a>
<a class="btn" href="../../hello.action?name=Platform%20Team">Run hello action</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Model Binding Guide</title>
<title>模型绑定指南</title>
<style>
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #7c3aed, #a855f7); }
.shell { max-width: 980px; 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); }
@@ -18,22 +18,22 @@
</head>
<body>
<div class="shell">
<div class="eyebrow">Guide</div>
<h1>Property binding vs model-driven binding</h1>
<p>This note helps explain how Struts2 turns request parameters into action state. It works well alongside the user form and validation examples.</p>
<div class="eyebrow">指南</div>
<h1>属性绑定与 ModelDriven 绑定怎么理解</h1>
<p>这页用来解释 Struts2 如何把请求参数装配进 Action。建议配合用户资料表单和字段校验实验一起看。</p>
<h2>Property-driven</h2>
<h2>属性驱动绑定</h2>
<ul>
<li>Define simple fields directly on the action.</li>
<li>Expose setters and getters for each form field.</li>
<li>Great for short demos and small forms.</li>
<li>直接在 Action 上定义简单字段。</li>
<li>为表单字段提供对应的 setter / getter</li>
<li>适合短流程 Demo 和小型表单。</li>
</ul>
<h2>Model-driven</h2>
<h2>ModelDriven 绑定</h2>
<ul>
<li>Return a dedicated model object from <code>getModel()</code>.</li>
<li>Keep request data separate from action orchestration logic.</li>
<li>Better for larger forms and nested objects.</li>
<li>通过 <code>getModel()</code> 返回一个独立模型对象。</li>
<li>把请求数据和 Action 编排逻辑拆开。</li>
<li>更适合复杂表单和嵌套对象。</li>
</ul>
<pre>public class UserAction extends ActionSupport implements ModelDriven&lt;User&gt; {
@@ -46,8 +46,8 @@
}</pre>
<div class="links">
<a class="btn" href="../../user/form.jsp">Open user form</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
<a class="btn" href="../../userFormPage.action">打开用户表单实验</a>
<a class="btn" href="../../index.action">返回门户</a>
</div>
</div>
</body>

View File

@@ -1,10 +1,10 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Guide</title>
<title>上传实验指南</title>
<style>
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #0ea5e9, #38bdf8); }
.shell { max-width: 980px; 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); }
@@ -18,16 +18,16 @@
</head>
<body>
<div class="shell">
<div class="eyebrow">Guide</div>
<h1>Upload demo guide</h1>
<p>The refreshed upload flow is deliberately safer for demo use. It shows multipart binding and metadata capture without persisting files.</p>
<div class="eyebrow">指南</div>
<h1>上传实验怎么讲</h1>
<p>现在这条上传链路故意做成“只采集元数据,不真实落盘”的安全版,更适合本地和 VPS 演示环境。</p>
<h2>Core binding fields</h2>
<h2>核心绑定字段</h2>
<ul>
<li><code>File upload</code> for the primary file object.</li>
<li><code>String uploadFileName</code> for the original filename.</li>
<li><code>String uploadContentType</code> for the MIME type.</li>
<li>Optional lists for multiple files.</li>
<li><code>File upload</code> 负责接收主文件对象。</li>
<li><code>String uploadFileName</code> 负责接收原始文件名。</li>
<li><code>String uploadContentType</code> 负责接收 MIME 类型。</li>
<li>多个文件时还可以再接文件列表。</li>
</ul>
<pre>public class FileUploadAction extends ActionSupport {
@@ -42,8 +42,8 @@
}</pre>
<div class="links">
<a class="btn" href="../../upload/index.jsp">Open upload demo</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
<a class="btn" href="../../uploadPage.action">打开上传实验</a>
<a class="btn" href="../../index.action">返回门户</a>
</div>
</div>
</body>

View File

@@ -1,11 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Action - Struts2 Demo Lab</title>
<title>Hello Action - Struts2 学习实验台</title>
<style>
body {
margin: 0;
@@ -92,32 +92,32 @@
<div class="card">
<div class="eyebrow">Hello Action</div>
<h1><s:property value="message" default="Hello from Struts2!"/></h1>
<p>This page proves the classic request -> action -> result flow. It accepts a name parameter, prepares a response in the action, and renders it through the JSP view.</p>
<p>这个页面用最短路径演示经典的 request -> action -> result 流程。你传入一个 name 参数Action 处理后再交给 JSP 结果页渲染。</p>
<div class="grid">
<section class="panel">
<h3>What just happened</h3>
<h3>刚刚发生了什么</h3>
<p><s:property value="tip" default="The action populated a few view properties before returning SUCCESS."/></p>
</section>
<section class="panel">
<h3>Good next step</h3>
<h3>建议下一步</h3>
<p><s:property value="nextStep" default="Open the login flow next."/></p>
</section>
<section class="panel">
<h3>Try another request</h3>
<p>Use a custom query parameter to change the greeting.</p>
<h3>再试一个请求</h3>
<p>你可以换一个查询参数,观察问候语如何跟着变化。</p>
<code><s:property value="requestSample" default="/hello?name=Platform%20Team"/></code>
</section>
<section class="panel">
<h3>Current input</h3>
<p>Name value: <strong><s:property value="name" default="World"/></strong></p>
<h3>当前输入</h3>
<p>name 参数值:<strong><s:property value="name" default="World"/></strong></p>
</section>
</div>
<div class="actions">
<a class="btn btn-primary" href="hello?name=Platform%20Team">Run again with sample name</a>
<a class="btn btn-soft" href="user/login.jsp">Open login demo</a>
<a class="btn btn-soft" href="index.jsp">Back to portal</a>
<a class="btn btn-primary" href="hello.action?name=Platform%20Team">用示例参数再运行一次</a>
<a class="btn btn-soft" href="loginPage.action">进入登录实验</a>
<a class="btn btn-soft" href="index.action">返回门户</a>
</div>
</div>
</body>

View File

@@ -1,514 +1,4 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Struts2 Demo Lab</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: 860px;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
}
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: white; }
.btn-soft { background: var(--soft); color: var(--brand); }
.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: 340px 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;
}
.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;
}
.tracker {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 14px;
}
.tracker-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.5);
}
.tracker-item input {
margin-top: 3px;
}
.tracker-item strong {
display: block;
margin-bottom: 4px;
}
@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">Struts2 Demo Lab</div>
<h1>Turn scattered examples into a guided demo portal</h1>
<p>This workspace now acts like a mini learning application instead of a pile of sample pages. You can jump into action basics, login flow, validation, uploads, and JSON demos from one place.</p>
</div>
<div class="hero-actions">
<a class="btn" href="hello?name=Platform%20Team">Run hello action</a>
<a class="btn-soft" href="user/login.jsp">Open login flow</a>
</div>
</div>
<div class="metrics">
<div class="metric"><span>Live action routes</span><strong>5</strong></div>
<div class="metric"><span>Guide pages</span><strong>4</strong></div>
<div class="metric"><span>Demo forms</span><strong>3</strong></div>
<div class="metric"><span>JSON endpoints</span><strong>2</strong></div>
<div class="metric"><span>Core lab track</span><strong>4</strong></div>
<div class="metric"><span>Completed labs</span><strong id="completedLabs">0</strong></div>
</div>
</section>
<div class="layout">
<aside class="sidebar">
<section class="card">
<div class="eyebrow">Quick search</div>
<h2>Filter demos by keyword</h2>
<div class="search">
<input id="searchInput" type="text" placeholder="Try login, validation, upload, json, or hello" />
</div>
<ul class="helper-list">
<li>Use the action demos to see real Struts requests and results.</li>
<li>Use the guide pages to compare implementation patterns.</li>
<li>Use the JSON routes to explain API-style actions in class.</li>
</ul>
</section>
<section class="card">
<div class="eyebrow">Fast routes</div>
<h2>Best places to start</h2>
<div class="demo-links">
<a class="link-btn" href="user/form.jsp">User form</a>
<a class="link-btn" href="validation/form.jsp">Validation form</a>
<a class="link-btn" href="upload/index.jsp">Upload flow</a>
<a class="link-btn" href="api/users">JSON endpoint</a>
</div>
</section>
<section class="card">
<div class="eyebrow">Lab tracker</div>
<h2>Mark the demos you have finished</h2>
<div class="tracker">
<label class="tracker-item">
<input type="checkbox" data-track="hello">
<span>
<strong>Hello action</strong>
See the smallest request -> action -> result path.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="login">
<span>
<strong>Login flow</strong>
Follow validation, action errors, and success routing.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="validation">
<span>
<strong>Validation flow</strong>
Compare invalid and valid submissions.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="json">
<span>
<strong>JSON and AJAX</strong>
Finish by explaining how Struts can return API-style payloads.
</span>
</label>
</div>
</section>
</aside>
<main class="content">
<section class="card">
<div class="eyebrow">Request lifecycle</div>
<h2>How to explain Struts2 with one mental model</h2>
<div class="pipeline">
<div class="step">
<strong>1. Request enters</strong>
<p>The browser sends a URL and optional form fields or query params.</p>
</div>
<div class="step">
<strong>2. Action mapping</strong>
<p><code>struts.xml</code> resolves the action name and target class.</p>
</div>
<div class="step">
<strong>3. Parameter binding</strong>
<p>Struts populates action properties before <code>execute()</code> runs.</p>
</div>
<div class="step">
<strong>4. Result routing</strong>
<p>The returned result name decides which JSP or JSON renderer will respond.</p>
</div>
</div>
</section>
<section class="card">
<div class="eyebrow">Catalog</div>
<h2>Interactive demos and guides</h2>
<div class="grid" id="demoGrid">
<article class="demo-card" data-keywords="hello action request parameter basics">
<div class="tag-list">
<span class="tag">Action</span>
<span class="tag">Beginner</span>
</div>
<h3>Hello action</h3>
<p>Run a real Struts action, inject a request parameter, and inspect the rendered result page.</p>
<div class="demo-links">
<a class="link-btn" href="hello?name=Platform%20Team">Run demo</a>
<a class="link-btn" href="demo/hello/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="login session form validation credentials">
<div class="tag-list">
<span class="tag">Form</span>
<span class="tag">Core flow</span>
</div>
<h3>Login flow</h3>
<p>Use demo credentials, see field validation, and land on a richer post-login dashboard.</p>
<div class="demo-links">
<a class="link-btn" href="user/login.jsp">Open login</a>
</div>
</article>
<article class="demo-card" data-keywords="user form submit profile">
<div class="tag-list">
<span class="tag">Action</span>
<span class="tag">Form submit</span>
</div>
<h3>User intake form</h3>
<p>Submit a small profile payload and see action-backed validation with a success summary.</p>
<div class="demo-links">
<a class="link-btn" href="user/form.jsp">Open form</a>
</div>
</article>
<article class="demo-card" data-keywords="validation field errors age email bio">
<div class="tag-list">
<span class="tag">Validation</span>
<span class="tag">Teaching</span>
</div>
<h3>Validation demo</h3>
<p>Test length, email, age, and text limits while keeping the page readable for explanation.</p>
<div class="demo-links">
<a class="link-btn" href="validation/form.jsp">Open validation</a>
</div>
</article>
<article class="demo-card" data-keywords="upload multipart metadata file">
<div class="tag-list">
<span class="tag">Upload</span>
<span class="tag">Safe demo</span>
</div>
<h3>Upload metadata demo</h3>
<p>Capture file metadata through Struts form binding without writing anything to disk.</p>
<div class="demo-links">
<a class="link-btn" href="upload/index.jsp">Open upload</a>
<a class="link-btn" href="demo/upload/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="ajax json api rest users">
<div class="tag-list">
<span class="tag">JSON</span>
<span class="tag">API style</span>
</div>
<h3>AJAX and REST payloads</h3>
<p>Show how Struts2 can return JSON from actions for AJAX examples and lightweight REST-style demos.</p>
<div class="demo-links">
<a class="link-btn" href="ajax">Action JSON</a>
<a class="link-btn" href="api/users">REST JSON</a>
<a class="link-btn" href="demo/ajax/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="model driven user bean object binding">
<div class="tag-list">
<span class="tag">Guide</span>
<span class="tag">Patterns</span>
</div>
<h3>Model binding guide</h3>
<p>Compare property-driven and model-driven input binding to explain how Struts builds action state.</p>
<div class="demo-links">
<a class="link-btn" href="demo/model/index.jsp">Open guide</a>
</div>
</article>
</div>
<div class="empty" id="emptyState" style="display: none; margin-top: 16px;">No demo matched the current search.</div>
</section>
<section class="card">
<div class="eyebrow">Learning pipeline</div>
<h2>Suggested order for the full demo</h2>
<div class="pipeline">
<div class="step">
<strong>1. Start with hello</strong>
<p>See the smallest action and result mapping first.</p>
</div>
<div class="step">
<strong>2. Move to forms</strong>
<p>Login and user intake show parameter binding in action.</p>
</div>
<div class="step">
<strong>3. Validate inputs</strong>
<p>Explain field errors, business rules, and success pages.</p>
</div>
<div class="step">
<strong>4. Expand to JSON</strong>
<p>Close the session with AJAX and REST-style payload demos.</p>
</div>
</div>
</section>
</main>
</div>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const cards = Array.from(document.querySelectorAll('.demo-card'));
const emptyState = document.getElementById('emptyState');
const trackerItems = Array.from(document.querySelectorAll('[data-track]'));
const trackerKey = 'struts_demo_tracker';
function getTrackerState() {
return JSON.parse(localStorage.getItem(trackerKey) || '{}');
}
function renderTracker() {
const state = getTrackerState();
let completed = 0;
trackerItems.forEach((item) => {
item.checked = Boolean(state[item.dataset.track]);
if (item.checked) {
completed += 1;
}
});
document.getElementById('completedLabs').textContent = completed;
}
trackerItems.forEach((item) => {
item.addEventListener('change', () => {
const state = getTrackerState();
state[item.dataset.track] = item.checked;
localStorage.setItem(trackerKey, JSON.stringify(state));
renderTracker();
});
});
searchInput.addEventListener('input', () => {
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';
});
renderTracker();
</script>
</body>
</html>
<%
response.sendRedirect(request.getContextPath() + "/index.action");
%>

View File

@@ -1,102 +0,0 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Form - Struts2 Demo Lab</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: 820px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
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; }
.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">User action</div>
<h1>Capture a user profile through Struts binding</h1>
<p>This form now demonstrates a more realistic user intake flow with field errors and a structured success screen.</p>
<s:form action="submitUser" method="post" namespace="/">
<div class="field">
<label for="username">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">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">Phone</label>
<s:textfield id="phone" name="phone" placeholder="13800000000"/>
<div class="error"><s:fielderror fieldName="phone"/></div>
</div>
<button type="submit">Submit profile</button>
</s:form>
<div class="links">
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="login.jsp">Open login demo</a>
</div>
</div>
</body>
</html>

View File

@@ -1,176 +0,0 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Demo - Struts2 Demo Lab</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(1040px, 100%);
display: grid;
grid-template-columns: 1fr 420px;
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);
}
.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; }
.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 {
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;
}
.link-btn {
display: inline-flex;
padding: 10px 14px;
border-radius: 999px;
background: #edf5ff;
color: #1464c7;
text-decoration: none;
font-weight: 700;
}
@media (max-width: 860px) {
.shell { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell">
<section class="panel">
<div class="eyebrow">Demo login</div>
<h1>Login flow with validation and guided next steps</h1>
<p>This page turns the original login example into a better teaching demo. It keeps validation close to the form, shows a reusable credentials note, and links to follow-up demos after success.</p>
<div class="note">
<strong>Demo credentials</strong>
<p style="margin-top: 8px;">Username: <code>admin</code><br/>Password: <code>123456</code></p>
</div>
<div class="stats">
<div class="stat">
<strong>What this demonstrates</strong>
<span>Action execution, field validation, action errors, and result routing.</span>
</div>
<div class="stat">
<strong>Suggested next route</strong>
<span>After login, continue with the user form or validation demo.</span>
</div>
</div>
<div class="links">
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="../hello?name=Team">Run hello action</a>
</div>
</section>
<section class="panel">
<div class="eyebrow">Sign in</div>
<h2>Enter the demo account</h2>
<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">Username</label>
<s:textfield id="username" name="username" placeholder="admin"/>
<div class="error"><s:fielderror fieldName="username"/></div>
</div>
<div class="field">
<label for="password">Password</label>
<s:password id="password" name="password" placeholder="123456" showPassword="true"/>
<div class="error"><s:fielderror fieldName="password"/></div>
</div>
<button type="submit">Continue to the dashboard</button>
</s:form>
</section>
</div>
</body>
</html>

View File

@@ -1,109 +0,0 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Validation Demo - Struts2 Demo Lab</title>
<style>
body {
margin: 0;
min-height: 100vh;
padding: 24px;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
}
.shell {
max-width: 900px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
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: #db2777;
font-weight: 800;
}
h1 { margin: 10px 0 12px; }
p { margin: 0; color: #6e5565; line-height: 1.85; }
.field { margin-top: 16px; }
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4a223b; }
input, textarea {
width: 100%;
padding: 13px 14px;
border-radius: 14px;
border: 1px solid #ecd6e0;
font: inherit;
}
textarea { resize: vertical; min-height: 120px; }
button {
width: 100%;
margin-top: 18px;
padding: 14px;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #db2777, #f472b6);
color: white;
font-size: 16px;
font-weight: 800;
cursor: pointer;
}
.error { color: #c0265d; font-size: 13px; margin-top: 6px; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.link-btn {
display: inline-flex;
padding: 10px 14px;
border-radius: 999px;
background: #fff0f7;
color: #db2777;
text-decoration: none;
font-weight: 700;
}
</style>
</head>
<body>
<div class="shell">
<div class="eyebrow">Validation demo</div>
<h1>Test field validation with realistic profile inputs</h1>
<p>This form shows how Struts2 can keep validation close to the action while still rendering a friendlier teaching page.</p>
<s:form action="validate" method="post" namespace="/">
<div class="field">
<label for="username">Username (3-20 chars)</label>
<s:textfield id="username" name="username" placeholder="release-manager"/>
<div class="error"><s:fielderror fieldName="username"/></div>
</div>
<div class="field">
<label for="email">Email</label>
<s:textfield id="email" name="email" placeholder="release@example.com"/>
<div class="error"><s:fielderror fieldName="email"/></div>
</div>
<div class="field">
<label for="age">Age (18-60)</label>
<s:textfield id="age" name="age" placeholder="30"/>
<div class="error"><s:fielderror fieldName="age"/></div>
</div>
<div class="field">
<label for="bio">Bio</label>
<s:textarea id="bio" name="bio" placeholder="Describe the role, strengths, or current platform focus."/>
<div class="error"><s:fielderror fieldName="bio"/></div>
</div>
<button type="submit">Validate this profile</button>
</s:form>
<div class="links">
<a class="link-btn" href="../index.jsp">Back to portal</a>
<a class="link-btn" href="../demo/ajax/index.jsp">Read the JSON guide</a>
</div>
</div>
</body>
</html>