Files
springboot-demo/target/classes/static/events.html

360 lines
14 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Event Lab</title>
<style>
:root {
--bg: #fff4ea;
--panel: rgba(255,255,255,0.95);
--line: #eed9c9;
--text: #132238;
--muted: #6a7484;
--brand: #dd6c1f;
--accent: #0f67b5;
--shadow: 0 22px 52px rgba(18, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--text);
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top right, rgba(221, 108, 31, 0.16), transparent 28%),
radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 22%),
var(--bg);
}
.page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
display: inline-flex;
padding: 7px 12px;
border-radius: 999px;
background: rgba(221, 108, 31, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.8; }
.grid {
display: grid;
gap: 18px;
grid-template-columns: 420px minmax(0, 1fr);
}
.card { padding: 22px; }
.list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.item, .timeline-step {
padding: 14px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.46);
}
.toolbar, .actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
border: 0;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
.btn { background: linear-gradient(135deg, var(--brand), #f39a55); color: #fff; }
.btn-soft { background: #edf5ff; color: var(--accent); }
.fields {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 14px;
}
label {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-weight: 700;
color: #22394f;
}
input, select {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: transparent;
color: var(--text);
outline: none;
}
.timeline {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
pre {
margin: 0;
min-height: 280px;
padding: 16px;
border-radius: 18px;
background: #111926;
color: #deebff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
@media (max-width: 1060px) {
.grid, .timeline, .fields { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.page { padding: 14px; }
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="eyebrow" data-i18n="heroBadge"></div>
<h1 data-i18n="heroTitle"></h1>
<p data-i18n="heroText"></p>
<div class="actions" style="margin-top:18px;">
<a class="btn-soft" href="/access.html" data-i18n="loginLink"></a>
<a class="btn-soft" href="/" data-i18n="homeLink"></a>
<a class="btn-soft" href="/aop.html" data-i18n="aopLink"></a>
<a class="btn-soft" href="/users.html" data-i18n="usersLink"></a>
</div>
</section>
<div class="grid">
<aside class="card">
<div class="eyebrow" data-i18n="storyBadge"></div>
<h2 data-i18n="storyTitle"></h2>
<div class="timeline">
<div class="timeline-step"><strong data-i18n="timeline1Title"></strong><p data-i18n="timeline1Text"></p></div>
<div class="timeline-step"><strong data-i18n="timeline2Title"></strong><p data-i18n="timeline2Text"></p></div>
<div class="timeline-step"><strong data-i18n="timeline3Title"></strong><p data-i18n="timeline3Text"></p></div>
<div class="timeline-step"><strong data-i18n="timeline4Title"></strong><p data-i18n="timeline4Text"></p></div>
</div>
<div class="list">
<div class="item">
<strong data-i18n="exp1Title"></strong>
<p data-i18n="exp1Text"></p>
</div>
<div class="item">
<strong data-i18n="exp2Title"></strong>
<p data-i18n="exp2Text"></p>
</div>
<div class="item">
<strong data-i18n="pairingTitle"></strong>
<p data-i18n="pairingText"></p>
</div>
</div>
</aside>
<main class="card">
<div class="eyebrow" data-i18n="liveBadge"></div>
<h2 data-i18n="liveTitle"></h2>
<p data-i18n="liveText"></p>
<div class="fields">
<label>
<span data-i18n="eventTypeLabel"></span>
<select id="eventType">
<option value="LOGIN">LOGIN</option>
<option value="CREATED">CREATED</option>
<option value="UPDATED">UPDATED</option>
<option value="DELETED">DELETED</option>
</select>
</label>
<label>
<span data-i18n="userIdLabel"></span>
<input id="userId" value="21">
</label>
<label>
<span data-i18n="userNameLabel"></span>
<input id="userName" value="observer-demo">
</label>
</div>
<div class="toolbar" style="margin-top:14px;">
<button class="btn" type="button" onclick="publishEvent()" data-i18n="publishButton"></button>
<button class="btn-soft" type="button" onclick="loadHistory()" data-i18n="historyButton"></button>
<button class="btn-soft" type="button" onclick="loadInfo()" data-i18n="notesButton"></button>
</div>
<div class="list" style="margin-top:18px;">
<div class="item">
<strong data-i18n="hintTitle"></strong>
<p data-i18n="hintText"></p>
</div>
</div>
<pre id="resultBox"></pre>
</main>
</div>
</div>
<script src="/learning-shell.js"></script>
<script>
const I18N = {
zh: {
title: "Spring 事件实验",
heroBadge: "事件实验",
heroTitle: "通过可视化时间线理解发布者与监听器的解耦。",
heroText: "这个页面把平时隐藏起来的行为展现出来:请求可以先返回,而监听器还会在后台继续处理。你可以连续发布多种事件,然后再查看共享事件历史。",
loginLink: "打开登录实验",
homeLink: "返回首页",
aopLink: "打开 AOP 实验",
usersLink: "打开用户实验",
storyBadge: "事件故事线",
storyTitle: "应该重点观察什么",
timeline1Title: "Controller",
timeline1Text: "接收事件发布请求。",
timeline2Title: "Publisher",
timeline2Text: "组装 UserEvent 并发出事件。",
timeline3Title: "Listeners",
timeline3Text: "记录历史并执行异步后续工作。",
timeline4Title: "History API",
timeline4Text: "帮助你回看刚刚到底发生了什么。",
exp1Title: "实验 1",
exp1Text: "用不同用户连续发布两次 LOGIN确认历史记录会增长但控制器代码无需改动。",
exp2Title: "实验 2",
exp2Text: "发布 CREATED 再刷新历史,观察同一个入口如何支持多种监听路径。",
pairingTitle: "代码联读建议",
pairingText: "把 UserEventPublisher 和 UserEventListener 放在一起读,会更容易看出解耦边界。",
liveBadge: "实时事件台",
liveTitle: "发布事件并查看历史",
liveText: "通过这个表单发布事件,再刷新历史,观察类型、用户、说明和时间戳。",
eventTypeLabel: "事件类型",
userIdLabel: "用户 ID",
userNameLabel: "用户名",
publishButton: "发布事件",
historyButton: "加载历史",
notesButton: "加载事件说明",
hintTitle: "理解提示",
hintText: "如果控制器已经返回,但历史还在继续增长,你看到的就是解耦后的后续行为。",
loadingPrefix: "正在加载 ",
requestFailedPrefix: "请求失败:",
placeholder: "先发布一个事件,或直接加载历史查看实时输出。"
},
en: {
title: "Spring Event Lab",
heroBadge: "Event Lab",
heroTitle: "Understand publisher-listener decoupling through a visible event timeline.",
heroText: "This page surfaces what normally stays hidden: a request can return quickly while listeners keep reacting in the background. Publish multiple event types and then inspect shared event history.",
loginLink: "Open login lab",
homeLink: "Back home",
aopLink: "Open AOP lab",
usersLink: "Open user lab",
storyBadge: "Event Story",
storyTitle: "What to watch for",
timeline1Title: "Controller",
timeline1Text: "Receives the publish request.",
timeline2Title: "Publisher",
timeline2Text: "Builds a UserEvent and emits it.",
timeline3Title: "Listeners",
timeline3Text: "Track history and optional async work.",
timeline4Title: "History API",
timeline4Text: "Lets you inspect what actually happened.",
exp1Title: "Experiment 1",
exp1Text: "Publish LOGIN twice with different users and confirm the history list grows without changing controller code.",
exp2Title: "Experiment 2",
exp2Text: "Publish CREATED and then refresh history to see the same endpoint support multiple listener paths.",
pairingTitle: "Code pairing tip",
pairingText: "Read UserEventPublisher and UserEventListener side by side to see the decoupling boundary.",
liveBadge: "Live Event Console",
liveTitle: "Publish events and inspect history",
liveText: "Use the form to publish events, then reload history to inspect type, user, detail, and timestamp.",
eventTypeLabel: "Event type",
userIdLabel: "User id",
userNameLabel: "User name",
publishButton: "Publish event",
historyButton: "Load history",
notesButton: "Load event notes",
hintTitle: "Interpretation hint",
hintText: "If the controller returns immediately but history keeps growing, you are seeing decoupled follow-up behavior in action.",
loadingPrefix: "Loading ",
requestFailedPrefix: "Request failed: ",
placeholder: "Publish an event or load history to inspect live output."
}
};
function pageText() {
return I18N[window.learningShell.getLanguage()] || I18N.zh;
}
function applyTranslations(text) {
document.title = text.title;
document.querySelectorAll("[data-i18n]").forEach(function (element) {
const key = element.getAttribute("data-i18n");
if (Object.prototype.hasOwnProperty.call(text, key)) {
element.textContent = text[key];
}
});
}
function renderLanguage() {
const text = pageText();
applyTranslations(text);
const box = document.getElementById("resultBox");
if (!box.dataset.state || box.dataset.state === "idle") {
box.textContent = text.placeholder;
box.dataset.state = "idle";
}
}
async function renderRequest(path, options) {
const box = document.getElementById("resultBox");
const text = pageText();
box.textContent = text.loadingPrefix + path + " ...";
box.dataset.state = "loading";
try {
const data = await window.learningShell.requestJson(path, options || {});
box.textContent = JSON.stringify(data, null, 2);
box.dataset.state = "live";
} catch (error) {
box.textContent = text.requestFailedPrefix + window.learningShell.describeError(error);
box.dataset.state = "live";
}
}
async function publishEvent() {
const type = document.getElementById("eventType").value;
const userId = document.getElementById("userId").value.trim() || "21";
const userName = document.getElementById("userName").value.trim() || "observer-demo";
const params = new URLSearchParams({ userId: userId, userName: userName, eventType: type });
await renderRequest("/aop/event/publish?" + params.toString(), { method: "POST" });
}
async function loadHistory() {
await renderRequest("/aop/event/history");
}
2026-03-18 15:18:30 +08:00
async function loadInfo() {
await renderRequest("/aop/event");
}
2026-03-18 15:18:30 +08:00
window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage();
</script>
</body>
</html>