Files
struts2-demo/web/index.jsp
2026-03-19 13:53:49 +08:00

515 lines
21 KiB
Plaintext

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Struts2 Demo Lab</title>
<style>
:root {
--bg: #f4f6fb;
--panel: rgba(255,255,255,0.94);
--line: #d7e0ea;
--text: #122033;
--muted: #5c6f84;
--brand: #1464c7;
--brand-2: #f68b1f;
--soft: #e8f2ff;
--shadow: 0 20px 44px rgba(18, 32, 51, 0.14);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top right, rgba(20, 100, 199, 0.15), transparent 28%),
radial-gradient(circle at bottom left, rgba(246, 139, 31, 0.12), transparent 24%),
var(--bg);
}
a { color: inherit; text-decoration: none; }
.shell { max-width: 1480px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero {
padding: 28px;
margin-bottom: 18px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 800;
color: var(--brand);
}
.hero-head {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-start;
}
.hero h1 {
margin: 10px 0 12px;
font-size: 42px;
line-height: 1.1;
}
.hero p {
margin: 0;
color: var(--muted);
line-height: 1.9;
max-width: 860px;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
}
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: white; }
.btn-soft { background: var(--soft); color: var(--brand); }
.metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 20px;
}
.metric {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.56);
}
.metric span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; }
.metric strong { font-size: 26px; }
.layout {
display: grid;
grid-template-columns: 340px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
.card { padding: 22px; }
.search {
display: flex;
gap: 10px;
margin-top: 14px;
}
.search input {
flex: 1;
border: 1px solid var(--line);
border-radius: 16px;
padding: 13px 14px;
font: inherit;
background: transparent;
color: var(--text);
outline: none;
}
.helper-list {
margin: 14px 0 0;
padding-left: 18px;
color: var(--muted);
line-height: 1.9;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.demo-card {
border: 1px solid var(--line);
border-radius: 22px;
padding: 18px;
background: rgba(255,255,255,0.45);
transition: transform 0.18s ease, border-color 0.18s ease;
}
.demo-card:hover {
transform: translateY(-3px);
border-color: rgba(20, 100, 199, 0.28);
}
.demo-card h3 {
margin: 10px 0 8px;
font-size: 20px;
}
.demo-card p {
margin: 0 0 14px;
color: var(--muted);
line-height: 1.8;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.tag {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: var(--soft);
color: var(--brand);
font-size: 12px;
font-weight: 700;
}
.demo-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.link-btn {
display: inline-flex;
padding: 9px 14px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--text);
background: white;
font-size: 13px;
font-weight: 700;
}
.pipeline {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.step {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.52);
}
.step strong { display: block; margin-bottom: 8px; }
.step p { margin: 0; color: var(--muted); line-height: 1.75; }
.empty {
padding: 14px;
border: 1px dashed var(--line);
border-radius: 16px;
color: var(--muted);
text-align: center;
}
.tracker {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 14px;
}
.tracker-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.5);
}
.tracker-item input {
margin-top: 3px;
}
.tracker-item strong {
display: block;
margin-bottom: 4px;
}
@media (max-width: 1100px) {
.layout { grid-template-columns: 1fr; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
.shell { padding: 14px; }
.hero-head, .search { flex-direction: column; align-items: stretch; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell">
<section class="hero">
<div class="hero-head">
<div>
<div class="eyebrow">Struts2 Demo Lab</div>
<h1>Turn scattered examples into a guided demo portal</h1>
<p>This workspace now acts like a mini learning application instead of a pile of sample pages. You can jump into action basics, login flow, validation, uploads, and JSON demos from one place.</p>
</div>
<div class="hero-actions">
<a class="btn" href="hello?name=Platform%20Team">Run hello action</a>
<a class="btn-soft" href="user/login.jsp">Open login flow</a>
</div>
</div>
<div class="metrics">
<div class="metric"><span>Live action routes</span><strong>5</strong></div>
<div class="metric"><span>Guide pages</span><strong>4</strong></div>
<div class="metric"><span>Demo forms</span><strong>3</strong></div>
<div class="metric"><span>JSON endpoints</span><strong>2</strong></div>
<div class="metric"><span>Core lab track</span><strong>4</strong></div>
<div class="metric"><span>Completed labs</span><strong id="completedLabs">0</strong></div>
</div>
</section>
<div class="layout">
<aside class="sidebar">
<section class="card">
<div class="eyebrow">Quick search</div>
<h2>Filter demos by keyword</h2>
<div class="search">
<input id="searchInput" type="text" placeholder="Try login, validation, upload, json, or hello" />
</div>
<ul class="helper-list">
<li>Use the action demos to see real Struts requests and results.</li>
<li>Use the guide pages to compare implementation patterns.</li>
<li>Use the JSON routes to explain API-style actions in class.</li>
</ul>
</section>
<section class="card">
<div class="eyebrow">Fast routes</div>
<h2>Best places to start</h2>
<div class="demo-links">
<a class="link-btn" href="user/form.jsp">User form</a>
<a class="link-btn" href="validation/form.jsp">Validation form</a>
<a class="link-btn" href="upload/index.jsp">Upload flow</a>
<a class="link-btn" href="api/users">JSON endpoint</a>
</div>
</section>
<section class="card">
<div class="eyebrow">Lab tracker</div>
<h2>Mark the demos you have finished</h2>
<div class="tracker">
<label class="tracker-item">
<input type="checkbox" data-track="hello">
<span>
<strong>Hello action</strong>
See the smallest request -> action -> result path.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="login">
<span>
<strong>Login flow</strong>
Follow validation, action errors, and success routing.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="validation">
<span>
<strong>Validation flow</strong>
Compare invalid and valid submissions.
</span>
</label>
<label class="tracker-item">
<input type="checkbox" data-track="json">
<span>
<strong>JSON and AJAX</strong>
Finish by explaining how Struts can return API-style payloads.
</span>
</label>
</div>
</section>
</aside>
<main class="content">
<section class="card">
<div class="eyebrow">Request lifecycle</div>
<h2>How to explain Struts2 with one mental model</h2>
<div class="pipeline">
<div class="step">
<strong>1. Request enters</strong>
<p>The browser sends a URL and optional form fields or query params.</p>
</div>
<div class="step">
<strong>2. Action mapping</strong>
<p><code>struts.xml</code> resolves the action name and target class.</p>
</div>
<div class="step">
<strong>3. Parameter binding</strong>
<p>Struts populates action properties before <code>execute()</code> runs.</p>
</div>
<div class="step">
<strong>4. Result routing</strong>
<p>The returned result name decides which JSP or JSON renderer will respond.</p>
</div>
</div>
</section>
<section class="card">
<div class="eyebrow">Catalog</div>
<h2>Interactive demos and guides</h2>
<div class="grid" id="demoGrid">
<article class="demo-card" data-keywords="hello action request parameter basics">
<div class="tag-list">
<span class="tag">Action</span>
<span class="tag">Beginner</span>
</div>
<h3>Hello action</h3>
<p>Run a real Struts action, inject a request parameter, and inspect the rendered result page.</p>
<div class="demo-links">
<a class="link-btn" href="hello?name=Platform%20Team">Run demo</a>
<a class="link-btn" href="demo/hello/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="login session form validation credentials">
<div class="tag-list">
<span class="tag">Form</span>
<span class="tag">Core flow</span>
</div>
<h3>Login flow</h3>
<p>Use demo credentials, see field validation, and land on a richer post-login dashboard.</p>
<div class="demo-links">
<a class="link-btn" href="user/login.jsp">Open login</a>
</div>
</article>
<article class="demo-card" data-keywords="user form submit profile">
<div class="tag-list">
<span class="tag">Action</span>
<span class="tag">Form submit</span>
</div>
<h3>User intake form</h3>
<p>Submit a small profile payload and see action-backed validation with a success summary.</p>
<div class="demo-links">
<a class="link-btn" href="user/form.jsp">Open form</a>
</div>
</article>
<article class="demo-card" data-keywords="validation field errors age email bio">
<div class="tag-list">
<span class="tag">Validation</span>
<span class="tag">Teaching</span>
</div>
<h3>Validation demo</h3>
<p>Test length, email, age, and text limits while keeping the page readable for explanation.</p>
<div class="demo-links">
<a class="link-btn" href="validation/form.jsp">Open validation</a>
</div>
</article>
<article class="demo-card" data-keywords="upload multipart metadata file">
<div class="tag-list">
<span class="tag">Upload</span>
<span class="tag">Safe demo</span>
</div>
<h3>Upload metadata demo</h3>
<p>Capture file metadata through Struts form binding without writing anything to disk.</p>
<div class="demo-links">
<a class="link-btn" href="upload/index.jsp">Open upload</a>
<a class="link-btn" href="demo/upload/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="ajax json api rest users">
<div class="tag-list">
<span class="tag">JSON</span>
<span class="tag">API style</span>
</div>
<h3>AJAX and REST payloads</h3>
<p>Show how Struts2 can return JSON from actions for AJAX examples and lightweight REST-style demos.</p>
<div class="demo-links">
<a class="link-btn" href="ajax">Action JSON</a>
<a class="link-btn" href="api/users">REST JSON</a>
<a class="link-btn" href="demo/ajax/index.jsp">Read guide</a>
</div>
</article>
<article class="demo-card" data-keywords="model driven user bean object binding">
<div class="tag-list">
<span class="tag">Guide</span>
<span class="tag">Patterns</span>
</div>
<h3>Model binding guide</h3>
<p>Compare property-driven and model-driven input binding to explain how Struts builds action state.</p>
<div class="demo-links">
<a class="link-btn" href="demo/model/index.jsp">Open guide</a>
</div>
</article>
</div>
<div class="empty" id="emptyState" style="display: none; margin-top: 16px;">No demo matched the current search.</div>
</section>
<section class="card">
<div class="eyebrow">Learning pipeline</div>
<h2>Suggested order for the full demo</h2>
<div class="pipeline">
<div class="step">
<strong>1. Start with hello</strong>
<p>See the smallest action and result mapping first.</p>
</div>
<div class="step">
<strong>2. Move to forms</strong>
<p>Login and user intake show parameter binding in action.</p>
</div>
<div class="step">
<strong>3. Validate inputs</strong>
<p>Explain field errors, business rules, and success pages.</p>
</div>
<div class="step">
<strong>4. Expand to JSON</strong>
<p>Close the session with AJAX and REST-style payload demos.</p>
</div>
</div>
</section>
</main>
</div>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const cards = Array.from(document.querySelectorAll('.demo-card'));
const emptyState = document.getElementById('emptyState');
const trackerItems = Array.from(document.querySelectorAll('[data-track]'));
const trackerKey = 'struts_demo_tracker';
function getTrackerState() {
return JSON.parse(localStorage.getItem(trackerKey) || '{}');
}
function renderTracker() {
const state = getTrackerState();
let completed = 0;
trackerItems.forEach((item) => {
item.checked = Boolean(state[item.dataset.track]);
if (item.checked) {
completed += 1;
}
});
document.getElementById('completedLabs').textContent = completed;
}
trackerItems.forEach((item) => {
item.addEventListener('change', () => {
const state = getTrackerState();
state[item.dataset.track] = item.checked;
localStorage.setItem(trackerKey, JSON.stringify(state));
renderTracker();
});
});
searchInput.addEventListener('input', () => {
const keyword = searchInput.value.trim().toLowerCase();
let visible = 0;
cards.forEach((card) => {
const match = !keyword || card.dataset.keywords.includes(keyword);
card.style.display = match ? 'block' : 'none';
if (match) {
visible += 1;
}
});
emptyState.style.display = visible ? 'none' : 'block';
});
renderTracker();
</script>
</body>
</html>