feat: expand struts demo lab

This commit is contained in:
Codex
2026-03-18 18:12:20 +08:00
parent 1f60832445
commit fba7b0497f
25 changed files with 1847 additions and 1447 deletions

View File

@@ -0,0 +1,48 @@
package com.demo.action;
import com.demo.model.User;
import com.opensymphony.xwork2.ActionSupport;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
public class AjaxAction extends ActionSupport {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private boolean success;
private String message;
private String serverTime;
private List<User> users;
@Override
public String execute() {
success = true;
message = "Mock AJAX response generated from the Struts2 action.";
serverTime = LocalDateTime.now().format(TIME_FORMATTER);
users = Arrays.asList(
new User(1L, "ops-admin", "ops-admin@example.com", "13800000001"),
new User(2L, "platform-owner", "platform-owner@example.com", "13800000002"),
new User(3L, "release-manager", "release-manager@example.com", "13800000003")
);
return SUCCESS;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
public String getServerTime() {
return serverTime;
}
public List<User> getUsers() {
return users;
}
}

View File

@@ -0,0 +1,93 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import java.io.File;
import java.util.List;
public class FileUploadAction extends ActionSupport {
private File upload;
private String uploadFileName;
private String uploadContentType;
private List<File> uploads;
private List<String> uploadsFileName;
private List<String> uploadsContentType;
private int fileCount;
private String summary;
@Override
public String execute() {
fileCount = 0;
if (upload != null) {
fileCount++;
}
if (uploads != null) {
fileCount += uploads.size();
}
if (fileCount == 0) {
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.";
return SUCCESS;
}
public File getUpload() {
return upload;
}
public void setUpload(File upload) {
this.upload = upload;
}
public String getUploadFileName() {
return uploadFileName;
}
public void setUploadFileName(String uploadFileName) {
this.uploadFileName = uploadFileName;
}
public String getUploadContentType() {
return uploadContentType;
}
public void setUploadContentType(String uploadContentType) {
this.uploadContentType = uploadContentType;
}
public List<File> getUploads() {
return uploads;
}
public void setUploads(List<File> uploads) {
this.uploads = uploads;
}
public List<String> getUploadsFileName() {
return uploadsFileName;
}
public void setUploadsFileName(List<String> uploadsFileName) {
this.uploadsFileName = uploadsFileName;
}
public List<String> getUploadsContentType() {
return uploadsContentType;
}
public void setUploadsContentType(List<String> uploadsContentType) {
this.uploadsContentType = uploadsContentType;
}
public int getFileCount() {
return fileCount;
}
public String getSummary() {
return summary;
}
}

View File

@@ -2,33 +2,50 @@ package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
/**
* Hello World 示例 Action
* 展示 Struts2 最基础的 Action 写法
*
* ActionSupport 提供了:
* - validate() 方法用于表单验证
* - getText() 方法用于国际化
* - addActionError()/addFieldError() 用于错误消息
*/
public class HelloAction extends ActionSupport {
private String name;
private String message;
private String tip;
private String nextStep;
private String requestSample;
@Override
public String execute() throws Exception {
// 业务逻辑
public String execute() {
if (name == null || name.trim().isEmpty()) {
name = "World";
} else {
name = name.trim();
}
message = "Hello, " + name + "! 欢迎学习 Struts2!";
message = "Hello, " + name + "! Welcome to the Struts2 demo lab.";
tip = "Struts2 injected the request parameter into the action property before execute() ran.";
nextStep = "Try the login flow or open the demo catalog to compare different action patterns.";
requestSample = "/hello?name=Platform%20Team";
return SUCCESS;
}
// Getter/Setter (Struts2 需要这些来获取/设置参数)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMessage() {
return message;
}
public String getTip() {
return tip;
}
public String getNextStep() {
return nextStep;
}
public String getRequestSample() {
return requestSample;
}
}

View File

@@ -2,46 +2,92 @@ package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
/**
* 用户登录 Action
* 展示:
* - 表单参数接收
* - 数据验证
* - Session 使用
* - 返回不同结果
*/
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class LoginAction extends ActionSupport {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String username;
private String password;
private String displayName;
private String role;
private String loginTime;
private String recommendation;
@Override
public String execute() throws Exception {
// 模拟登录验证
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功,设置 session
return SUCCESS;
} else if (username != null && !username.isEmpty()) {
// 登录失败
addActionError("用户名或密码错误!");
public String execute() {
username = normalize(username);
password = normalize(password);
if (!hasSubmitted()) {
return INPUT;
}
if ("admin".equals(username) && "123456".equals(password)) {
displayName = "System Demo Admin";
role = "Administrator";
loginTime = LocalDateTime.now().format(TIME_FORMATTER);
recommendation = "Continue with the user form, validation sample, or upload flow to explore the rest of the demo.";
return SUCCESS;
}
addActionError("Invalid demo credentials. Use admin / 123456.");
return INPUT;
}
// 表单验证
@Override
public void validate() {
if (username == null || username.trim().length() < 3) {
addFieldError("username", "用户名至少3个字符");
if (!hasSubmitted()) {
return;
}
if (username == null || username.length() < 3) {
addFieldError("username", "Username must be at least 3 characters.");
}
if (password == null || password.length() < 6) {
addFieldError("password", "密码至少6个字符");
addFieldError("password", "Password must be at least 6 characters.");
}
}
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
private boolean hasSubmitted() {
return (username != null && !username.trim().isEmpty())
|| (password != null && !password.trim().isEmpty());
}
private String normalize(String value) {
return value == null ? null : value.trim();
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDisplayName() {
return displayName;
}
public String getRole() {
return role;
}
public String getLoginTime() {
return loginTime;
}
public String getRecommendation() {
return recommendation;
}
}

View File

@@ -0,0 +1,84 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class UserAction extends ActionSupport {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String username;
private String email;
private String phone;
private String submittedAt;
private String profileStage;
public String submit() {
username = normalize(username);
email = normalize(email);
phone = normalize(phone);
if (!isValid()) {
return INPUT;
}
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.";
return SUCCESS;
}
private boolean isValid() {
boolean valid = true;
if (username == null || username.length() < 3) {
addFieldError("username", "Username must be at least 3 characters.");
valid = false;
}
if (email == null || !email.contains("@")) {
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.");
valid = false;
}
return valid;
}
private String normalize(String value) {
return value == null ? null : value.trim();
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getSubmittedAt() {
return submittedAt;
}
public String getProfileStage() {
return profileStage;
}
}

View File

@@ -0,0 +1,88 @@
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ValidationAction extends ActionSupport {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String username;
private String email;
private Integer age;
private String bio;
private String scoreBand;
private String submittedAt;
@Override
public String execute() {
username = normalize(username);
email = normalize(email);
bio = normalize(bio);
scoreBand = age >= 30 ? "Mid-career operator profile" : "Early-career operator profile";
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
return SUCCESS;
}
@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.");
}
if (email == null || !email.contains("@") || email.indexOf('@') == email.length() - 1) {
addFieldError("email", "Enter a valid email address.");
}
if (age == null || age < 18 || age > 60) {
addFieldError("age", "Age must be between 18 and 60.");
}
if (bio != null && bio.trim().length() > 240) {
addFieldError("bio", "Bio must stay under 240 characters.");
}
}
private String normalize(String value) {
return value == null ? null : value.trim();
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getBio() {
return bio;
}
public void setBio(String bio) {
this.bio = bio;
}
public String getScoreBand() {
return scoreBand;
}
public String getSubmittedAt() {
return submittedAt;
}
}

View File

@@ -0,0 +1,38 @@
package com.demo.action.rest;
import com.demo.model.User;
import com.opensymphony.xwork2.ActionSupport;
import java.util.Arrays;
import java.util.List;
public class UserRestAction extends ActionSupport {
private boolean success;
private String message;
private List<User> users;
@Override
public String execute() {
success = true;
message = "REST-style demo payload";
users = Arrays.asList(
new User(101L, "alpha", "alpha@example.com", "13800001001"),
new User(102L, "beta", "beta@example.com", "13800001002"),
new User(103L, "gamma", "gamma@example.com", "13800001003")
);
return SUCCESS;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
public List<User> getUsers() {
return users;
}
}

View File

@@ -1,30 +1,54 @@
package com.demo.model;
/**
* 用户模型
* 展示 Struts2 的 ModelDriven 和 Bean 封装
*/
public class User {
private Long id;
private String username;
private String email;
private String phone;
public User() {}
public User() {
}
public User(Long id, String username, String email) {
this(id, username, email, null);
}
public User(Long id, String username, String email, String phone) {
this.id = id;
this.username = username;
this.email = email;
this.phone = phone;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}

View File

@@ -4,62 +4,47 @@
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<!-- 开发模式 - 修改配置后自动reload -->
<constant name="struts.devMode" value="true"/>
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<!-- 编码设置 -->
<constant name="struts.i18n.encoding" value="UTF-8"/>
<!-- ==================== 示例包定义 ==================== -->
<package name="default" namespace="/" extends="struts-default">
<!-- 首页 -->
<action name="index">
<result>/index.jsp</result>
</action>
<!-- Hello World 示例 -->
<action name="hello" class="com.demo.action.HelloAction" method="execute">
<result>/hello.jsp</result>
</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>
</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>
</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>
</action>
<!-- AJAX 示例 -->
<action name="ajax" class="com.demo.action.AjaxAction" method="execute">
<result type="json"/>
</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>
</action>
</package>
<!-- REST 风格示例 -->
<package name="rest" namespace="/api" extends="struts-default">
<action name="users" class="com.demo.action.rest.UserRestAction">
<action name="users" class="com.demo.action.rest.UserRestAction" method="execute">
<result type="json"/>
</action>
</package>
</struts>
</struts>

View File

@@ -5,14 +5,13 @@
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Struts2 学习 Demo</display-name>
<display-name>Struts2 Demo Lab</display-name>
<!-- Struts2 核心过滤器 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
@@ -21,8 +20,7 @@
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- 错误页面配置 -->
<error-page>
<error-code>404</error-code>
<location>/error/404.jsp</location>
@@ -31,4 +29,4 @@
<error-code>500</error-code>
<location>/error/500.jsp</location>
</error-page>
</web-app>
</web-app>

View File

@@ -1,101 +1,51 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AJAX + JSON - Struts2 学习</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AJAX and JSON Guide</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.breadcrumb { background: white; padding: 15px 25px; border-radius: 10px; margin-bottom: 20px; }
.content { background: white; border-radius: 20px; padding: 40px; }
h1 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 15px; }
h2 { color: #764ba2; margin-top: 30px; }
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 10px; overflow-x: auto; }
.keyword { color: #569cd6; }
.string { color: #ce9178; }
.comment { color: #6a9955; }
.btn { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; }
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #122c63, #1464c7); }
.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); }
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #1464c7; font-weight: 800; }
h1, h2 { margin: 10px 0 12px; }
p, li { color: #53667d; line-height: 1.9; }
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #e8f2ff; color: #1464c7; }
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="/">🏠 首页</a> / AJAX + JSON
</div>
<div class="content">
<h1>⚡ AJAX + JSON</h1>
<h2>1. JSON 结果类型</h2>
<div class="section">
<p>使用 struts2-json-plugin 实现 AJAX 返回 JSON</p>
<pre><span class="comment">// struts.xml 配置</span>
<span class="keyword">&lt;action</span> name=<span class="string">"getUser"</span> class=<span class="string">"com.demo.UserAction"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;result</span> type=<span class="string">"json"</span><span class="keyword">&gt;</span>
<span class="comment">&lt;!-- 可选: 只包含指定属性 --&gt;</span>
<span class="keyword">&lt;param</span> name=<span class="string">"includeProperties"</span><span class="keyword">&gt;</span>user\..*,success<span class="keyword">&lt;/param&gt;</span>
<span class="keyword">&lt;/result&gt;</span>
<span class="keyword">&lt;/action&gt;</span></pre>
</div>
<h2>2. Action 示例</h2>
<div class="section">
<pre><span class="keyword">public class</span> UserAction <span class="keyword">extends</span> ActionSupport {
<span class="keyword">private</span> User user;
<span class="keyword">private</span> boolean success;
<span class="keyword">private</span> String message;
<span class="comment">// getter 是必须的JSON 插件只序列化有 getter 的属性</span>
<span class="keyword">public</span> User getUser() { <span class="keyword">return</span> user; }
<span class="keyword">public</span> boolean isSuccess() { <span class="keyword">return</span> success; }
<span class="keyword">public</span> String getMessage() { <span class="keyword">return</span> message; }
<span class="keyword">public</span> String getData() {
user = <span class="keyword">new</span> User(<span class="string">"张三"</span>, <span class="string">"zhangsan@email.com"</span>);
success = <span class="keyword">true</span>;
message = <span class="string">"获取成功"</span>;
<span class="keyword">return</span> SUCCESS;
<div class="shell">
<div class="eyebrow">Guide</div>
<h1>Returning JSON from Struts2 actions</h1>
<p>The demo now includes both an action-level JSON response and a REST-style JSON route. That makes it easier to compare MVC pages with lightweight API payloads.</p>
<h2>What the action returns</h2>
<ul>
<li>A <code>success</code> flag.</li>
<li>A short message describing the payload.</li>
<li>Sample user records that can be consumed by AJAX.</li>
</ul>
<pre>public class AjaxAction extends ActionSupport {
private boolean success;
private String message;
private List&lt;User&gt; users;
public String execute() {
success = true;
message = "Mock AJAX response";
return SUCCESS;
}
}</pre>
</div>
<h2>3. 前端 AJAX 调用</h2>
<div class="section">
<pre><span class="comment">// 原生 fetch</span>
fetch(<span class="string">'/getUser'</span>)
.then(r =&gt; r.json())
.then(data =&gt; {
console.log(data.user);
console.log(data.success);
});
<span class="comment">// jQuery</span>
$.getJSON(<span class="string">'/getUser'</span>, <span class="keyword">function</span>(data) {
$(<span class="string">'#username'</span>).text(data.user.username);
});</pre>
</div>
<h2>4. 完整示例:实时搜索</h2>
<div class="section">
<pre><span class="comment">// HTML</span>
<span class="keyword">&lt;input</span> type=<span class="string">"text"</span> id=<span class="string">"keyword"</span> onkeyup=<span class="string">"search(this.value)"</span>/&gt;
<span class="keyword">&lt;div</span> id=<span class="string">"results"</span><span class="keyword">&gt;&lt;/div&gt;</span>
<span class="comment">// JS</span>
<span class="keyword">function</span> search(keyword) {
$.getJSON(<span class="string">'/search'</span>, {keyword: keyword}, <span class="keyword">function</span>(data) {
$(<span class="string">'#results'</span>).empty();
data.results.forEach(<span class="keyword">function</span>(item) {
$(<span class="string">'#results'</span>).append(<span class="string">'&lt;div&gt;'</span> + item.name + <span class="string">'&lt;/div&gt;'</span>);
});
});
}</pre>
</div>
<a href="/demo/interceptor" class="btn">下一节:拦截器 →</a>
<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="../../index.jsp">Back to portal</a>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,168 +1,53 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World - Struts2 学习</title>
<title>Hello Action Guide</title>
<style>
body { font-family: 'Segoe UI', 'PingFang SC', sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.breadcrumb { background: white; padding: 15px 25px; border-radius: 10px; margin-bottom: 20px; }
.breadcrumb a { color: #667eea; text-decoration: none; }
.content { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); }
h1 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 15px; }
h2 { color: #764ba2; margin-top: 30px; }
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; }
code { background: #1e1e1e; color: #d4d4d4; padding: 2px 8px; border-radius: 4px; font-family: Consolas, monospace; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 10px; overflow-x: auto; font-size: 0.9em; }
.keyword { color: #569cd6; }
.string { color: #ce9178; }
.comment { color: #6a9955; }
.btn { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; margin-right: 10px; }
.btn:hover { background: #764ba2; }
.note { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0; border-radius: 0 10px 10px 0; }
body { margin: 0; padding: 24px; font-family: "Aptos", "Segoe UI", sans-serif; background: linear-gradient(135deg, #1464c7, #3a8dff); }
.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); }
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #1464c7; font-weight: 800; }
h1, h2 { margin: 10px 0 12px; }
p, li { color: #53667d; line-height: 1.9; }
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #e8f2ff; color: #1464c7; }
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="/">🏠 首页</a> / Hello World
</div>
<div class="content">
<h1>👋 Hello World - 第一个 Struts2 应用</h1>
<div class="note">
<strong>📝 本节要点:</strong> 创建最简单的 Struts2 应用,掌握 Action 编写和 struts.xml 配置
</div>
<h2>1. 项目结构</h2>
<pre>
webapp/
├── WEB-INF/
│ ├── web.xml
│ └── lib/
│ └── struts2-core.jar
├── struts.xml ← Action 配置
├── index.jsp
└── hello.jsp ← 结果页面</pre>
<h2>2. web.xml 配置</h2>
<div class="section">
<pre><span class="comment">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="keyword">&lt;web-app</span> xmlns=<span class="string">"http://xmlns.jcp.org/xml/ns/javaee"</span><span class="keyword">&gt;</span>
<span class="comment">&lt;!-- Struts2 核心过滤器 --&gt;</span>
<span class="keyword">&lt;filter&gt;</span>
<span class="keyword">&lt;filter-name&gt;</span>struts2<span class="keyword">&lt;/filter-name&gt;</span>
<span class="keyword">&lt;filter-class&gt;</span>
org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
<span class="keyword">&lt;/filter-class&gt;</span>
<span class="keyword">&lt;/filter&gt;</span>
<span class="keyword">&lt;filter-mapping&gt;</span>
<span class="keyword">&lt;filter-name&gt;</span>struts2<span class="keyword">&lt;/filter-name&gt;</span>
<span class="keyword">&lt;url-pattern&gt;</span>/*<span class="keyword">&lt;/url-pattern&gt;</span>
<span class="keyword">&lt;/filter-mapping&gt;</span>
<span class="keyword">&lt;/web-app&gt;</span></pre>
</div>
<h2>3. struts.xml 配置</h2>
<div class="section">
<pre><span class="comment">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="keyword">&lt;!DOCTYPE struts PUBLIC</span>
<span class="string">"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"</span>
<span class="string">"http://struts.apache.org/dtds/struts-2.5.dtd"</span><span class="keyword">&gt;</span>
<div class="shell">
<div class="eyebrow">Guide</div>
<h1>Hello action walkthrough</h1>
<p>The hello demo is still the best first stop for explaining how Struts2 maps a request to an action and then routes to a JSP result.</p>
<span class="keyword">&lt;struts&gt;</span>
<span class="comment">&lt;!-- 开发模式 --&gt;</span>
<span class="keyword">&lt;constant</span> name=<span class="string">"struts.devMode"</span> value=<span class="string">"true"</span><span class="keyword">/&gt;</span>
<span class="comment">&lt;!-- 定义包 --&gt;</span>
<span class="keyword">&lt;package</span> name=<span class="string">"default"</span> namespace=<span class="string">"/"</span> extends=<span class="string">"struts-default"</span><span class="keyword">&gt;</span>
<span class="comment">&lt;!-- Action 配置 --&gt;</span>
<span class="keyword">&lt;action</span> name=<span class="string">"hello"</span> class=<span class="string">"com.demo.HelloAction"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;result&gt;</span>/hello.jsp<span class="keyword">&lt;/result&gt;</span>
<span class="keyword">&lt;/action&gt;</span>
<span class="keyword">&lt;/package&gt;</span>
<span class="keyword">&lt;/struts&gt;</span></pre>
</div>
<h2>4. HelloAction.java</h2>
<div class="section">
<pre><span class="keyword">package</span> com.demo;
<h2>Flow</h2>
<ul>
<li>The browser requests <code>/hello</code> or <code>/hello?name=Team</code>.</li>
<li>Struts populates the <code>name</code> property on <code>HelloAction</code>.</li>
<li><code>execute()</code> builds a view message and returns <code>SUCCESS</code>.</li>
<li>The framework resolves the success result to <code>/hello.jsp</code>.</li>
</ul>
<span class="keyword">import</span> com.opensymphony.xwork2.ActionSupport;
<h2>Minimal action shape</h2>
<pre>public class HelloAction extends ActionSupport {
private String name;
private String message;
<span class="comment">/**
* 第一个 Struts2 Action
* 继承 ActionSupport 可以获得验证、国际化的能力
*/</span>
<span class="keyword">public class</span> HelloAction <span class="keyword">extends</span> ActionSupport {
<span class="comment">// 接收参数</span>
<span class="keyword">private</span> String name;
<span class="comment">// 返回给页面的数据</span>
<span class="keyword">private</span> String message;
<span class="comment">/**
* 执行方法,默认调用
*/</span>
<span class="annotation">@Override</span>
<span class="keyword">public</span> String execute() <span class="keyword">throws</span> Exception {
<span class="keyword">if</span> (name == <span class="keyword">null</span> || name.trim().isEmpty()) {
name = <span class="string">"Struts2 学习者"</span>;
public String execute() {
if (name == null || name.trim().isEmpty()) {
name = "World";
}
message = <span class="string">"你好, "</span> + name + <span class="string">"! 欢迎学习 Struts2!"</span>;
<span class="keyword">return</span> SUCCESS; <span class="comment">// 返回 "success"</span>
message = "Hello, " + name + "!";
return SUCCESS;
}
<span class="comment">// Getter/Setter 必须提供Struts2 通过反射注入参数</span>
<span class="keyword">public</span> String getName() { <span class="keyword">return</span> name; }
<span class="keyword">public void</span> setName(String name) { <span class="keyword">this</span>.name = name; }
<span class="keyword">public</span> String getMessage() { <span class="keyword">return</span> message; }
<span class="keyword">public void</span> setMessage(String message) { <span class="keyword">this</span>.message = message; }
}</pre>
</div>
<h2>5. hello.jsp 结果页面</h2>
<div class="section">
<pre><span class="comment">&lt;%@ page contentType="text/html;charset=UTF-8" language="java" %&gt;</span>
<span class="comment">&lt;%@ taglib prefix="s" uri="/struts-tags" %&gt;</span>
<span class="keyword">&lt;!DOCTYPE html&gt;</span>
<span class="keyword">&lt;html&gt;</span>
<span class="keyword">&lt;body&gt;</span>
<span class="comment">&lt;!-- 使用 Struts2 标签获取 Action 返回的值 --&gt;</span>
<span class="keyword">&lt;h1&gt;&lt;s:property</span> value=<span class="string">"message"</span>/&gt;&lt;/h1&gt;
<span class="keyword">&lt;/body&gt;</span>
<span class="keyword">&lt;/html&gt;</span></pre>
</div>
<h2>6. 访问方式</h2>
<div class="section">
<p>启动应用后,访问:</p>
<code>http://localhost:8080/hello</code> - 默认执行<br>
<code>http://localhost:8080/hello?name=张三</code> - 传递参数
</div>
<h2>执行流程图</h2>
<div class="section">
<pre style="text-align: center;">
浏览器请求 → Struts2 Filter → ActionProxy → HelloAction.execute()
→ 返回 "success" → 配置的 result → hello.jsp → 浏览器</pre>
</div>
<div style="margin-top: 30px;">
<a href="/demo/validation" class="btn">下一节:表单验证 →</a>
<a href="/" class="btn" style="background: #764ba2;">← 返回首页</a>
</div>
<div class="links">
<a class="btn" href="../../hello?name=Platform%20Team">Run hello action</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,173 +1,54 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据封装 - Struts2 学习</title>
<title>Model Binding Guide</title>
<style>
body { font-family: 'Segoe UI', 'PingFang SC', sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.breadcrumb { background: white; padding: 15px 25px; border-radius: 10px; margin-bottom: 20px; }
.breadcrumb a { color: #667eea; text-decoration: none; }
.content { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); }
h1 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 15px; }
h2 { color: #764ba2; margin-top: 30px; }
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 10px; overflow-x: auto; font-size: 0.9em; }
.keyword { color: #569cd6; }
.string { color: #ce9178; }
.comment { color: #6a9955; }
.btn { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; margin-right: 10px; }
.note { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0; border-radius: 0 10px 10px 0; }
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); }
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #7c3aed; font-weight: 800; }
h1, h2 { margin: 10px 0 12px; }
p, li { color: #5e5570; line-height: 1.9; }
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #f0e9ff; color: #7c3aed; }
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="/">🏠 首页</a> / 数据封装
</div>
<div class="content">
<h1>📦 数据封装 - 属性驱动 vs 模型驱动</h1>
<div class="note">
<strong>📝 本节要点:</strong> 掌握 Struts2 两种数据封装方式,将请求参数封装到 Java 对象
</div>
<h2>1. 两种封装方式对比</h2>
<div class="section">
<table style="width:100%; border-collapse: collapse;">
<tr style="background:#667eea; color:white;">
<th style="padding:10px;">方式</th>
<th style="padding:10px;">实现</th>
<th style="padding:10px;">适用场景</th>
</tr>
<tr style="background:#f8f9fa;">
<td style="padding:10px;"><strong>属性驱动</strong></td>
<td style="padding:10px;">Action 中定义属性 + getter/setter</td>
<td style="padding:10px;">简单场景,属性较少</td>
</tr>
<tr>
<td style="padding:10px;"><strong>模型驱动</strong></td>
<td style="padding:10px;">实现 ModelDriven 接口</td>
<td style="padding:10px;">复杂对象,表单字段多</td>
</tr>
</table>
</div>
<h2>2. 方式一:属性驱动 (Property Driven)</h2>
<h3>2.1 基本属性封装</h3>
<div class="section">
<pre><span class="comment">/**
* 直接在 Action 中定义属性
* 表单: &lt;input name="username"&gt; 会自动调用 setUsername()
*/</span>
<span class="keyword">public class</span> UserAction <span class="keyword">extends</span> ActionSupport {
<span class="comment">// Struts2 自动调用 setter 注入参数</span>
<span class="keyword">private</span> String username;
<span class="keyword">private</span> String email;
<span class="keyword">private</span> Integer age;
<span class="annotation">@Override</span>
<span class="keyword">public</span> String execute() {
System.out.println(username + <span class="string">" - "</span> + email + <span class="string">" - "</span> + age);
<span class="keyword">return</span> SUCCESS;
}
<span class="comment">// 必须提供 setter 和 getter</span>
<span class="keyword">public</span> String getUsername() { <span class="keyword">return</span> username; }
<span class="keyword">public void</span> setUsername(String username) { <span class="keyword">this</span>.username = username; }
<span class="keyword">public</span> String getEmail() { <span class="keyword">return</span> email; }
<span class="keyword">public void</span> setEmail(String email) { <span class="keyword">this</span>.email = email; }
<span class="keyword">public</span> Integer getAge() { <span class="keyword">return</span> age; }
<span class="keyword">public void</span> setAge(Integer age) { <span class="keyword">this</span>.age = age; }
}</pre>
</div>
<h3>2.2 复杂属性 (嵌套对象)</h3>
<div class="section">
<p>表单中的 <code>user.username</code> 会自动调用 setUser(用户对象).setUsername()</p>
<pre><span class="comment">// Action</span>
<span class="keyword">private</span> User user; <span class="comment">// 包含 username, email 属性的对象</span>
<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>
<span class="comment">// 表单</span>
<span class="keyword">&lt;input</span> name=<span class="string">"user.username"</span>/&gt;
<span class="keyword">&lt;input</span> name=<span class="string">"user.email"</span>/&gt;</pre>
</div>
<h2>3. 方式二:模型驱动 (ModelDriven)</h2>
<div class="note">
<strong>💡 推荐:</strong> 对于复杂表单使用模型驱动更清晰Action 和 Model 分离
</div>
<div class="section">
<pre><span class="keyword">import</span> com.opensymphony.xwork2.ModelDriven;
<h2>Property-driven</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>
</ul>
<span class="comment">/**
* 实现 ModelDriven 接口
* 需要:
* 1. 实现 getModel() 方法
* 2. 泛型指定模型类型
*/</span>
<span class="keyword">public class</span> UserAction <span class="keyword">extends</span> ActionSupport <span class="keyword">implements</span> ModelDriven&lt;User&gt; {
<span class="comment">// 模型对象</span>
<span class="keyword">private</span> User user = <span class="keyword">new</span> User();
<span class="comment">/**
* 返回模型对象
* Struts2 会把参数注入到返回的对象中
*/</span>
<span class="annotation">@Override</span>
<span class="keyword">public</span> User getModel() {
<span class="keyword">return</span> user;
}
<span class="annotation">@Override</span>
<span class="keyword">public</span> String execute() {
<span class="comment">// 直接使用 user 对象</span>
System.out.println(user.getUsername());
<span class="keyword">return</span> SUCCESS;
<h2>Model-driven</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>
</ul>
<pre>public class UserAction extends ActionSupport implements ModelDriven&lt;User&gt; {
private final User user = new User();
@Override
public User getModel() {
return user;
}
}</pre>
</div>
<h2>4. List/Map 属性封装</h2>
<div class="section">
<pre><span class="comment">// Action - 接收 List</span>
<span class="keyword">private</span> List&lt;User&gt; users;
<span class="keyword">public void</span> setUsers(List&lt;User&gt; users) { <span class="keyword">this</span>.users = users; }
<span class="keyword">public</span> List&lt;User&gt; getUsers() { <span class="keyword">return</span> users; }
<span class="comment">// 页面表单 - users[0].username, users[1].username</span>
<span class="keyword">&lt;input</span> name=<span class="string">"users[0].username"</span> value=<span class="string">"张三"</span>/&gt;
<span class="keyword">&lt;input</span> name=<span class="string">"users[1].username"</span> value=<span class="string">"李四"</span>/&gt;
<span class="comment">// Action - 接收 Map</span>
<span class="keyword">private</span> Map&lt;String, User&gt; userMap;
<span class="keyword">&lt;input</span> name=<span class="string">"userMap['one'].username"</span>/&gt;</pre>
</div>
<h2>5. 封装流程图</h2>
<div class="section">
<pre style="text-align: center; font-size: 1.1em;">
请求参数 → Action setter 方法 → 属性赋值
实现 ModelDriven → getModel() → 模型对象赋值
值栈 (Value Stack)</pre>
</div>
<div style="margin-top: 30px;">
<a href="/demo/upload" class="btn">下一节:文件上传 →</a>
<a href="/" class="btn" style="background: #764ba2;">← 返回首页</a>
</div>
<div class="links">
<a class="btn" href="../../user/form.jsp">Open user form</a>
<a class="btn" href="../../index.jsp">Back to portal</a>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,102 +1,50 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传 - Struts2 学习</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Guide</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.breadcrumb { background: white; padding: 15px 25px; border-radius: 10px; margin-bottom: 20px; }
.content { background: white; border-radius: 20px; padding: 40px; }
h1 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 15px; }
h2 { color: #764ba2; margin-top: 30px; }
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 10px; overflow-x: auto; }
.keyword { color: #569cd6; }
.string { color: #ce9178; }
.comment { color: #6a9955; }
.btn { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; }
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); }
.eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; color: #0284c7; font-weight: 800; }
h1, h2 { margin: 10px 0 12px; }
p, li { color: #53667d; line-height: 1.9; }
pre { background: #101827; color: #d9e7ff; padding: 18px; border-radius: 18px; overflow-x: auto; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.btn { display: inline-flex; padding: 10px 14px; border-radius: 999px; text-decoration: none; font-weight: 700; background: #edf7ff; color: #0284c7; }
</style>
</head>
<body>
<div class="container">
<div class="breadcrumb">
<a href="/">🏠 首页</a> / 文件上传
</div>
<div class="content">
<h1>📁 文件上传</h1>
<h2>1. 单文件上传</h2>
<div class="section">
<pre><span class="comment">// Action</span>
<span class="keyword">public class</span> UploadAction <span class="keyword">extends</span> ActionSupport {
<span class="comment">// 文件对象</span>
<span class="keyword">private</span> File upload;
<span class="comment">// 文件名 (格式: 属性名FileName)</span>
<span class="keyword">private</span> String uploadFileName;
<span class="comment">// 文件类型 (格式: 属性名ContentType)</span>
<span class="keyword">private</span> String uploadContentType;
<span class="annotation">@Override</span>
<span class="keyword">public</span> String execute() <span class="keyword">throws</span> Exception {
<span class="comment">// 保存文件</span>
String path = ServletActionContext.getServletContext()
.getRealPath(<span class="string">"/upload"</span>);
<span class="keyword">new</span> File(path, uploadFileName).createNewFile();
<span class="comment">// 复制文件</span>
FileUtils.copyFile(upload, <span class="keyword">new</span> File(path, uploadFileName));
<span class="keyword">return</span> SUCCESS;
}
<span class="comment">// getter/setter</span>
<span class="keyword">public</span> File getUpload() { <span class="keyword">return</span> upload; }
<span class="keyword">public void</span> setUpload(File upload) { <span class="keyword">this</span>.upload = upload; }
<span class="keyword">public</span> String getUploadFileName() { <span class="keyword">return</span> uploadFileName; }
<span class="keyword">public void</span> setUploadFileName(String uploadFileName) { <span class="keyword">this</span>.uploadFileName = uploadFileName; }
<span class="keyword">public</span> String getUploadContentType() { <span class="keyword">return</span> uploadContentType; }
<span class="keyword">public void</span> setUploadContentType(String uploadContentType) { <span class="keyword">this</span>.uploadContentType = uploadContentType; }
}</pre>
</div>
<h2>2. JSP 表单</h2>
<div class="section">
<pre><span class="comment">&lt;%@ taglib prefix="s" uri="/struts-tags" %&gt;</span>
<span class="keyword">&lt;s:form</span> action=<span class="string">"upload"</span> method=<span class="string">"post"</span> enctype=<span class="string">"multipart/form-data"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;s:file</span> name=<span class="string">"upload"</span> label=<span class="string">"选择文件"</span>/&gt;
<span class="keyword">&lt;s:submit</span> value=<span class="string">"上传"</span>/&gt;
<span class="keyword">&lt;/s:form&gt;</span></pre>
</div>
<h2>3. 多文件上传</h2>
<div class="section">
<pre><span class="comment">// 使用 List 接收多文件</span>
<span class="keyword">private</span> List&lt;File&gt; uploads;
<span class="keyword">private</span> List&lt;String&gt; uploadsFileName;
<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>
<span class="comment">// 表单</span>
<span class="keyword">&lt;s:file</span> name=<span class="string">"uploads"</span> multiple=<span class="string">"multiple"</span>/&gt;</pre>
</div>
<h2>4. 限制文件大小和类型</h2>
<div class="section">
<pre><span class="comment">// struts.xml 配置</span>
<span class="keyword">&lt;action</span> name=<span class="string">"upload"</span> class=<span class="string">"com.demo.UploadAction"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;interceptor-ref</span> name=<span class="string">"fileUpload"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;param</span> name=<span class="string">"maximumSize"</span><span class="keyword">&gt;</span>10485760<span class="keyword">&lt;/param&gt;</span><span class="comment">&lt;!-- 10MB --&gt;</span>
<span class="keyword">&lt;param</span> name=<span class="string">"allowedTypes"</span><span class="keyword">&gt;</span>image/png,image/jpeg<span class="keyword">&lt;/param&gt;</span>
<span class="keyword">&lt;/interceptor-ref&gt;</span>
<span class="keyword">&lt;result&gt;</span>/upload/success.jsp<span class="keyword">&lt;/result&gt;</span>
<span class="keyword">&lt;/action&gt;</span></pre>
</div>
<a href="/demo/ajax" class="btn">下一节AJAX →</a>
<h2>Core binding fields</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>
</ul>
<pre>public class FileUploadAction extends ActionSupport {
private File upload;
private String uploadFileName;
private String uploadContentType;
public String execute() {
// demo keeps metadata only
return SUCCESS;
}
}</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>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,21 +1,47 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>404 - 页面未找到</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Struts2 Demo Lab</title>
<style>
body { font-family: sans-serif; background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.card { background: white; padding: 50px; border-radius: 15px; text-align: center; }
h1 { color: #e53935; font-size: 4em; margin-bottom: 20px; }
p { color: #666; }
a { color: #667eea; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #122c63 0%, #1464c7 52%, #f68b1f 100%);
}
.card {
width: min(680px, calc(100vw - 32px));
background: rgba(255,255,255,0.95);
border-radius: 28px;
padding: 28px;
text-align: center;
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
}
h1 { margin: 0 0 12px; font-size: 52px; color: #1464c7; }
p { margin: 0; color: #56697f; line-height: 1.85; }
a {
display: inline-flex;
margin-top: 20px;
padding: 12px 18px;
border-radius: 999px;
background: #e8f2ff;
color: #1464c7;
text-decoration: none;
font-weight: 700;
}
</style>
</head>
<body>
<div class="card">
<h1>404</h1>
<p>页面未找到</p>
<p><a href="../index.jsp">返回首页</a></p>
<p>The requested demo page was not found. Use the portal to jump back into the working Struts2 examples.</p>
<a href="../index.jsp">Back to portal</a>
</div>
</body>
</html>
</html>

47
web/error/500.jsp Normal file
View File

@@ -0,0 +1,47 @@
<%@ 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>500 - Struts2 Demo Lab</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #7f1d1d 0%, #dc2626 55%, #fee2e2 100%);
}
.card {
width: min(700px, calc(100vw - 32px));
background: rgba(255,255,255,0.95);
border-radius: 28px;
padding: 28px;
text-align: center;
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
}
h1 { margin: 0 0 12px; font-size: 52px; color: #b91c1c; }
p { margin: 0; color: #6d4b4b; line-height: 1.85; }
a {
display: inline-flex;
margin-top: 20px;
padding: 12px 18px;
border-radius: 999px;
background: #fee2e2;
color: #b91c1c;
text-decoration: none;
font-weight: 700;
}
</style>
</head>
<body>
<div class="card">
<h1>500</h1>
<p>The demo hit an internal error. Return to the portal and try one of the working examples again.</p>
<a href="../index.jsp">Back to portal</a>
</div>
</body>
</html>

View File

@@ -1,47 +1,124 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Hello - Struts2 Demo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Action - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #1464c7 0%, #3f8df7 52%, #f68b1f 100%);
color: #122033;
}
.card {
background: white;
padding: 50px;
border-radius: 20px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
color: #667eea;
font-size: 3em;
margin-bottom: 20px;
}
p { color: #666; font-size: 1.2em; }
.btn {
display: inline-block;
margin-top: 30px;
padding: 14px 40px;
background: #667eea;
color: white;
text-decoration: none;
width: min(860px, calc(100vw - 32px));
background: rgba(255,255,255,0.94);
border: 1px solid rgba(255,255,255,0.45);
border-radius: 30px;
padding: 30px;
box-shadow: 0 24px 60px rgba(0,0,0,0.24);
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #1464c7;
font-weight: 800;
}
h1 {
margin: 12px 0;
font-size: 42px;
line-height: 1.12;
}
p {
margin: 0;
color: #53667d;
line-height: 1.9;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 20px;
}
.panel {
padding: 18px;
border-radius: 20px;
background: #f6f9fd;
border: 1px solid #dbe5f0;
}
.panel h3 {
margin: 0 0 8px;
font-size: 18px;
}
.panel code {
display: inline-block;
margin-top: 8px;
padding: 4px 8px;
border-radius: 10px;
background: #e8f2ff;
color: #1464c7;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 24px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 18px;
border-radius: 999px;
text-decoration: none;
font-weight: 700;
}
.btn-primary { background: #1464c7; color: #fff; }
.btn-soft { background: #e8f2ff; color: #1464c7; }
@media (max-width: 720px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="card">
<h1><s:property value="message" default="Hello World!"/></h1>
<p>Struts2 Action 执行成功!</p>
<a href="index.jsp" class="btn">返回首页</a>
<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>
<div class="grid">
<section class="panel">
<h3>What just happened</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>
<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>
<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>
</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>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,552 +1,403 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Struts2 全栈学习平台</title>
<title>Struts2 Demo Lab</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #667eea;
--secondary: #764ba2;
--success: #11998e;
--warning: #f093fb;
--danger: #f5576c;
--dark: #2d3748;
--light: #f7fafc;
--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 {
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
margin: 0;
min-height: 100vh;
color: var(--dark);
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);
}
.header {
background: rgba(255,255,255,0.95);
padding: 20px 0;
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
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);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
.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;
align-items: center;
}
.logo {
font-size: 1.8em;
font-weight: bold;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-links a {
margin-left: 25px;
text-decoration: none;
color: var(--dark);
font-weight: 500;
transition: color 0.3s;
}
.nav-links a:hover { color: var(--primary); }
.container {
max-width: 1400px;
margin: 40px auto;
padding: 0 20px;
}
.hero {
text-align: center;
color: white;
margin-bottom: 50px;
gap: 20px;
align-items: flex-start;
}
.hero h1 {
font-size: 3em;
margin-bottom: 20px;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
margin: 10px 0 12px;
font-size: 42px;
line-height: 1.1;
}
.hero p {
font-size: 1.3em;
opacity: 0.9;
margin: 0;
color: var(--muted);
line-height: 1.9;
max-width: 860px;
}
.progress-section {
background: white;
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.progress-section h2 {
margin-bottom: 20px;
color: var(--primary);
}
.progress-bar {
.hero-actions {
display: flex;
gap: 15px;
gap: 10px;
flex-wrap: wrap;
}
.progress-item {
flex: 1;
min-width: 150px;
padding: 15px;
background: var(--light);
border-radius: 10px;
text-align: center;
border-left: 4px solid var(--primary);
transition: transform 0.3s;
}
.progress-item:hover { transform: translateY(-5px); }
.progress-item.completed { border-color: var(--success); }
.progress-item.current { border-color: var(--warning); background: #fff3e0; }
.progress-item h3 { font-size: 1.1em; margin-bottom: 5px; }
.progress-item span { font-size: 0.85em; color: #666; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
}
.card {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid var(--light);
}
.card-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
.btn, .btn-soft {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
margin-right: 15px;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
}
.card-icon.green { background: #e8f5e9; }
.card-icon.blue { background: #e3f2fd; }
.card-icon.pink { background: #fce4ec; }
.card-icon.yellow { background: #fff3e0; }
.card-icon.purple { background: #f3e5f5; }
.card h3 {
font-size: 1.3em;
color: var(--dark);
.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;
}
.card p {
color: #666;
line-height: 1.7;
margin-bottom: 15px;
.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-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
margin-right: 5px;
margin-bottom: 5px;
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: var(--soft);
color: var(--brand);
font-size: 12px;
font-weight: 700;
}
.tag.basic { background: #e8f5e9; color: #2e7d32; }
.tag.advance { background: #e3f2fd; color: #1565c0; }
.tag.expert { background: #fce4ec; color: #c2185b; }
.btn {
display: inline-block;
padding: 10px 25px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
text-decoration: none;
border-radius: 25px;
font-weight: 500;
margin-top: 10px;
transition: transform 0.3s, box-shadow 0.3s;
.demo-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
.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;
}
.feature-list {
list-style: none;
margin: 15px 0;
.pipeline {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.feature-list li {
padding: 8px 0;
padding-left: 25px;
position: relative;
.step {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.52);
}
.feature-list li::before {
content: "✓";
position: absolute;
left: 0;
color: var(--success);
font-weight: bold;
}
.code-preview {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 10px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.85em;
overflow-x: auto;
margin: 15px 0;
}
.code-preview .keyword { color: #569cd6; }
.code-preview .string { color: #ce9178; }
.code-preview .comment { color: #6a9955; }
.code-preview .annotation { color: #c586c0; }
.footer {
.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;
padding: 40px 20px;
color: white;
opacity: 0.8;
}
@media (max-width: 768px) {
.hero h1 { font-size: 2em; }
.grid { grid-template-columns: 1fr; }
.progress-bar { flex-direction: column; }
.nav-links { display: none; }
@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="header">
<div class="header-content">
<div class="logo">🚀 Struts2 学习平台</div>
<div class="nav-links">
<a href="#course">课程体系</a>
<a href="#examples">示例代码</a>
<a href="#projects">实战项目</a>
<a href="#resources">资源下载</a>
</div>
</div>
</div>
<div class="container">
<div class="hero">
<h1>从零掌握 Struts2 框架</h1>
<p>完整的 Struts2 学习路径 | 视频教程 | 代码示例 | 实战项目</p>
</div>
<!-- 学习进度 -->
<div class="progress-section">
<h2>📈 学习路径</h2>
<div class="progress-bar">
<div class="progress-item completed">
<h3>第一阶段</h3>
<span>环境搭建 + 基础</span>
</div>
<div class="progress-item current">
<h3>第二阶段</h3>
<span>核心组件</span>
</div>
<div class="progress-item">
<h3>第三阶段</h3>
<span>数据处理</span>
</div>
<div class="progress-item">
<h3>第四阶段</h3>
<span>企业应用</span>
</div>
</div>
</div>
<!-- 课程卡片 -->
<div class="grid" id="course">
<!-- 1. Hello World -->
<div class="card">
<div class="card-header">
<div class="card-icon green">👋</div>
<h3>1. Hello World</h3>
</div>
<p>Struts2 入门最简单的示例,掌握基本的 Action 配置和参数传递。</p>
<ul class="feature-list">
<li>Action 编写</li>
<li>struts.xml 配置</li>
<li>结果视图映射</li>
</ul>
<span class="tag basic">入门</span>
<a href="demo/hello/index.jsp" class="btn">开始学习</a>
</div>
<!-- 2. 表单与验证 -->
<div class="card">
<div class="card-header">
<div class="card-icon blue">✅</div>
<h3>2. 表单与验证</h3>
</div>
<p>深入理解 Struts2 的两种验证方式:编程式验证和声明式验证。</p>
<ul class="feature-list">
<li>validate() 方法</li>
<li>XML 验证文件</li>
<li>字段级验证</li>
</ul>
<span class="tag basic">入门</span>
<a href="demo/validation/index.jsp" class="btn">开始学习</a>
</div>
<!-- 3. 数据封装 -->
<div class="card">
<div class="card-header">
<div class="card-icon pink">📦</div>
<h3>3. 数据封装</h3>
</div>
<p>掌握 Struts2 的数据封装机制,包括属性驱动和模型驱动。</p>
<ul class="feature-list">
<li>属性驱动</li>
<li>模型驱动 (ModelDriven)</li>
<li>复杂对象封装</li>
</ul>
<span class="tag basic">进阶</span>
<a href="demo/model/index.jsp" class="btn">开始学习</a>
</div>
<!-- 4. 文件上传 -->
<div class="card">
<div class="card-header">
<div class="card-icon yellow">📁</div>
<h3>4. 文件上传</h3>
</div>
<p>Struts2 文件上传功能,支持单文件和多文件上传。</p>
<ul class="feature-list">
<li>File 对象接收</li>
<li>文件类型验证</li>
<li>多文件上传</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/upload/index.jsp" class="btn">开始学习</a>
</div>
<!-- 5. 类型转换 -->
<div class="card">
<div class="card-header">
<div class="card-icon purple">🔄</div>
<h3>5. 类型转换器</h3>
</div>
<p>自定义类型转换器,处理特殊数据类型和复杂对象。</p>
<ul class="feature-list">
<li>TypeConverter</li>
<li>StrutsTypeConverter</li>
<li>局部/全局转换器</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/converter/index.jsp" class="btn">开始学习</a>
</div>
<!-- 6. AJAX -->
<div class="card">
<div class="card-header">
<div class="card-icon green">⚡</div>
<h3>6. AJAX + JSON</h3>
</div>
<p>使用 Struts2 JSON 插件实现异步数据交互。</p>
<ul class="feature-list">
<li>JSON 结果类型</li>
<li>AJAX 表单提交</li>
<li>局部页面刷新</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/ajax/index.jsp" class="btn">开始学习</a>
</div>
<!-- 7. 拦截器 -->
<div class="card">
<div class="card-header">
<div class="card-icon blue">🛡️</div>
<h3>7. 拦截器</h3>
</div>
<p>理解拦截器机制,这是 Struts2 框架的核心灵魂。</p>
<ul class="feature-list">
<li>自定义拦截器</li>
<li>拦截器栈</li>
<li>方法过滤拦截器</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/interceptor/index.jsp" class="btn">开始学习</a>
</div>
<!-- 8. OGNL -->
<div class="card">
<div class="card-header">
<div class="card-icon pink">📊</div>
<h3>8. OGNL 表达式</h3>
</div>
<p>掌握 Struts2 的表达式语言,操作对象图和集合。</p>
<ul class="feature-list">
<li>基础语法</li>
<li>集合操作</li>
<li>上下文对象</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/ognl/index.jsp" class="btn">开始学习</a>
</div>
<!-- 9. Session/Request -->
<div class="card">
<div class="card-header">
<div class="card-icon yellow">🔐</div>
<h3>9. 作用域对象</h3>
</div>
<p>掌握 ActionContext 和值栈的操作。</p>
<ul class="feature-list">
<li>Session 管理</li>
<li>Request/Response</li>
<li>值栈操作</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/scope/index.jsp" class="btn">开始学习</a>
</div>
<!-- 10. REST -->
<div class="card">
<div class="card-header">
<div class="card-icon purple">🌐</div>
<h3>10. RESTful</h3>
</div>
<p>使用 Struts2 实现 RESTful 风格的 API 设计。</p>
<ul class="feature-list">
<li>REST 插件</li>
<li>URL 参数映射</li>
<li>内容协商</li>
</ul>
<span class="tag expert">专家</span>
<a href="demo/rest/index.jsp" class="btn">开始学习</a>
</div>
<!-- 11. 国际化 -->
<div class="card">
<div class="card-header">
<div class="card-icon green">🌍</div>
<h3>11. 国际化 (i18n)</h3>
</div>
<p>实现多语言支持的应用程序。</p>
<ul class="feature-list">
<li>资源文件配置</li>
<li>getText() 使用</li>
<li>动态语言切换</li>
</ul>
<span class="tag advance">进阶</span>
<a href="demo/i18n/index.jsp" class="btn">开始学习</a>
</div>
<!-- 12. 源码解析 -->
<div class="card">
<div class="card-header">
<div class="card-icon blue">🔍</div>
<h3>12. 源码解析</h3>
</div>
<p>深入理解 Struts2 框架的内部实现原理。</p>
<ul class="feature-list">
<li>请求处理流程</li>
<li>拦截器链源码</li>
<li>OGNL 原理</li>
</ul>
<span class="tag expert">专家</span>
<a href="demo/source/index.jsp" class="btn">开始学习</a>
</div>
</div>
<!-- 配置参考 -->
<div class="progress-section" style="margin-top: 50px;" id="examples">
<h2>⚙️ 配置文件示例</h2>
<div class="grid">
<div class="shell">
<section class="hero">
<div class="hero-head">
<div>
<h3 style="margin: 15px 0;">struts.xml 基础配置</h3>
<div class="code-preview">
<span class="comment">&lt;?xml version="1.0"?&gt;</span>
<span class="keyword">&lt;struts&gt;</span>
<span class="comment">&lt;!-- 开发模式 --&gt;</span>
<span class="keyword">&lt;constant</span> name=<span class="string">"struts.devMode"</span> value=<span class="string">"true"</span><span class="keyword">/&gt;</span>
<span class="comment">&lt;!-- 包定义 --&gt;</span>
<span class="keyword">&lt;package</span> name=<span class="string">"default"</span> namespace=<span class="string">"/"</span> extends=<span class="string">"struts-default"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;action</span> name=<span class="string">"hello"</span> class=<span class="string">"com.demo.HelloAction"</span><span class="keyword">&gt;</span>
<span class="keyword">&lt;result&gt;/hello.jsp&lt;/result&gt;</span>
<span class="keyword">&lt;/action&gt;</span>
<span class="keyword">&lt;/package&gt;</span>
<span class="keyword">&lt;/struts&gt;</span>
</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>
<h3 style="margin: 15px 0;">Action 基础写法</h3>
<div class="code-preview">
<span class="keyword">public class</span> HelloAction <span class="keyword">extends</span> ActionSupport {
<span class="keyword">private</span> String name;
<span class="annotation">@Override</span>
<span class="keyword">public</span> String execute() {
<span class="keyword">return</span> SUCCESS;
}
<span class="comment">// getter/setter</span>
<span class="keyword">public</span> String getName() { <span class="keyword">return</span> name; }
<span class="keyword">public void</span> setName(String name) { <span class="keyword">this</span>.name = name; }
}
</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>
<!-- 实战项目 -->
<div class="progress-section" style="margin-top: 50px;" id="projects">
<h2>💼 实战项目</h2>
<div class="grid">
<div class="card">
<h3>📝 博客系统</h3>
<p>完整的博客系统,包含文章管理、评论、分类等功能。</p>
<span class="tag basic">入门</span>
<a href="#" class="btn">查看详情</a>
</div>
<div class="card">
<h3>📋 待办事项</h3>
<p>Todo 列表应用,练习 CRUD 操作和 AJAX。</p>
<span class="tag basic">入门</span>
<a href="#" class="btn">查看详情</a>
</div>
<div class="card">
<h3>🛒 商城购物车</h3>
<p>购物车功能,练习 Session 和订单管理。</p>
<span class="tag advance">进阶</span>
<a href="#" class="btn">查看详情</a>
</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>
</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>
</aside>
<main class="content">
<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>
<div class="footer">
<p>Struts2 版本: 2.5.30 | Tomcat 9 | JDK 17</p>
<p>本学习平台包含完整的代码示例和配置文件</p>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const cards = Array.from(document.querySelectorAll('.demo-card'));
const emptyState = document.getElementById('emptyState');
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';
});
</script>
</body>
</html>
</html>

View File

@@ -1,87 +1,117 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传 - Struts2 Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Demo - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
body {
margin: 0;
min-height: 100vh;
padding: 20px;
padding: 24px;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 55%, #dff4ff 100%);
}
.container { max-width: 600px; margin: 0 auto; }
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
.shell {
max-width: 860px;
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);
}
h2 { color: #4facfe; margin-bottom: 20px; }
.info {
background: #fff3e0;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #ff9800;
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #0284c7;
font-weight: 800;
}
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: #555; font-weight: 500; }
input[type="file"] {
width: 100%;
padding: 12px;
border: 2px dashed #4facfe;
border-radius: 8px;
background: #f0f9ff;
h1 { margin: 10px 0 12px; }
p { margin: 0; color: #54687c; line-height: 1.85; }
.note {
margin-top: 16px;
padding: 16px;
border-radius: 18px;
background: #eef8ff;
border: 1px solid #d6e9f7;
}
.field { margin-top: 16px; }
label { display: block; margin-bottom: 8px; font-weight: 700; color: #173652; }
input[type="file"] {
width: 100%;
padding: 16px;
border-radius: 16px;
border: 1px dashed #5ab8f0;
background: #f8fcff;
}
button {
width: 100%;
margin-top: 18px;
padding: 14px;
background: #4facfe;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #0284c7, #38bdf8);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 800;
cursor: pointer;
}
button:hover { background: #00c6fb; }
.action-error {
margin-top: 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: #edf7ff;
color: #0284c7;
text-decoration: none;
font-weight: 700;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h2>📁 文件上传</h2>
<div class="info">
<strong>Struts2 文件上传原理:</strong>
<ul style="margin:10px 0 0 20px;">
<li>使用 <code>File</code> 类型接收文件</li>
<li>使用 <code>String fileName</code> 获取原始文件名</li>
<li>使用 <code>String contentType</code> 获取文件类型</li>
<li>底层使用 commons-fileupload</li>
</ul>
<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="note">
<strong>What the action collects</strong>
<p style="margin-top: 8px;">Primary filename, content type, and total selected file count.</p>
</div>
<s:if test="hasActionErrors()">
<div class="action-error"><s:actionerror/></div>
</s:if>
<s:form action="upload" method="post" enctype="multipart/form-data" namespace="/">
<div class="field">
<label>Primary file</label>
<s:file name="upload"/>
</div>
<s:form action="upload" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>选择文件</label>
<s:file name="upload"/>
</div>
<div class="form-group">
<label>选择文件 (多文件)</label>
<s:file name="uploads" multiple="multiple"/>
</div>
<button type="submit">上传文件</button>
</s:form>
<p style="text-align:center;margin-top:20px;">
<a href="../index.jsp">← 返回首页</a>
</p>
<div class="field">
<label>Extra files</label>
<s:file name="uploads" multiple="multiple"/>
</div>
<button type="submit">Submit upload metadata</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>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,39 +1,92 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>操作成功</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Summary - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
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, #0ea5e9 0%, #38bdf8 55%, #dff4ff 100%);
}
.card {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
width: min(760px, 100%);
background: rgba(255,255,255,0.95);
border-radius: 28px;
padding: 28px;
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
}
h1 { color: #11998e; }
.btn {
display: inline-block;
margin-top: 20px;
padding: 12px 30px;
background: #11998e;
color: white;
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #0284c7;
font-weight: 800;
}
h1 { margin: 10px 0 12px; }
p { margin: 0; color: #53677b; line-height: 1.85; }
.stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.stat {
padding: 16px;
border-radius: 18px;
background: #f5fbff;
border: 1px solid #d8ebf8;
}
.stat span { display: block; font-size: 12px; color: #62809d; margin-bottom: 6px; }
.stat strong { font-size: 20px; }
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.link-btn {
display: inline-flex;
padding: 10px 14px;
border-radius: 999px;
background: #edf7ff;
color: #0284c7;
text-decoration: none;
border-radius: 25px;
font-weight: 700;
}
@media (max-width: 720px) {
.stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="card">
<h1>✅ 操作成功!</h1>
<a href="../../index.jsp" class="btn">返回首页</a>
<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="stats">
<div class="stat">
<span>Selected files</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>
</div>
<div class="stat">
<span>Content type</span>
<strong><s:property value="uploadContentType" default="Unknown"/></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>
</div>
</div>
</body>
</html>
</html>

View File

@@ -1,58 +1,102 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>用户表单 - Struts2 Demo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Form - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
body {
margin: 0;
min-height: 100vh;
padding: 20px;
padding: 24px;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #f97316 0%, #fb923c 50%, #ffedd5 100%);
}
.container { max-width: 600px; margin: 0 auto; }
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
.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;
}
h2 { color: #f5576c; margin-bottom: 20px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: #555; }
input { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; }
button {
width: 100%;
margin-top: 18px;
padding: 14px;
background: #f5576c;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #ea580c, #fb923c);
color: white;
border: none;
border-radius: 8px;
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="container">
<div class="card">
<h2>👤 用户信息提交</h2>
<s:form action="submitUser">
<div class="form-group">
<label>用户名</label>
<s:textfield name="username"/>
</div>
<div class="form-group">
<label>邮箱</label>
<s:textfield name="email"/>
</div>
<div class="form-group">
<label>电话</label>
<s:textfield name="phone"/>
</div>
<button type="submit">提交</button>
</s:form>
<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>
</html>

View File

@@ -1,94 +1,176 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录 - Struts2 Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Demo - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
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%);
}
.login-box {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
width: 400px;
.shell {
width: min(1040px, 100%);
display: grid;
grid-template-columns: 1fr 420px;
gap: 18px;
}
h2 { color: #333; margin-bottom: 30px; text-align: center; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: #555; font-weight: 500; }
input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
.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;
}
input:focus { border-color: #667eea; outline: none; }
.error { color: #e53935; font-size: 0.85em; margin-top: 5px; }
.error-field { border-color: #e53935 !important; }
button {
width: 100%;
padding: 14px;
background: #667eea;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #1464c7, #3d8ef6);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 800;
cursor: pointer;
transition: background 0.3s;
}
button:hover { background: #764ba2; }
.tips {
background: #e8f5e9;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 0.9em;
color: #2e7d32;
.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; }
}
a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="login-box">
<h2>🔐 用户登录</h2>
<div class="tips">
<strong>测试账号:</strong> admin / 123456
</div>
<!-- Struts2 表单标签 -->
<s:form action="login" method="post" namespace="/">
<div class="form-group">
<label>用户名</label>
<s:textfield name="username" placeholder="请输入用户名" cssClass="%{hasErrors('username') ? 'error-field' : ''}"/>
<s:fielderror fieldName="username"/>
<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="form-group">
<label>密码</label>
<s:password name="password" placeholder="请输入密码" showPassword="true"/>
<s:fielderror fieldName="password"/>
<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>
<!-- 显示 Action 错误消息 -->
<s:actionerror/>
<button type="submit">登录</button>
</s:form>
<p style="text-align:center;margin-top:20px;">
<a href="index.jsp">← 返回首页</a>
</p>
<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>
</html>

View File

@@ -1,44 +1,111 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>登录成功 - Struts2 Demo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Success Dashboard - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
body {
margin: 0;
min-height: 100vh;
padding: 24px;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #0d8f7c 0%, #31c48d 55%, #d4f7e7 100%);
color: #123028;
}
.shell {
max-width: 1080px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 18px;
}
.card {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
background: rgba(255,255,255,0.94);
border-radius: 28px;
padding: 28px;
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
}
h1 { color: #11998e; font-size: 3em; margin-bottom: 20px; }
p { color: #666; font-size: 1.2em; margin-bottom: 30px; }
.btn {
display: inline-block;
padding: 12px 30px;
background: #11998e;
color: white;
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #0d8f7c;
font-weight: 800;
}
h1, h2 { margin: 10px 0 12px; }
p { margin: 0; color: #446056; line-height: 1.85; }
.stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.stat {
padding: 16px;
border-radius: 18px;
border: 1px solid #d7eee6;
background: #f7fcfa;
}
.stat span { display: block; color: #5f7f75; font-size: 12px; margin-bottom: 6px; }
.stat strong { font-size: 22px; }
.links {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 18px;
}
.link-btn {
display: inline-flex;
padding: 10px 14px;
border-radius: 999px;
background: #e8f8f2;
color: #0d8f7c;
text-decoration: none;
border-radius: 25px;
transition: transform 0.3s;
font-weight: 700;
}
@media (max-width: 760px) {
.stats { grid-template-columns: 1fr; }
}
.btn:hover { transform: scale(1.05); }
</style>
</head>
<body>
<div class="card">
<h1>✅ 登录成功!</h1>
<p>欢迎回来,<s:property value="username"/></p>
<a href="../index.jsp" class="btn">返回首页</a>
<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>
</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="stats">
<div class="stat">
<span>Username</span>
<strong><s:property value="username" default="demo-user"/></strong>
</div>
<div class="stat">
<span>Role or stage</span>
<strong><s:property value="role != null ? role : profileStage"/></strong>
</div>
<div class="stat">
<span>Timestamp</span>
<strong><s:property value="loginTime != null ? loginTime : submittedAt"/></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>
</div>
</body>
</html>
</html>

View File

@@ -1,103 +1,109 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表单验证 - Struts2 Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Validation Demo - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
body {
margin: 0;
min-height: 100vh;
padding: 20px;
padding: 24px;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
}
.container { max-width: 600px; margin: 0 auto; }
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
.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);
}
h2 { color: #f5576c; margin-bottom: 20px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: #555; font-weight: 500; }
input, textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #db2777;
font-weight: 800;
}
input:focus, textarea:focus { border-color: #f5576c; outline: none; }
.error { color: #e53935; font-size: 0.85em; margin-top: 5px; }
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;
background: #f5576c;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, #db2777, #f472b6);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 800;
cursor: pointer;
}
button:hover { background: #e91e63; }
.info {
background: #e3f2fd;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
color: #e91e63;
.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="container">
<div class="card">
<h2>📝 表单验证示例</h2>
<div class="info">
<strong>验证方式:</strong>
<ul style="margin:10px 0 0 20px;">
<li>在 Action 中重写 <code>validate()</code> 方法</li>
<li>使用 XML 验证文件 (ActionName-validation.xml)</li>
<li>使用注解验证 (@Required, @IntRange 等)</li>
</ul>
<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>
<s:form action="validate" method="post">
<div class="form-group">
<label>用户名 (3-20字符)</label>
<s:textfield name="username"/>
<s:fielderror fieldName="username"/>
</div>
<div class="form-group">
<label>邮箱</label>
<s:textfield name="email"/>
<s:fielderror fieldName="email"/>
</div>
<div class="form-group">
<label>年龄 (18-60)</label>
<s:textfield name="age"/>
<s:fielderror fieldName="age"/>
</div>
<div class="form-group">
<label>个人简介</label>
<s:textarea name="bio" rows="4"/>
</div>
<s:actionerror/>
<button type="submit">提交验证</button>
</s:form>
<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>
</html>

View File

@@ -1,54 +1,86 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>验证成功 - Struts2 Demo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Validation Summary - Struts2 Demo Lab</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
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, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
}
.card {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
min-width: 400px;
width: min(820px, 100%);
background: rgba(255,255,255,0.95);
border-radius: 28px;
padding: 28px;
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
}
h1 { color: #11998e; margin-bottom: 20px; }
.data {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
text-align: left;
margin: 20px 0;
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #db2777;
font-weight: 800;
}
.data p { margin: 10px 0; }
.btn {
display: inline-block;
padding: 12px 30px;
background: #11998e;
color: white;
h1 { margin: 10px 0 12px; }
p { margin: 0; color: #6a5060; line-height: 1.85; }
.stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.stat {
padding: 16px;
border-radius: 18px;
background: #fff5fa;
border: 1px solid #f1d8e5;
}
.stat span { display: block; font-size: 12px; color: #8a697b; margin-bottom: 6px; }
.stat strong { font-size: 18px; }
.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;
border-radius: 25px;
font-weight: 700;
}
@media (max-width: 720px) {
.stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="card">
<h1>✅ 验证通过!</h1>
<div class="data">
<p><strong>用户名:</strong> <s:property value="username"/></p>
<p><strong>邮箱:</strong> <s:property value="email"/></p>
<p><strong>年龄:</strong> <s:property value="age"/></p>
<p><strong>简介:</strong> <s:property value="bio"/></p>
<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="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>
<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>
</div>
<a href="form.jsp" class="btn">继续测试</a>
</div>
</body>
</html>
</html>