Compare commits
9 Commits
1f60832445
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb18c4d99a | ||
|
|
589f33dc92 | ||
|
|
da1586f6fe | ||
|
|
dfbe28a047 | ||
|
|
2d826474bb | ||
|
|
5e318cb7f4 | ||
|
|
4cc4c26f2b | ||
|
|
5466fb1ffd | ||
|
|
fba7b0497f |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
*.log
|
||||
91
pom.xml
Normal file
91
pom.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.demo</groupId>
|
||||
<artifactId>struts2-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>war</packaging>
|
||||
|
||||
<name>Struts2 Demo Lab</name>
|
||||
<description>Classic Struts2 learning project packaged as a runnable Maven WAR.</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<struts2.version>2.5.33</struts2.version>
|
||||
<jetty.version>9.4.54.v20240208</jetty.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-core</artifactId>
|
||||
<version>${struts2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-json-plugin</artifactId>
|
||||
<version>${struts2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/web/WEB-INF/classes</sourceDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.basedir}/web/WEB-INF/classes</directory>
|
||||
<excludes>
|
||||
<exclude>**/*.java</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<release>${maven.compiler.release}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<configuration>
|
||||
<warSourceDirectory>${project.basedir}/web</warSourceDirectory>
|
||||
<packagingExcludes>WEB-INF/classes/**/*.java</packagingExcludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-maven-plugin</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<configuration>
|
||||
<scanIntervalSeconds>0</scanIntervalSeconds>
|
||||
<webAppSourceDirectory>${project.basedir}/web</webAppSourceDirectory>
|
||||
<webApp>
|
||||
<contextPath>/</contextPath>
|
||||
</webApp>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
48
web/WEB-INF/classes/com/demo/action/AjaxAction.java
Normal file
48
web/WEB-INF/classes/com/demo/action/AjaxAction.java
Normal 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;
|
||||
}
|
||||
}
|
||||
44
web/WEB-INF/classes/com/demo/action/DashboardAction.java
Normal file
44
web/WEB-INF/classes/com/demo/action/DashboardAction.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import org.apache.struts2.interceptor.SessionAware;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class DashboardAction extends ActionSupport implements SessionAware {
|
||||
|
||||
private Map<String, Object> session;
|
||||
private String displayName;
|
||||
private String role;
|
||||
private String loginTime;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
displayName = value(LoginAction.SESSION_USER, "ops-admin");
|
||||
role = value(LoginAction.SESSION_ROLE, "admin");
|
||||
loginTime = value(LoginAction.SESSION_LOGIN_TIME, "--");
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
private String value(String key, String fallback) {
|
||||
Object value = session == null ? null : session.get(key);
|
||||
return value == null ? fallback : String.valueOf(value);
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public String getLoginTime() {
|
||||
return loginTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSession(Map<String, Object> session) {
|
||||
this.session = session;
|
||||
}
|
||||
}
|
||||
93
web/WEB-INF/classes/com/demo/action/FileUploadAction.java
Normal file
93
web/WEB-INF/classes/com/demo/action/FileUploadAction.java
Normal 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 = "metadata-only";
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,96 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import org.apache.struts2.interceptor.SessionAware;
|
||||
|
||||
/**
|
||||
* 用户登录 Action
|
||||
* 展示:
|
||||
* - 表单参数接收
|
||||
* - 数据验证
|
||||
* - Session 使用
|
||||
* - 返回不同结果
|
||||
*/
|
||||
public class LoginAction extends ActionSupport {
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
public class LoginAction extends ActionSupport implements SessionAware {
|
||||
|
||||
public static final String SESSION_USER = "demoUser";
|
||||
public static final String SESSION_ROLE = "demoRole";
|
||||
public static final String SESSION_LOGIN_TIME = "demoLoginTime";
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private Map<String, Object> session;
|
||||
private String username;
|
||||
private String password;
|
||||
private String displayName;
|
||||
private String loginTime;
|
||||
|
||||
@Override
|
||||
public String execute() throws Exception {
|
||||
// 模拟登录验证
|
||||
if ("admin".equals(username) && "123456".equals(password)) {
|
||||
// 登录成功,设置 session
|
||||
return SUCCESS;
|
||||
} else if (username != null && !username.isEmpty()) {
|
||||
// 登录失败
|
||||
addActionError("用户名或密码错误!");
|
||||
return INPUT;
|
||||
}
|
||||
public String execute() {
|
||||
username = normalize(username);
|
||||
password = normalize(password);
|
||||
|
||||
if (!hasSubmitted()) {
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
if ("admin".equals(username) && "123456".equals(password)) {
|
||||
displayName = "ops-admin";
|
||||
loginTime = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
session.put(SESSION_USER, displayName);
|
||||
session.put(SESSION_ROLE, "admin");
|
||||
session.put(SESSION_LOGIN_TIME, loginTime);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
addActionError("演示账号或密码不正确,请使用 admin / 123456。/ 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", "用户名至少 3 个字符。/ Username must be at least 3 characters.");
|
||||
}
|
||||
if (password == null || password.length() < 6) {
|
||||
addFieldError("password", "密码至少6个字符");
|
||||
addFieldError("password", "密码至少 6 个字符。/ 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 getLoginTime() {
|
||||
return loginTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSession(Map<String, Object> session) {
|
||||
this.session = session;
|
||||
}
|
||||
}
|
||||
26
web/WEB-INF/classes/com/demo/action/LogoutAction.java
Normal file
26
web/WEB-INF/classes/com/demo/action/LogoutAction.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.demo.action;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import org.apache.struts2.interceptor.SessionAware;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class LogoutAction extends ActionSupport implements SessionAware {
|
||||
|
||||
private Map<String, Object> session;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
if (session != null) {
|
||||
session.remove(LoginAction.SESSION_USER);
|
||||
session.remove(LoginAction.SESSION_ROLE);
|
||||
session.remove(LoginAction.SESSION_LOGIN_TIME);
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSession(Map<String, Object> session) {
|
||||
this.session = session;
|
||||
}
|
||||
}
|
||||
90
web/WEB-INF/classes/com/demo/action/UserAction.java
Normal file
90
web/WEB-INF/classes/com/demo/action/UserAction.java
Normal file
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
private boolean profileReady;
|
||||
|
||||
public String submit() {
|
||||
username = normalize(username);
|
||||
email = normalize(email);
|
||||
phone = normalize(phone);
|
||||
|
||||
if (!isValid()) {
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
submittedAt = LocalDateTime.now().format(TIME_FORMATTER);
|
||||
profileReady = phone != null && phone.length() >= 7;
|
||||
profileStage = profileReady ? "ready" : "review";
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
boolean valid = true;
|
||||
if (username == null || username.length() < 3) {
|
||||
addFieldError("username", "用户名至少 3 个字符。/ 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", "手机号至少 7 位数字。/ 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;
|
||||
}
|
||||
|
||||
public boolean isProfileReady() {
|
||||
return profileReady;
|
||||
}
|
||||
}
|
||||
94
web/WEB-INF/classes/com/demo/action/ValidationAction.java
Normal file
94
web/WEB-INF/classes/com/demo/action/ValidationAction.java
Normal file
@@ -0,0 +1,94 @@
|
||||
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;
|
||||
private boolean seniorTrack;
|
||||
|
||||
@Override
|
||||
public String execute() {
|
||||
username = normalize(username);
|
||||
email = normalize(email);
|
||||
bio = normalize(bio);
|
||||
seniorTrack = age >= 30;
|
||||
scoreBand = seniorTrack ? "mid" : "early";
|
||||
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", "用户名长度需在 3 到 20 个字符之间。/ Username must be between 3 and 20 characters.");
|
||||
}
|
||||
if (email == null || !email.contains("@") || email.indexOf('@') == email.length() - 1) {
|
||||
addFieldError("email", "请输入有效邮箱。/ Enter a valid email address.");
|
||||
}
|
||||
if (age == null || age < 18 || age > 60) {
|
||||
addFieldError("age", "年龄需在 18 到 60 岁之间。/ Age must be between 18 and 60.");
|
||||
}
|
||||
if (bio != null && bio.trim().length() > 240) {
|
||||
addFieldError("bio", "简介不能超过 240 个字符。/ 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;
|
||||
}
|
||||
|
||||
public boolean isSeniorTrack() {
|
||||
return seniorTrack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.demo.action.interceptor;
|
||||
|
||||
import com.demo.action.LoginAction;
|
||||
import com.opensymphony.xwork2.ActionInvocation;
|
||||
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
|
||||
import org.apache.struts2.ServletActionContext;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
public class AuthInterceptor extends AbstractInterceptor {
|
||||
|
||||
@Override
|
||||
public String intercept(ActionInvocation invocation) throws Exception {
|
||||
Map<String, Object> session = invocation.getInvocationContext().getSession();
|
||||
if (session != null && session.get(LoginAction.SESSION_USER) != null) {
|
||||
return invocation.invoke();
|
||||
}
|
||||
|
||||
String namespace = invocation.getProxy().getNamespace();
|
||||
if (namespace != null && namespace.startsWith("/api")) {
|
||||
HttpServletResponse response = ServletActionContext.getResponse();
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"success\":false,\"code\":401,\"message\":\"请先登录后再访问 API / Please log in before calling this API.\"}");
|
||||
response.getWriter().flush();
|
||||
return null;
|
||||
}
|
||||
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
38
web/WEB-INF/classes/com/demo/action/rest/UserRestAction.java
Normal file
38
web/WEB-INF/classes/com/demo/action/rest/UserRestAction.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,62 +4,102 @@
|
||||
"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.devMode" value="false"/>
|
||||
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
|
||||
<constant name="struts.i18n.encoding" value="UTF-8"/>
|
||||
<constant name="struts.action.extension" value="action"/>
|
||||
|
||||
<!-- ==================== 示例包定义 ==================== -->
|
||||
<package name="default" namespace="/" extends="struts-default">
|
||||
<package name="default" namespace="/" extends="json-default">
|
||||
<interceptors>
|
||||
<interceptor name="auth" class="com.demo.action.interceptor.AuthInterceptor"/>
|
||||
<interceptor-stack name="secureStack">
|
||||
<interceptor-ref name="auth"/>
|
||||
<interceptor-ref name="defaultStack"/>
|
||||
</interceptor-stack>
|
||||
</interceptors>
|
||||
|
||||
<global-results>
|
||||
<result name="login" type="redirectAction">loginPage</result>
|
||||
</global-results>
|
||||
|
||||
<!-- 首页 -->
|
||||
<action name="index">
|
||||
<result>/index.jsp</result>
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result>/WEB-INF/views/index.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="loginPage">
|
||||
<result>/WEB-INF/views/user/login.jsp</result>
|
||||
</action>
|
||||
|
||||
<!-- Hello World 示例 -->
|
||||
<action name="hello" class="com.demo.action.HelloAction" method="execute">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<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>
|
||||
<result name="success" type="redirectAction">index</result>
|
||||
<result name="input">/WEB-INF/views/user/login.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="dashboard" class="com.demo.action.DashboardAction" method="execute">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result>/WEB-INF/views/user/dashboard.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="logout" class="com.demo.action.LogoutAction" method="execute">
|
||||
<result type="redirectAction">loginPage</result>
|
||||
</action>
|
||||
|
||||
<action name="userFormPage">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result>/WEB-INF/views/user/form.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="validationPage">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result>/WEB-INF/views/validation/form.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="uploadPage">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result>/WEB-INF/views/upload/index.jsp</result>
|
||||
</action>
|
||||
|
||||
<!-- 用户表单提交 -->
|
||||
<action name="submitUser" class="com.demo.action.UserAction" method="submit">
|
||||
<result name="success">/user/success.jsp</result>
|
||||
<result name="input">/user/form.jsp</result>
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result name="success">/WEB-INF/views/user/success.jsp</result>
|
||||
<result name="input">/WEB-INF/views/user/form.jsp</result>
|
||||
</action>
|
||||
|
||||
<!-- 文件上传示例 -->
|
||||
<action name="upload" class="com.demo.action.FileUploadAction" method="execute">
|
||||
<result name="success">/upload/success.jsp</result>
|
||||
<result name="input">/upload/index.jsp</result>
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result name="success">/WEB-INF/views/upload/success.jsp</result>
|
||||
<result name="input">/WEB-INF/views/upload/index.jsp</result>
|
||||
</action>
|
||||
|
||||
<!-- AJAX 示例 -->
|
||||
<action name="ajax" class="com.demo.action.AjaxAction" method="execute">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<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>
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result name="success">/WEB-INF/views/validation/success.jsp</result>
|
||||
<result name="input">/WEB-INF/views/validation/form.jsp</result>
|
||||
</action>
|
||||
|
||||
</package>
|
||||
|
||||
<!-- REST 风格示例 -->
|
||||
<package name="rest" namespace="/api" extends="struts-default">
|
||||
<action name="users" class="com.demo.action.rest.UserRestAction">
|
||||
<package name="rest" namespace="/api" extends="json-default">
|
||||
<interceptors>
|
||||
<interceptor name="auth" class="com.demo.action.interceptor.AuthInterceptor"/>
|
||||
<interceptor-stack name="secureStack">
|
||||
<interceptor-ref name="auth"/>
|
||||
<interceptor-ref name="defaultStack"/>
|
||||
</interceptor-stack>
|
||||
</interceptors>
|
||||
<action name="users" class="com.demo.action.rest.UserRestAction" method="execute">
|
||||
<interceptor-ref name="secureStack"/>
|
||||
<result type="json"/>
|
||||
</action>
|
||||
</package>
|
||||
|
||||
</struts>
|
||||
286
web/WEB-INF/views/index.jsp
Normal file
286
web/WEB-INF/views/index.jsp
Normal file
@@ -0,0 +1,286 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Struts2 学习门户</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eff2f5;
|
||||
--panel: rgba(255,255,255,0.97);
|
||||
--line: #d4d9e6;
|
||||
--text: #1f2b3d;
|
||||
--muted: #52607a;
|
||||
--brand: #1464c7;
|
||||
--accent: #0d9488;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||
background: linear-gradient(135deg, #e3ebff 0%, #f1f5f9 55%, #effaf5 100%);
|
||||
color: var(--text);
|
||||
}
|
||||
.shell {
|
||||
width: min(1200px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border-radius: 26px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 25px 50px rgba(15, 23, 42, 0.1);
|
||||
border: 1px solid var(--line);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
margin: 8px 0 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn, .btn-soft, .btn-ghost {
|
||||
padding: 12px 18px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn {
|
||||
background: linear-gradient(135deg, var(--brand), #3d6fe7);
|
||||
color: white;
|
||||
}
|
||||
.btn-soft {
|
||||
background: #f4f7ff;
|
||||
color: var(--brand);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.btn-ghost {
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
.lang-warning {
|
||||
font-size: 14px;
|
||||
color: var(--brand);
|
||||
}
|
||||
.portal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.portal-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255,255,255,0.85);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.portal-links {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.portal-links .link-btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.insight-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.insight-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
min-height: 150px;
|
||||
}
|
||||
.chain-pill {
|
||||
display: inline-flex;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: #f4f7ff;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.session-note {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card hero-card">
|
||||
<div class="eyebrow" id="heroEyebrow">Struts2 学习门户</div>
|
||||
<h1 id="heroTitle">登录之后才能继续学习</h1>
|
||||
<p id="heroDesc">所有课程模块(首页/实验/JSON 等)都由 AuthInterceptor 保护,未登录访问会自动跳转。请先登录再回来。</p>
|
||||
<div class="lang-warning" id="heroWarning">登录后才能看到完整课程导航与实验入口。</div>
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="dashboard.action" id="heroActionPrimary">进入仪表盘</a>
|
||||
<a class="btn-soft" href="logout.action" id="heroActionSecondary">退出登录</a>
|
||||
<button class="btn-ghost" type="button" id="languageBtnHero">EN</button>
|
||||
</div>
|
||||
<div class="session-note" id="heroSessionNote">
|
||||
<s:if test="#session.demoUser != null">
|
||||
当前:<s:property value="#session.demoUser"/>(<s:property value="#session.demoRole"/>)
|
||||
</s:if>
|
||||
<s:else>
|
||||
当前尚未登录,请先完成登录再浏览实验。
|
||||
</s:else>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card portal-section">
|
||||
<div class="eyebrow" id="portalEyebrow">学习模块导航</div>
|
||||
<h2 id="portalTitle">分步骤理解 Struts2</h2>
|
||||
<p id="portalDesc">每个模块都通过 `.action` 路由触达,由统一的 Session 鉴权链路保护。</p>
|
||||
<div class="portal-grid">
|
||||
<article class="portal-card">
|
||||
<h3 id="portalCard1Title">请求生命周期</h3>
|
||||
<p id="portalCard1Desc">Dispatcher → Action → Interceptor → Result,严格按链路走向。</p>
|
||||
<div>
|
||||
<span class="chain-pill">Dispatcher</span>
|
||||
<span class="chain-pill">Action</span>
|
||||
<span class="chain-pill">Interceptors</span>
|
||||
<span class="chain-pill">Result</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="portal-card">
|
||||
<h3 id="portalCard2Title">登录保护</h3>
|
||||
<p id="portalCard2Desc">`LoginAction` 写入 Session,`AuthInterceptor` 阻止任何未登录访问。</p>
|
||||
<div class="portal-links">
|
||||
<a class="link-btn" href="loginPage.action">登录页</a>
|
||||
<a class="link-btn" href="dashboard.action">仪表盘</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="portal-card">
|
||||
<h3 id="portalCard3Title">表单与校验</h3>
|
||||
<p id="portalCard3Desc">表单字段直接绑定到 Action,ValidationAction 负责完整校验流程。</p>
|
||||
<div class="portal-links">
|
||||
<a class="link-btn" href="userFormPage.action">表单</a>
|
||||
<a class="link-btn" href="validationPage.action">校验</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="portal-card">
|
||||
<h3 id="portalCard4Title">AJAX / REST 对照</h3>
|
||||
<p id="portalCard4Desc">对比 `ajax.action` 与 `api/users.action` 的 JSON 输出。</p>
|
||||
<div class="portal-links">
|
||||
<a class="link-btn" href="ajax.action">AJAX</a>
|
||||
<a class="link-btn" href="api/users.action">REST</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card insight-section">
|
||||
<div class="eyebrow">Insight</div>
|
||||
<h2>安全学习链路速览</h2>
|
||||
<div class="insight-grid">
|
||||
<article class="insight-card">
|
||||
<h3>登录才能使用</h3>
|
||||
<p>未登录访问 `dashboard.action` 等任何端点都会被 AuthInterceptor 重定向至 `loginPage.action`。</p>
|
||||
</article>
|
||||
<article class="insight-card">
|
||||
<h3>表单绑定观察</h3>
|
||||
<p>`UserAction` 按字段绑定,`ValidationAction` 造成的字段错误会带回当前 JSP。</p>
|
||||
</article>
|
||||
<article class="insight-card">
|
||||
<h3>AJAX vs REST</h3>
|
||||
<p>`ajax.action` 返回对话式 JSON,`api/users.action` 呈现 REST 样式,便于教学对照。</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="../assets/struts-lab.js"></script>
|
||||
<script>
|
||||
strutsLab.mount({
|
||||
buttonId: "languageBtnHero",
|
||||
messages: {
|
||||
zh: {
|
||||
pageTitle: "Struts2 学习门户",
|
||||
text: {
|
||||
heroEyebrow: "Struts2 学习门户",
|
||||
heroTitle: "登录之后才能继续学习",
|
||||
heroDesc: "所有入口都由 Session + AuthInterceptor 保护,未登录将自动跳回登录页;登录成功后会先回到这个统一门户。",
|
||||
heroWarning: "当前门户本身也在登录保护之下,适合用来总览各实验模块。",
|
||||
heroActionPrimary: "进入仪表盘",
|
||||
heroActionSecondary: "退出登录",
|
||||
portalEyebrow: "学习模块导航",
|
||||
portalTitle: "分步骤理解 Struts2",
|
||||
portalDesc: "每个 `.action` 路径都在登录后才能访问,Session 鉴权串联整个体验。",
|
||||
portalCard1Title: "请求生命周期",
|
||||
portalCard1Desc: "Dispatcher → Action → Interceptor → Result,典型执行路径。",
|
||||
portalCard2Title: "登录保护",
|
||||
portalCard2Desc: "`LoginAction` 写入 Session,`AuthInterceptor` 拦截未登录。",
|
||||
portalCard3Title: "表单与校验",
|
||||
portalCard3Desc: "Action 字段绑定 + ValidationAction 校验,成功后进入汇总。",
|
||||
portalCard4Title: "AJAX / REST 对照",
|
||||
portalCard4Desc: "对比 `ajax.action`(AJAX)与 `api/users.action`(REST)。"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Struts2 Learning Portal",
|
||||
text: {
|
||||
heroEyebrow: "Struts2 Learning Portal",
|
||||
heroTitle: "Login before you explore",
|
||||
heroDesc: "All modules are protected by Session + AuthInterceptor. After login, this protected portal becomes the unified entry.",
|
||||
heroWarning: "The portal itself is protected and serves as the overview for every lab module.",
|
||||
heroActionPrimary: "Go to dashboard",
|
||||
heroActionSecondary: "Log out",
|
||||
portalEyebrow: "Module guide",
|
||||
portalTitle: "Step-by-step Struts2",
|
||||
portalDesc: "Every `.action` route sits behind login; the session chain secures the experience.",
|
||||
portalCard1Title: "Request lifecycle",
|
||||
portalCard1Desc: "Dispatcher → Action → Interceptor → Result shows the execution path.",
|
||||
portalCard2Title: "Login protection",
|
||||
portalCard2Desc: "`LoginAction` writes Session, `AuthInterceptor` blocks unauthenticated hits.",
|
||||
portalCard3Title: "Forms & validation",
|
||||
portalCard3Desc: "Field binding plus `ValidationAction` determine success/failure views.",
|
||||
portalCard4Title: "AJAX / REST contrast",
|
||||
portalCard4Desc: "Compare `ajax.action` (AJAX) with `api/users.action` (REST) outputs."
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
129
web/WEB-INF/views/upload/index.jsp
Normal file
129
web/WEB-INF/views/upload/index.jsp
Normal file
@@ -0,0 +1,129 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上传元数据实验 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 55%, #dff4ff 100%);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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: #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;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #0284c7, #38bdf8);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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="shell">
|
||||
<div class="eyebrow">受保护上传页</div>
|
||||
<h1>只采集上传元数据,不把文件真正落盘</h1>
|
||||
<p>这个页面保留了 multipart 表单绑定的教学意义,但避免真实写盘,更适合本地、VPS 和课堂演示环境。</p>
|
||||
|
||||
<s:if test="#session.demoUser == null">
|
||||
<div class="note">
|
||||
<strong>当前未登录</strong>
|
||||
<p style="margin-top: 8px;">上传页也已经接入 Session 保护。请先登录,再看 Struts2 对文件字段和元数据的绑定结果。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../loginPage.action">先去登录</a>
|
||||
<a class="link-btn" href="../index.action">返回门户</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<div class="note">
|
||||
<strong>这一页采集什么</strong>
|
||||
<p style="margin-top: 8px;">主文件名、内容类型和总文件数量。不会真实保存上传文件。</p>
|
||||
</div>
|
||||
|
||||
<s:if test="hasActionErrors()">
|
||||
<div class="action-error"><s:property value="actionErrors[0]"/></div>
|
||||
</s:if>
|
||||
|
||||
<form action="<s:url action='upload' namespace='/'/>" method="post" enctype="multipart/form-data">
|
||||
<div class="field">
|
||||
<label for="upload">主文件</label>
|
||||
<input id="upload" type="file" name="upload"/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="uploads">附加文件</label>
|
||||
<input id="uploads" type="file" name="uploads" multiple="multiple"/>
|
||||
</div>
|
||||
|
||||
<button type="submit">提交并查看上传元数据</button>
|
||||
</form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../index.action">回到门户</a>
|
||||
</div>
|
||||
</s:else>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
92
web/WEB-INF/views/upload/success.jsp
Normal file
92
web/WEB-INF/views/upload/success.jsp
Normal file
@@ -0,0 +1,92 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上传元数据结果 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
.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;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="eyebrow">上传结果</div>
|
||||
<h1>上传元数据已经被 Action 成功接收</h1>
|
||||
<p>这个实验只记录元数据,不真正保存文件。重点是理解 Struts2 对 multipart 表单字段的接收方式。</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>文件数量</span>
|
||||
<strong><s:property value="fileCount" default="0"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>主文件名</span>
|
||||
<strong><s:property value="uploadFileName" default="未提供"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>内容类型</span>
|
||||
<strong><s:property value="uploadContentType" default="未知"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../uploadPage.action">再做一次上传实验</a>
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
341
web/WEB-INF/views/user/dashboard.jsp
Normal file
341
web/WEB-INF/views/user/dashboard.jsp
Normal file
@@ -0,0 +1,341 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录后仪表盘 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eff7f5;
|
||||
--panel: rgba(255,255,255,0.95);
|
||||
--line: #d5ebe3;
|
||||
--text: #123028;
|
||||
--muted: #4a655c;
|
||||
--brand: #0d8f7c;
|
||||
--soft: #e8f8f2;
|
||||
--shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(13, 143, 124, 0.15), transparent 24%),
|
||||
radial-gradient(circle at bottom left, rgba(49, 196, 141, 0.12), transparent 22%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.shell {
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.hero-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand);
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2, h3 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: var(--muted); line-height: 1.85; }
|
||||
.actions, .links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn, .btn-soft, .btn-ghost, .link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--line);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn { background: linear-gradient(135deg, var(--brand), #31c48d); color: white; border: 0; }
|
||||
.btn-soft { background: var(--soft); color: var(--brand); }
|
||||
.btn-ghost, .link-btn { background: white; color: var(--text); }
|
||||
.stats, .grid, .steps {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.steps { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.stat, .panel, .step {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.68);
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||||
.stat strong { font-size: 22px; }
|
||||
.tag-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--soft);
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.panel ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.85;
|
||||
}
|
||||
.step strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.step p { margin: 0; }
|
||||
@media (max-width: 960px) {
|
||||
.stats, .grid, .steps { grid-template-columns: 1fr; }
|
||||
.hero-head { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card">
|
||||
<div class="hero-head">
|
||||
<div>
|
||||
<div class="eyebrow" id="heroEyebrow">登录后仪表盘</div>
|
||||
<h1 id="heroTitle">Session 已建立,后续实验页现在通过拦截器受保护</h1>
|
||||
<p id="heroText">这一页的作用不是展示“登录成功”四个字,而是把你接下来的学习路线展开:继续看用户表单、校验、上传,或者回到门户解释这条 Session 鉴权链路是如何工作的。</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
|
||||
<a class="btn" href="../userFormPage.action" id="primaryAction">进入用户表单</a>
|
||||
<a class="btn-soft" href="../logout.action" id="logoutAction">退出登录</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span id="statUserLabel">当前用户</span>
|
||||
<strong><s:property value="displayName"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="statRoleLabel">当前角色</span>
|
||||
<strong id="roleValue" data-role-code="<s:property value='role'/>"><s:property value="role"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="statTimeLabel">登录时间</span>
|
||||
<strong><s:property value="loginTime"/></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow" id="secureEyebrow">受保护实验</div>
|
||||
<h2 id="secureTitle">现在你可以进入这些需要登录的页面</h2>
|
||||
<div class="grid">
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagUser">字段绑定</span>
|
||||
<span class="tag" id="tagUserSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelUserTitle">用户资料表单</h3>
|
||||
<p id="panelUserText">继续看请求参数如何绑定到 Action 属性,并在成功页生成一份汇总结果。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../userFormPage.action" id="panelUserLink">打开用户表单</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagValidation">validate()</span>
|
||||
<span class="tag" id="tagValidationSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelValidationTitle">字段校验页</h3>
|
||||
<p id="panelValidationText">对比校验失败和成功状态,理解为什么 Struts2 会在业务逻辑前先执行校验方法。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../validationPage.action" id="panelValidationLink">打开校验页</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagUpload">multipart</span>
|
||||
<span class="tag" id="tagUploadSecure">受保护</span>
|
||||
</div>
|
||||
<h3 id="panelUploadTitle">上传元数据页</h3>
|
||||
<p id="panelUploadText">这里保留文件上传的教学价值,但不把文件真正写入磁盘,适合安全演示。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../uploadPage.action" id="panelUploadLink">打开上传页</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="tag-list">
|
||||
<span class="tag" id="tagPortal">解释链路</span>
|
||||
<span class="tag" id="tagPortalTrace">回看入口</span>
|
||||
</div>
|
||||
<h3 id="panelPortalTitle">回门户讲完整链路</h3>
|
||||
<p id="panelPortalText">回到入口页,可以从公开入口、登录动作、Session 写入、拦截器保护这条路线整体复盘。</p>
|
||||
<div class="links" style="margin-top: 14px;">
|
||||
<a class="link-btn" href="../index.action" id="panelPortalLink">返回门户</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow" id="flowEyebrow">鉴权流程</div>
|
||||
<h2 id="flowTitle">这次你实际走过的 Struts2 登录链路</h2>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<strong id="flowStep1Title">1. 表单提交到 login Action</strong>
|
||||
<p id="flowStep1Text">用户名和密码先进入 Action,校验失败会直接回到登录页。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep2Title">2. Action 写入 Session</strong>
|
||||
<p id="flowStep2Text">账号通过后,用户、角色和登录时间被放进 Session。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep3Title">3. 拦截器保护页面动作</strong>
|
||||
<p id="flowStep3Text">用户表单、校验页、上传页和仪表盘都先经过 AuthInterceptor。</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<strong id="flowStep4Title">4. 未登录会被打回登录页</strong>
|
||||
<p id="flowStep4Text">如果没有 Session,安全动作不会继续执行,而是直接跳回登录入口。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="../assets/struts-lab.js"></script>
|
||||
<script>
|
||||
strutsLab.mount({
|
||||
messages: {
|
||||
zh: {
|
||||
pageTitle: "登录后仪表盘 - Struts2 学习实验台",
|
||||
text: {
|
||||
heroEyebrow: "登录后仪表盘",
|
||||
heroTitle: "Session 已建立,后续实验页现在通过拦截器受保护",
|
||||
heroText: "这一页的作用不是展示“登录成功”四个字,而是把你接下来的学习路线展开:继续看用户表单、校验、上传,或者回到门户解释这条 Session 鉴权链路是如何工作的。",
|
||||
primaryAction: "进入用户表单",
|
||||
logoutAction: "退出登录",
|
||||
statUserLabel: "当前用户",
|
||||
statRoleLabel: "当前角色",
|
||||
statTimeLabel: "登录时间",
|
||||
secureEyebrow: "受保护实验",
|
||||
secureTitle: "现在你可以进入这些需要登录的页面",
|
||||
tagUser: "字段绑定",
|
||||
tagUserSecure: "受保护",
|
||||
panelUserTitle: "用户资料表单",
|
||||
panelUserText: "继续看请求参数如何绑定到 Action 属性,并在成功页生成一份汇总结果。",
|
||||
panelUserLink: "打开用户表单",
|
||||
tagValidation: "validate()",
|
||||
tagValidationSecure: "受保护",
|
||||
panelValidationTitle: "字段校验页",
|
||||
panelValidationText: "对比校验失败和成功状态,理解为什么 Struts2 会在业务逻辑前先执行校验方法。",
|
||||
panelValidationLink: "打开校验页",
|
||||
tagUpload: "multipart",
|
||||
tagUploadSecure: "受保护",
|
||||
panelUploadTitle: "上传元数据页",
|
||||
panelUploadText: "这里保留文件上传的教学价值,但不把文件真正写入磁盘,适合安全演示。",
|
||||
panelUploadLink: "打开上传页",
|
||||
tagPortal: "解释链路",
|
||||
tagPortalTrace: "回看入口",
|
||||
panelPortalTitle: "回门户讲完整链路",
|
||||
panelPortalText: "回到入口页,可以从公开入口、登录动作、Session 写入、拦截器保护这条路线整体复盘。",
|
||||
panelPortalLink: "返回门户",
|
||||
flowEyebrow: "鉴权流程",
|
||||
flowTitle: "这次你实际走过的 Struts2 登录链路",
|
||||
flowStep1Title: "1. 表单提交到 login Action",
|
||||
flowStep1Text: "用户名和密码先进入 Action,校验失败会直接回到登录页。",
|
||||
flowStep2Title: "2. Action 写入 Session",
|
||||
flowStep2Text: "账号通过后,用户、角色和登录时间被放进 Session。",
|
||||
flowStep3Title: "3. 拦截器保护页面动作",
|
||||
flowStep3Text: "用户表单、校验页、上传页和仪表盘都先经过 AuthInterceptor。",
|
||||
flowStep4Title: "4. 未登录会被打回登录页",
|
||||
flowStep4Text: "如果没有 Session,安全动作不会继续执行,而是直接跳回登录入口。"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Post-login Dashboard - Struts2 Learning Lab",
|
||||
text: {
|
||||
heroEyebrow: "Post-login dashboard",
|
||||
heroTitle: "A session now exists, and later lab pages are protected by an interceptor",
|
||||
heroText: "This page is not just a success notice. It expands the next learning route: continue to user forms, validation, and upload, or go back to the portal and explain the full session-auth path.",
|
||||
primaryAction: "Open user form",
|
||||
logoutAction: "Log out",
|
||||
statUserLabel: "Current user",
|
||||
statRoleLabel: "Current role",
|
||||
statTimeLabel: "Login time",
|
||||
secureEyebrow: "Protected labs",
|
||||
secureTitle: "These pages now require a valid login session",
|
||||
tagUser: "Binding",
|
||||
tagUserSecure: "Protected",
|
||||
panelUserTitle: "User profile form",
|
||||
panelUserText: "Continue with request parameter binding into action properties and a structured success summary.",
|
||||
panelUserLink: "Open user form",
|
||||
tagValidation: "validate()",
|
||||
tagValidationSecure: "Protected",
|
||||
panelValidationTitle: "Validation page",
|
||||
panelValidationText: "Compare invalid and successful states and explain why Struts2 runs validate() before business output.",
|
||||
panelValidationLink: "Open validation page",
|
||||
tagUpload: "multipart",
|
||||
tagUploadSecure: "Protected",
|
||||
panelUploadTitle: "Upload metadata page",
|
||||
panelUploadText: "Keeps the teaching value of file upload while avoiding real disk writes, which is safer for demo environments.",
|
||||
panelUploadLink: "Open upload page",
|
||||
tagPortal: "Trace the path",
|
||||
tagPortalTrace: "Back to entry",
|
||||
panelPortalTitle: "Return to the portal",
|
||||
panelPortalText: "Go back and explain the full route from public entry to login action, session write, and interceptor protection.",
|
||||
panelPortalLink: "Back to portal",
|
||||
flowEyebrow: "Auth flow",
|
||||
flowTitle: "The Struts2 login path you just completed",
|
||||
flowStep1Title: "1. Form submits to the login action",
|
||||
flowStep1Text: "Username and password reach the action first; validation failure returns to the login page.",
|
||||
flowStep2Title: "2. The action writes into session",
|
||||
flowStep2Text: "After success, user, role, and login time are stored in session.",
|
||||
flowStep3Title: "3. The interceptor protects page actions",
|
||||
flowStep3Text: "The user form, validation page, upload page, and dashboard all pass through AuthInterceptor first.",
|
||||
flowStep4Title: "4. Unauthenticated access goes back to login",
|
||||
flowStep4Text: "If no session exists, secure actions stop immediately and redirect to the login entry."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: function (ui) {
|
||||
const roleNode = document.getElementById("roleValue");
|
||||
if (roleNode && roleNode.dataset.roleCode === "admin") {
|
||||
roleNode.textContent = ui.language === "zh" ? "演示管理员" : "Demo administrator";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
132
web/WEB-INF/views/user/form.jsp
Normal file
132
web/WEB-INF/views/user/form.jsp
Normal file
@@ -0,0 +1,132 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户资料表单 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #f97316 0%, #fb923c 50%, #ffedd5 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.96);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #ea580c;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6c5545; line-height: 1.85; }
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4c3422; }
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #ead8ca;
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #ea580c, #fb923c);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error { color: #c2410c; font-size: 13px; margin-top: 6px; }
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #fff5ef;
|
||||
border: 1px solid #f2d9ca;
|
||||
}
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff1e8;
|
||||
color: #c2410c;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">受保护表单</div>
|
||||
<h1>通过 Action 属性绑定提交一份用户资料</h1>
|
||||
<p>这个页面现在放在登录保护链路里。重点不是字段本身,而是观察请求参数如何进入 Action,再在成功页汇总成一份结构化结果。</p>
|
||||
|
||||
<s:if test="#session.demoUser == null">
|
||||
<div class="note">
|
||||
<strong>当前未登录</strong>
|
||||
<p style="margin-top: 8px;">这个实验页已经接入 Session 保护。请先登录,再回来看字段绑定和错误回显。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../loginPage.action">先去登录</a>
|
||||
<a class="link-btn" href="../index.action">返回门户</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<div class="note">
|
||||
<strong>建议观察点</strong>
|
||||
<p style="margin-top: 8px;">依次观察:字段名如何对应 Action 属性、校验错误如何回显、成功页如何读取 Action 结果。</p>
|
||||
</div>
|
||||
|
||||
<form action="<s:url action='submitUser' namespace='/'/>" method="post">
|
||||
<div class="field">
|
||||
<label for="username">用户名</label>
|
||||
<input id="username" name="username" placeholder="platform-owner" value='<s:property value="username"/>'/>
|
||||
<s:if test="fieldErrors['username'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['username'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<input id="email" name="email" placeholder="platform@example.com" value='<s:property value="email"/>'/>
|
||||
<s:if test="fieldErrors['email'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['email'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="phone">手机号</label>
|
||||
<input id="phone" name="phone" placeholder="13800000000" value='<s:property value="phone"/>'/>
|
||||
<s:if test="fieldErrors['phone'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['phone'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<button type="submit">提交资料并生成汇总页</button>
|
||||
</form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../validationPage.action">下一步看校验页</a>
|
||||
</div>
|
||||
</s:else>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
278
web/WEB-INF/views/user/login.jsp
Normal file
278
web/WEB-INF/views/user/login.jsp
Normal file
@@ -0,0 +1,278 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session 登录实验 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #122c63 0%, #1464c7 52%, #4db5ff 100%);
|
||||
}
|
||||
.shell {
|
||||
width: min(1120px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 430px;
|
||||
gap: 18px;
|
||||
}
|
||||
.panel {
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.22);
|
||||
}
|
||||
.top-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #1464c7;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1, h2 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #566a80; line-height: 1.85; }
|
||||
.btn-ghost, .link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d7e5f7;
|
||||
background: #ffffff;
|
||||
color: #1464c7;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #edf5ff;
|
||||
border: 1px solid #d7e5f7;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.stat {
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d7e5f7;
|
||||
background: #f8fbff;
|
||||
}
|
||||
.stat strong { display: block; margin-bottom: 6px; }
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
color: #1f3650;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #d6e1ed;
|
||||
font: inherit;
|
||||
}
|
||||
button.submit-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #1464c7, #3d8ef6);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error {
|
||||
color: #c53d3d;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.action-error {
|
||||
margin: 0 0 16px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff1f1;
|
||||
color: #b63a3a;
|
||||
border: 1px solid #f1c4c4;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="panel">
|
||||
<div class="top-actions">
|
||||
<div class="eyebrow" id="leftEyebrow">Session 登录章节</div>
|
||||
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
|
||||
</div>
|
||||
<h1 id="leftTitle">用最经典的 Struts2 Session 登录,把后续实验页保护起来</h1>
|
||||
<p id="leftText">这个页面不只是演示登录表单,而是把 Action 校验、写入 Session、拦截器校验和登录后仪表盘串成一个完整章节,适合你系统掌握老项目最常见的鉴权思路。</p>
|
||||
|
||||
<div class="note">
|
||||
<strong id="credentialTitle">演示账号</strong>
|
||||
<p id="credentialBody" style="margin-top: 8px;">用户名:<code>admin</code><br/>密码:<code>123456</code></p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<strong id="statOneTitle">这一页讲什么</strong>
|
||||
<span id="statOneText">Action 校验、错误回显、Session 写入、登录后跳转。</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong id="statTwoTitle">下一步看什么</strong>
|
||||
<span id="statTwoText">登录成功后会先回到学习门户,再进入仪表盘、用户表单、校验页和上传页。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.action" id="backPortalLink">返回门户</a>
|
||||
<s:if test="#session.demoUser != null">
|
||||
<a class="link-btn" href="../index.action" id="dashboardLink">进入学习门户</a>
|
||||
<a class="link-btn" href="../logout.action" id="logoutLink">退出登录</a>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<a class="link-btn" href="../hello.action?name=Team" id="helloLink">运行 Hello Action</a>
|
||||
</s:else>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="eyebrow" id="formEyebrow">登录表单</div>
|
||||
<h2 id="formTitle">输入演示账号</h2>
|
||||
|
||||
<s:if test="#session.demoUser != null">
|
||||
<div class="note">
|
||||
<strong id="loggedInTitle">当前已登录</strong>
|
||||
<p id="loggedInText" style="margin-top: 8px;">你已经拥有可访问的 Session,可以先进入学习门户总览全部模块,也可以退出后重新体验登录过程。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../index.action" id="loggedInDashboardLink">打开学习门户</a>
|
||||
<a class="link-btn" href="../logout.action" id="loggedInLogoutLink">退出后重试</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:if test="hasActionErrors()">
|
||||
<div class="action-error"><s:property value="actionErrors[0]"/></div>
|
||||
</s:if>
|
||||
|
||||
<form action="<s:url action='login' namespace='/'/>" method="post">
|
||||
<div class="field">
|
||||
<label for="username" id="usernameLabel">用户名</label>
|
||||
<input id="username" name="username" placeholder="admin" autocomplete="username" value='<s:property value="username"/>'/>
|
||||
<s:if test="fieldErrors['username'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['username'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password" id="passwordLabel">密码</label>
|
||||
<input id="password" name="password" type="password" placeholder="123456" autocomplete="current-password"/>
|
||||
<s:if test="fieldErrors['password'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['password'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" type="submit" id="submitBtn">写入 Session 并进入学习门户</button>
|
||||
</form>
|
||||
</s:else>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="../assets/struts-lab.js"></script>
|
||||
<script>
|
||||
strutsLab.mount({
|
||||
messages: {
|
||||
zh: {
|
||||
pageTitle: "Session 登录实验 - Struts2 学习实验台",
|
||||
text: {
|
||||
leftEyebrow: "Session 登录章节",
|
||||
leftTitle: "用最经典的 Struts2 Session 登录,把后续实验页保护起来",
|
||||
leftText: "这个页面不只是演示登录表单,而是把 Action 校验、写入 Session、拦截器校验和登录后仪表盘串成一个完整章节,适合你系统掌握老项目最常见的鉴权思路。",
|
||||
credentialTitle: "演示账号",
|
||||
statOneTitle: "这一页讲什么",
|
||||
statOneText: "Action 校验、错误回显、Session 写入、登录后跳转。",
|
||||
statTwoTitle: "下一步看什么",
|
||||
statTwoText: "登录成功后会先回到学习门户,再进入仪表盘、用户表单、校验页和上传页。",
|
||||
backPortalLink: "返回门户",
|
||||
dashboardLink: "进入学习门户",
|
||||
logoutLink: "退出登录",
|
||||
formEyebrow: "登录表单",
|
||||
formTitle: "输入演示账号",
|
||||
loggedInTitle: "当前已登录",
|
||||
loggedInText: "你已经拥有可访问的 Session,可以先进入学习门户总览全部模块,也可以退出后重新体验登录过程。",
|
||||
loggedInDashboardLink: "打开学习门户",
|
||||
loggedInLogoutLink: "退出后重试",
|
||||
helloLink: "运行 Hello Action",
|
||||
usernameLabel: "用户名",
|
||||
passwordLabel: "密码",
|
||||
submitBtn: "写入 Session 并进入学习门户"
|
||||
},
|
||||
html: {
|
||||
credentialBody: "用户名:<code>admin</code><br/>密码:<code>123456</code>"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Session Login Lab - Struts2 Learning Lab",
|
||||
text: {
|
||||
leftEyebrow: "Session login chapter",
|
||||
leftTitle: "Use the classic Struts2 session login pattern to protect later lab pages",
|
||||
leftText: "This page is not just a login form. It connects action validation, session writes, interceptor checks, and a post-login dashboard into one complete learning chapter.",
|
||||
credentialTitle: "Demo credentials",
|
||||
statOneTitle: "What this page teaches",
|
||||
statOneText: "Action validation, error echoing, session writes, and post-login routing.",
|
||||
statTwoTitle: "What to open next",
|
||||
statTwoText: "After login, return to the learning portal first, then continue to the dashboard, user form, validation page, and upload page.",
|
||||
backPortalLink: "Back to portal",
|
||||
dashboardLink: "Open learning portal",
|
||||
logoutLink: "Log out",
|
||||
formEyebrow: "Login form",
|
||||
formTitle: "Enter the demo account",
|
||||
loggedInTitle: "Already logged in",
|
||||
loggedInText: "A valid session already exists. Open the learning portal for the full overview or log out and replay the login flow.",
|
||||
loggedInDashboardLink: "Open learning portal",
|
||||
loggedInLogoutLink: "Log out and retry",
|
||||
helloLink: "Run hello action",
|
||||
usernameLabel: "Username",
|
||||
passwordLabel: "Password",
|
||||
submitBtn: "Write session and enter learning portal"
|
||||
},
|
||||
html: {
|
||||
credentialBody: "Username: <code>admin</code><br/>Password: <code>123456</code>"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
132
web/WEB-INF/views/user/success.jsp
Normal file
132
web/WEB-INF/views/user/success.jsp
Normal file
@@ -0,0 +1,132 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户资料汇总 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #0d8f7c 0%, #31c48d 55%, #d4f7e7 100%);
|
||||
color: #123028;
|
||||
}
|
||||
.shell {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
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: #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;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="card">
|
||||
<div class="eyebrow">提交成功</div>
|
||||
<h1>用户资料已经通过 Action 处理并汇总完成</h1>
|
||||
<p>这页现在只负责展示用户资料提交的结果,不再和登录成功页面混用,便于你单独理解“表单提交成功页”这一类经典 Struts2 结果页。</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">当前提交</div>
|
||||
<h2><s:property value="username"/></h2>
|
||||
<p>
|
||||
<s:if test="profileReady">
|
||||
这份资料已经达到“可继续演示”的状态,可以继续进入校验页或上传页。
|
||||
</s:if>
|
||||
<s:else>
|
||||
资料已经提交成功,但手机号仍偏弱。你可以回表单页再试一次,观察不同输入对结果的影响。
|
||||
</s:else>
|
||||
</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>用户名</span>
|
||||
<strong><s:property value="username"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>邮箱</span>
|
||||
<strong><s:property value="email"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>手机号</span>
|
||||
<strong><s:property value="phone"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>资料状态</span>
|
||||
<strong>
|
||||
<s:if test="profileReady">可继续实验</s:if>
|
||||
<s:else>建议复查</s:else>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>提交时间</span>
|
||||
<strong><s:property value="submittedAt"/></strong>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>结果类型</span>
|
||||
<strong>/WEB-INF/views/user/success.jsp</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../userFormPage.action">再试一次</a>
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../validationPage.action">继续看校验页</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
141
web/WEB-INF/views/validation/form.jsp
Normal file
141
web/WEB-INF/views/validation/form.jsp
Normal file
@@ -0,0 +1,141 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>字段校验实验 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, #db2777 0%, #f472b6 55%, #ffe4f1 100%);
|
||||
}
|
||||
.shell {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #db2777;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #6e5565; line-height: 1.85; }
|
||||
.field { margin-top: 16px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 700; color: #4a223b; }
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #ecd6e0;
|
||||
font: inherit;
|
||||
}
|
||||
textarea { resize: vertical; min-height: 120px; }
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #db2777, #f472b6);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.error { color: #c0265d; font-size: 13px; margin-top: 6px; }
|
||||
.note {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #fff3f8;
|
||||
border: 1px solid #f3d8e6;
|
||||
}
|
||||
.links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff0f7;
|
||||
color: #db2777;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">受保护校验页</div>
|
||||
<h1>在业务执行前,先把输入规则讲清楚</h1>
|
||||
<p>这个实验页专门用来讲 Struts2 的 <code>validate()</code>。你可以故意输错,再看字段错误如何被带回到原页面。</p>
|
||||
|
||||
<s:if test="#session.demoUser == null">
|
||||
<div class="note">
|
||||
<strong>当前未登录</strong>
|
||||
<p style="margin-top: 8px;">校验页也已经接入 Session 保护。请先登录,再体验校验失败和成功两种状态。</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../loginPage.action">先去登录</a>
|
||||
<a class="link-btn" href="../index.action">返回门户</a>
|
||||
</div>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<div class="note">
|
||||
<strong>建议观察点</strong>
|
||||
<p style="margin-top: 8px;">先故意输入一个短用户名、错误邮箱和超范围年龄,再修正后重新提交,对比两次页面反馈。</p>
|
||||
</div>
|
||||
|
||||
<form action="<s:url action='validate' namespace='/'/>" method="post">
|
||||
<div class="field">
|
||||
<label for="username">用户名(3 到 20 字符)</label>
|
||||
<input id="username" name="username" placeholder="release-manager" value='<s:property value="username"/>'/>
|
||||
<s:if test="fieldErrors['username'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['username'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<input id="email" name="email" placeholder="release@example.com" value='<s:property value="email"/>'/>
|
||||
<s:if test="fieldErrors['email'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['email'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="age">年龄(18 到 60)</label>
|
||||
<input id="age" name="age" placeholder="30" value='<s:property value="age"/>'/>
|
||||
<s:if test="fieldErrors['age'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['age'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="bio">简介</label>
|
||||
<textarea id="bio" name="bio" placeholder="描述角色、经验或当前负责的平台方向。"><s:property value="bio"/></textarea>
|
||||
<s:if test="fieldErrors['bio'] != null">
|
||||
<div class="error"><s:property value="fieldErrors['bio'][0]"/></div>
|
||||
</s:if>
|
||||
</div>
|
||||
|
||||
<button type="submit">提交并查看校验结果</button>
|
||||
</form>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
<a class="link-btn" href="../uploadPage.action">继续看上传页</a>
|
||||
</div>
|
||||
</s:else>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
102
web/WEB-INF/views/validation/success.jsp
Normal file
102
web/WEB-INF/views/validation/success.jsp
Normal file
@@ -0,0 +1,102 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>校验结果汇总 - Struts2 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
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 {
|
||||
width: min(860px, 100%);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.18);
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #db2777;
|
||||
font-weight: 800;
|
||||
}
|
||||
h1 { margin: 10px 0 12px; }
|
||||
p { margin: 0; color: #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;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="eyebrow">校验通过</div>
|
||||
<h1>所有输入都满足当前 Action 的校验规则</h1>
|
||||
<p>这页的价值在于把字段校验和业务执行分开解释。只有当所有字段都通过时,Action 才会进入成功结果页。</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><span>用户名</span><strong><s:property value="username"/></strong></div>
|
||||
<div class="stat"><span>邮箱</span><strong><s:property value="email"/></strong></div>
|
||||
<div class="stat"><span>年龄</span><strong><s:property value="age"/></strong></div>
|
||||
<div class="stat">
|
||||
<span>阶段判断</span>
|
||||
<strong><s:if test="seniorTrack">中阶段运维画像</s:if><s:else>早阶段运维画像</s:else></strong>
|
||||
</div>
|
||||
<div class="stat"><span>提交时间</span><strong><s:property value="submittedAt"/></strong></div>
|
||||
<div class="stat"><span>简介</span><strong><s:property value="bio" default="未填写简介"/></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="link-btn" href="../validationPage.action">重新做一次校验</a>
|
||||
<a class="link-btn" href="../dashboard.action">返回仪表盘</a>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>规则入口</span>
|
||||
<strong>validate()</strong>
|
||||
先校验,再决定是否允许执行 <code>execute()</code>。
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>教学重点</span>
|
||||
<strong>输入先于业务</strong>
|
||||
这正是传统 Struts2 表单页面里最值得讲清楚的部分。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,9 +5,8 @@
|
||||
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>
|
||||
@@ -22,7 +21,6 @@
|
||||
<welcome-file>index.jsp</welcome-file>
|
||||
</welcome-file-list>
|
||||
|
||||
<!-- 错误页面配置 -->
|
||||
<error-page>
|
||||
<error-code>404</error-code>
|
||||
<location>/error/404.jsp</location>
|
||||
|
||||
92
web/assets/struts-lab.js
Normal file
92
web/assets/struts-lab.js
Normal file
@@ -0,0 +1,92 @@
|
||||
(function () {
|
||||
const LANGUAGE_KEY = "struts_lab_language";
|
||||
|
||||
function getLanguage() {
|
||||
return localStorage.getItem(LANGUAGE_KEY) === "en" ? "en" : "zh";
|
||||
}
|
||||
|
||||
function setLanguage(language) {
|
||||
const resolved = language === "en" ? "en" : "zh";
|
||||
localStorage.setItem(LANGUAGE_KEY, resolved);
|
||||
document.documentElement.lang = resolved === "zh" ? "zh-CN" : "en";
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function applyText(values) {
|
||||
Object.entries(values || {}).forEach(([id, value]) => {
|
||||
const node = document.getElementById(id);
|
||||
if (node) {
|
||||
node.textContent = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyHtml(values) {
|
||||
Object.entries(values || {}).forEach(([id, value]) => {
|
||||
const node = document.getElementById(id);
|
||||
if (node) {
|
||||
node.innerHTML = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyPlaceholders(values) {
|
||||
Object.entries(values || {}).forEach(([id, value]) => {
|
||||
const node = document.getElementById(id);
|
||||
if (node) {
|
||||
node.setAttribute("placeholder", value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mount(config) {
|
||||
const state = {
|
||||
language: setLanguage(getLanguage())
|
||||
};
|
||||
const button = document.getElementById(config.buttonId || "languageBtn");
|
||||
|
||||
function messages() {
|
||||
return config.messages[state.language] || config.messages.zh || {};
|
||||
}
|
||||
|
||||
function render() {
|
||||
const current = messages();
|
||||
if (current.pageTitle) {
|
||||
document.title = current.pageTitle;
|
||||
}
|
||||
applyText(current.text);
|
||||
applyHtml(current.html);
|
||||
applyPlaceholders(current.placeholders);
|
||||
if (button) {
|
||||
button.textContent = state.language === "zh" ? "EN" : "中文";
|
||||
}
|
||||
if (typeof config.render === "function") {
|
||||
config.render({
|
||||
language: state.language,
|
||||
messages: current,
|
||||
setLanguage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.addEventListener("click", function () {
|
||||
state.language = setLanguage(state.language === "zh" ? "en" : "zh");
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
render();
|
||||
return {
|
||||
getLanguage: function () {
|
||||
return state.language;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.strutsLab = {
|
||||
mount: mount,
|
||||
getLanguage: getLanguage,
|
||||
setLanguage: setLanguage
|
||||
};
|
||||
})();
|
||||
@@ -1,100 +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>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>
|
||||
<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>1. JSON 结果类型</h2>
|
||||
<div class="section">
|
||||
<p>使用 struts2-json-plugin 实现 AJAX 返回 JSON</p>
|
||||
<pre><span class="comment">// struts.xml 配置</span>
|
||||
<span class="keyword"><action</span> name=<span class="string">"getUser"</span> class=<span class="string">"com.demo.UserAction"</span><span class="keyword">></span>
|
||||
<span class="keyword"><result</span> type=<span class="string">"json"</span><span class="keyword">></span>
|
||||
<span class="comment"><!-- 可选: 只包含指定属性 --></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"includeProperties"</span><span class="keyword">></span>user\..*,success<span class="keyword"></param></span>
|
||||
<span class="keyword"></result></span>
|
||||
<span class="keyword"></action></span></pre>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<h2>2. Action 示例</h2>
|
||||
<div class="section">
|
||||
<pre><span class="keyword">public class</span> UserAction <span class="keyword">extends</span> ActionSupport {
|
||||
<pre>public class AjaxAction extends ActionSupport {
|
||||
private boolean success;
|
||||
private String message;
|
||||
private List<User> users;
|
||||
|
||||
<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;
|
||||
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 => r.json())
|
||||
.then(data => {
|
||||
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"><input</span> type=<span class="string">"text"</span> id=<span class="string">"keyword"</span> onkeyup=<span class="string">"search(this.value)"</span>/>
|
||||
<span class="keyword"><div</span> id=<span class="string">"results"</span><span class="keyword">></div></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">'<div>'</span> + item.name + <span class="string">'</div>'</span>);
|
||||
});
|
||||
});
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<a href="/demo/interceptor" class="btn">下一节:拦截器 →</a>
|
||||
<div class="links">
|
||||
<a class="btn" href="../../ajax.action">Open action JSON</a>
|
||||
<a class="btn" href="../../api/users.action">Open REST JSON</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,167 +1,52 @@
|
||||
<%@ 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="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>
|
||||
|
||||
<div class="content">
|
||||
<h1>👋 Hello World - 第一个 Struts2 应用</h1>
|
||||
<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>
|
||||
|
||||
<div class="note">
|
||||
<strong>📝 本节要点:</strong> 创建最简单的 Struts2 应用,掌握 Action 编写和 struts.xml 配置
|
||||
</div>
|
||||
<h2>Minimal action shape</h2>
|
||||
<pre>public class HelloAction extends ActionSupport {
|
||||
private String name;
|
||||
private String message;
|
||||
|
||||
<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"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
<span class="keyword"><web-app</span> xmlns=<span class="string">"http://xmlns.jcp.org/xml/ns/javaee"</span><span class="keyword">></span>
|
||||
|
||||
<span class="comment"><!-- Struts2 核心过滤器 --></span>
|
||||
<span class="keyword"><filter></span>
|
||||
<span class="keyword"><filter-name></span>struts2<span class="keyword"></filter-name></span>
|
||||
<span class="keyword"><filter-class></span>
|
||||
org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
|
||||
<span class="keyword"></filter-class></span>
|
||||
<span class="keyword"></filter></span>
|
||||
|
||||
<span class="keyword"><filter-mapping></span>
|
||||
<span class="keyword"><filter-name></span>struts2<span class="keyword"></filter-name></span>
|
||||
<span class="keyword"><url-pattern></span>/*<span class="keyword"></url-pattern></span>
|
||||
<span class="keyword"></filter-mapping></span>
|
||||
|
||||
<span class="keyword"></web-app></span></pre>
|
||||
</div>
|
||||
|
||||
<h2>3. struts.xml 配置</h2>
|
||||
<div class="section">
|
||||
<pre><span class="comment"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
<span class="keyword"><!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">></span>
|
||||
|
||||
<span class="keyword"><struts></span>
|
||||
<span class="comment"><!-- 开发模式 --></span>
|
||||
<span class="keyword"><constant</span> name=<span class="string">"struts.devMode"</span> value=<span class="string">"true"</span><span class="keyword">/></span>
|
||||
|
||||
<span class="comment"><!-- 定义包 --></span>
|
||||
<span class="keyword"><package</span> name=<span class="string">"default"</span> namespace=<span class="string">"/"</span> extends=<span class="string">"struts-default"</span><span class="keyword">></span>
|
||||
|
||||
<span class="comment"><!-- Action 配置 --></span>
|
||||
<span class="keyword"><action</span> name=<span class="string">"hello"</span> class=<span class="string">"com.demo.HelloAction"</span><span class="keyword">></span>
|
||||
<span class="keyword"><result></span>/hello.jsp<span class="keyword"></result></span>
|
||||
<span class="keyword"></action></span>
|
||||
|
||||
<span class="keyword"></package></span>
|
||||
<span class="keyword"></struts></span></pre>
|
||||
</div>
|
||||
|
||||
<h2>4. HelloAction.java</h2>
|
||||
<div class="section">
|
||||
<pre><span class="keyword">package</span> com.demo;
|
||||
|
||||
<span class="keyword">import</span> com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
<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"><%@ page contentType="text/html;charset=UTF-8" language="java" %></span>
|
||||
<span class="comment"><%@ taglib prefix="s" uri="/struts-tags" %></span>
|
||||
<span class="keyword"><!DOCTYPE html></span>
|
||||
<span class="keyword"><html></span>
|
||||
<span class="keyword"><body></span>
|
||||
<span class="comment"><!-- 使用 Struts2 标签获取 Action 返回的值 --></span>
|
||||
<span class="keyword"><h1><s:property</span> value=<span class="string">"message"</span>/></h1>
|
||||
<span class="keyword"></body></span>
|
||||
<span class="keyword"></html></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.action?name=Platform%20Team">Run hello action</a>
|
||||
<a class="btn" href="../../index.jsp">Back to portal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -4,169 +4,50 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据封装 - Struts2 学习</title>
|
||||
<title>模型绑定指南</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="shell">
|
||||
<div class="eyebrow">指南</div>
|
||||
<h1>属性绑定与 ModelDriven 绑定怎么理解</h1>
|
||||
<p>这页用来解释 Struts2 如何把请求参数装配进 Action。建议配合用户资料表单和字段校验实验一起看。</p>
|
||||
|
||||
<div class="content">
|
||||
<h1>📦 数据封装 - 属性驱动 vs 模型驱动</h1>
|
||||
<h2>属性驱动绑定</h2>
|
||||
<ul>
|
||||
<li>直接在 Action 上定义简单字段。</li>
|
||||
<li>为表单字段提供对应的 setter / getter。</li>
|
||||
<li>适合短流程 Demo 和小型表单。</li>
|
||||
</ul>
|
||||
|
||||
<div class="note">
|
||||
<strong>📝 本节要点:</strong> 掌握 Struts2 两种数据封装方式,将请求参数封装到 Java 对象
|
||||
</div>
|
||||
<h2>ModelDriven 绑定</h2>
|
||||
<ul>
|
||||
<li>通过 <code>getModel()</code> 返回一个独立模型对象。</li>
|
||||
<li>把请求数据和 Action 编排逻辑拆开。</li>
|
||||
<li>更适合复杂表单和嵌套对象。</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
<pre>public class UserAction extends ActionSupport implements ModelDriven<User> {
|
||||
private final User user = new User();
|
||||
|
||||
<h2>2. 方式一:属性驱动 (Property Driven)</h2>
|
||||
|
||||
<h3>2.1 基本属性封装</h3>
|
||||
<div class="section">
|
||||
<pre><span class="comment">/**
|
||||
* 直接在 Action 中定义属性
|
||||
* 表单: <input name="username"> 会自动调用 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>
|
||||
|
||||
<span class="comment">// 表单</span>
|
||||
<span class="keyword"><input</span> name=<span class="string">"user.username"</span>/>
|
||||
<span class="keyword"><input</span> name=<span class="string">"user.email"</span>/></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;
|
||||
|
||||
<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<User> {
|
||||
|
||||
<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;
|
||||
@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<User> users;
|
||||
<span class="keyword">public void</span> setUsers(List<User> users) { <span class="keyword">this</span>.users = users; }
|
||||
<span class="keyword">public</span> List<User> getUsers() { <span class="keyword">return</span> users; }
|
||||
|
||||
<span class="comment">// 页面表单 - users[0].username, users[1].username</span>
|
||||
<span class="keyword"><input</span> name=<span class="string">"users[0].username"</span> value=<span class="string">"张三"</span>/>
|
||||
<span class="keyword"><input</span> name=<span class="string">"users[1].username"</span> value=<span class="string">"李四"</span>/>
|
||||
|
||||
<span class="comment">// Action - 接收 Map</span>
|
||||
<span class="keyword">private</span> Map<String, User> userMap;
|
||||
<span class="keyword"><input</span> name=<span class="string">"userMap['one'].username"</span>/></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="../../userFormPage.action">打开用户表单实验</a>
|
||||
<a class="btn" href="../../index.action">返回门户</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -3,99 +3,47 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>文件上传 - Struts2 学习</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上传实验指南</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>
|
||||
<div class="shell">
|
||||
<div class="eyebrow">指南</div>
|
||||
<h1>上传实验怎么讲</h1>
|
||||
<p>现在这条上传链路故意做成“只采集元数据,不真实落盘”的安全版,更适合本地和 VPS 演示环境。</p>
|
||||
|
||||
<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 {
|
||||
<h2>核心绑定字段</h2>
|
||||
<ul>
|
||||
<li><code>File upload</code> 负责接收主文件对象。</li>
|
||||
<li><code>String uploadFileName</code> 负责接收原始文件名。</li>
|
||||
<li><code>String uploadContentType</code> 负责接收 MIME 类型。</li>
|
||||
<li>多个文件时还可以再接文件列表。</li>
|
||||
</ul>
|
||||
|
||||
<span class="comment">// 文件对象</span>
|
||||
<span class="keyword">private</span> File upload;
|
||||
<pre>public class FileUploadAction extends ActionSupport {
|
||||
private File upload;
|
||||
private String uploadFileName;
|
||||
private String uploadContentType;
|
||||
|
||||
<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;
|
||||
public String execute() {
|
||||
// demo keeps metadata only
|
||||
return 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"><%@ taglib prefix="s" uri="/struts-tags" %></span>
|
||||
<span class="keyword"><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">></span>
|
||||
<span class="keyword"><s:file</span> name=<span class="string">"upload"</span> label=<span class="string">"选择文件"</span>/>
|
||||
<span class="keyword"><s:submit</span> value=<span class="string">"上传"</span>/>
|
||||
<span class="keyword"></s:form></span></pre>
|
||||
</div>
|
||||
|
||||
<h2>3. 多文件上传</h2>
|
||||
<div class="section">
|
||||
<pre><span class="comment">// 使用 List 接收多文件</span>
|
||||
<span class="keyword">private</span> List<File> uploads;
|
||||
<span class="keyword">private</span> List<String> uploadsFileName;
|
||||
|
||||
<span class="comment">// 表单</span>
|
||||
<span class="keyword"><s:file</span> name=<span class="string">"uploads"</span> multiple=<span class="string">"multiple"</span>/></pre>
|
||||
</div>
|
||||
|
||||
<h2>4. 限制文件大小和类型</h2>
|
||||
<div class="section">
|
||||
<pre><span class="comment">// struts.xml 配置</span>
|
||||
<span class="keyword"><action</span> name=<span class="string">"upload"</span> class=<span class="string">"com.demo.UploadAction"</span><span class="keyword">></span>
|
||||
<span class="keyword"><interceptor-ref</span> name=<span class="string">"fileUpload"</span><span class="keyword">></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"maximumSize"</span><span class="keyword">></span>10485760<span class="keyword"></param></span><span class="comment"><!-- 10MB --></span>
|
||||
<span class="keyword"><param</span> name=<span class="string">"allowedTypes"</span><span class="keyword">></span>image/png,image/jpeg<span class="keyword"></param></span>
|
||||
<span class="keyword"></interceptor-ref></span>
|
||||
<span class="keyword"><result></span>/upload/success.jsp<span class="keyword"></result></span>
|
||||
<span class="keyword"></action></span></pre>
|
||||
</div>
|
||||
|
||||
<a href="/demo/ajax" class="btn">下一节:AJAX →</a>
|
||||
<div class="links">
|
||||
<a class="btn" href="../../uploadPage.action">打开上传实验</a>
|
||||
<a class="btn" href="../../index.action">返回门户</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -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>
|
||||
47
web/error/500.jsp
Normal file
47
web/error/500.jsp
Normal 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>
|
||||
121
web/hello.jsp
121
web/hello.jsp
@@ -1,47 +1,124 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<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 学习实验台</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
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);
|
||||
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 {
|
||||
color: #667eea;
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
margin: 12px 0;
|
||||
font-size: 42px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
p { color: #666; font-size: 1.2em; }
|
||||
.btn {
|
||||
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: 30px;
|
||||
padding: 14px 40px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
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;
|
||||
border-radius: 30px;
|
||||
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>这个页面用最短路径演示经典的 request -> action -> result 流程。你传入一个 name 参数,Action 处理后再交给 JSP 结果页渲染。</p>
|
||||
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<h3>刚刚发生了什么</h3>
|
||||
<p><s:property value="tip" default="The action populated a few view properties before returning SUCCESS."/></p>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>建议下一步</h3>
|
||||
<p><s:property value="nextStep" default="Open the login flow next."/></p>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>再试一个请求</h3>
|
||||
<p>你可以换一个查询参数,观察问候语如何跟着变化。</p>
|
||||
<code><s:property value="requestSample" default="/hello?name=Platform%20Team"/></code>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h3>当前输入</h3>
|
||||
<p>name 参数值:<strong><s:property value="name" default="World"/></strong></p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="hello.action?name=Platform%20Team">用示例参数再运行一次</a>
|
||||
<a class="btn btn-soft" href="loginPage.action">进入登录实验</a>
|
||||
<a class="btn btn-soft" href="index.action">返回门户</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
554
web/index.jsp
554
web/index.jsp
@@ -1,552 +1,4 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Struts2 全栈学习平台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--primary: #667eea;
|
||||
--secondary: #764ba2;
|
||||
--success: #11998e;
|
||||
--warning: #f093fb;
|
||||
--danger: #f5576c;
|
||||
--dark: #2d3748;
|
||||
--light: #f7fafc;
|
||||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--dark);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
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;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
.hero p {
|
||||
font-size: 1.3em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.feature-list li {
|
||||
padding: 8px 0;
|
||||
padding-left: 25px;
|
||||
position: relative;
|
||||
}
|
||||
.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 {
|
||||
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; }
|
||||
}
|
||||
</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>
|
||||
<h3 style="margin: 15px 0;">struts.xml 基础配置</h3>
|
||||
<div class="code-preview">
|
||||
<span class="comment"><?xml version="1.0"?></span>
|
||||
<span class="keyword"><struts></span>
|
||||
<span class="comment"><!-- 开发模式 --></span>
|
||||
<span class="keyword"><constant</span> name=<span class="string">"struts.devMode"</span> value=<span class="string">"true"</span><span class="keyword">/></span>
|
||||
|
||||
<span class="comment"><!-- 包定义 --></span>
|
||||
<span class="keyword"><package</span> name=<span class="string">"default"</span> namespace=<span class="string">"/"</span> extends=<span class="string">"struts-default"</span><span class="keyword">></span>
|
||||
|
||||
<span class="keyword"><action</span> name=<span class="string">"hello"</span> class=<span class="string">"com.demo.HelloAction"</span><span class="keyword">></span>
|
||||
<span class="keyword"><result>/hello.jsp</result></span>
|
||||
<span class="keyword"></action></span>
|
||||
|
||||
<span class="keyword"></package></span>
|
||||
<span class="keyword"></struts></span>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Struts2 版本: 2.5.30 | Tomcat 9 | JDK 17</p>
|
||||
<p>本学习平台包含完整的代码示例和配置文件</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<%
|
||||
response.sendRedirect(request.getContextPath() + "/index.action");
|
||||
%>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>文件上传 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
h2 { color: #4facfe; margin-bottom: 20px; }
|
||||
.info {
|
||||
background: #fff3e0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #4facfe;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background: #00c6fb; }
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>操作成功</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { color: #11998e; }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 12px 30px;
|
||||
background: #11998e;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 25px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>✅ 操作成功!</h1>
|
||||
<a href="../../index.jsp" class="btn">返回首页</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>用户表单 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
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%;
|
||||
padding: 14px;
|
||||
background: #f5576c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,94 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>用户登录 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-box {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
|
||||
width: 400px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
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;
|
||||
}
|
||||
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>
|
||||
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<s:password name="password" placeholder="请输入密码" showPassword="true"/>
|
||||
<s:fielderror fieldName="password"/>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>登录成功 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 15px 50px rgba(0,0,0,0.3);
|
||||
}
|
||||
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;
|
||||
text-decoration: none;
|
||||
border-radius: 25px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,103 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>表单验证 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
input:focus, textarea:focus { border-color: #f5576c; outline: none; }
|
||||
.error { color: #e53935; font-size: 0.85em; margin-top: 5px; }
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #f5576c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
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;
|
||||
}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,54 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>验证成功 - Struts2 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
min-width: 400px;
|
||||
}
|
||||
h1 { color: #11998e; margin-bottom: 20px; }
|
||||
.data {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.data p { margin: 10px 0; }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background: #11998e;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 25px;
|
||||
}
|
||||
</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>
|
||||
<a href="form.jsp" class="btn">继续测试</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user