Files
springboot-demo/target/classes/static/events.html
2026-03-23 13:05:44 +08:00

360 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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");
}
async function loadInfo() {
await renderRequest("/aop/event");
}
window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage();
</script>
</body>
</html>