feat: finish bilingual learning auth cockpit
This commit is contained in:
@@ -3,251 +3,357 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>事件机制 - Spring Boot</title>
|
||||
<title>Spring Event Lab</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||
h1 { color: #6DB33F; margin: 20px 0; }
|
||||
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||
h3 { color: #6DB33F; margin: 15px 0 10px; }
|
||||
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; margin: 5px; }
|
||||
.btn-primary { background: #6DB33F; color: white; }
|
||||
.btn-primary:hover { background: #5da32f; }
|
||||
.btn-info { background: #17a2b8; color: white; }
|
||||
.btn-warning { background: #ffc107; color: #333; }
|
||||
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
|
||||
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||
pre code { background: none; color: inherit; }
|
||||
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; max-height: 300px; overflow-y: auto; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||
th { background: #6DB33F; color: white; }
|
||||
.event-flow { display: flex; align-items: center; justify-content: center; margin: 20px 0; }
|
||||
.event-flow span { padding: 15px 25px; background: #6DB33F; color: white; border-radius: 8px; margin: 0 10px; }
|
||||
.event-flow .arrow { font-size: 24px; color: #6DB33F; }
|
||||
.nav { margin-bottom: 20px; }
|
||||
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin:15px 0; }
|
||||
.lab h4 { color:#ad6800; margin-bottom:8px; }
|
||||
: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="nav">
|
||||
<a href="/">← 返回首页</a>
|
||||
<a href="/users.html">用户管理</a>
|
||||
<a href="/aop.html">AOP 切面</a>
|
||||
</div>
|
||||
|
||||
<h1>📡 Spring 事件机制</h1>
|
||||
|
||||
<div class="lab">
|
||||
<h4>🧪 实验任务卡(事件)</h4>
|
||||
<label style="display:block;margin-bottom:8px;"><input id="eventTaskDone" type="checkbox" onchange="toggleEventTaskDone(this)"> 本任务我已经完成</label>
|
||||
<ul style="padding-left:20px;line-height:1.8;">
|
||||
<li>目标:体验发布者与监听者解耦</li>
|
||||
<li>步骤1:输入 userId/userName,点击“发布登录事件”</li>
|
||||
<li>步骤2:重复发布不同用户,比较返回结果</li>
|
||||
<li>预期:接口立即返回;监听处理在日志中可观察</li>
|
||||
<li>常见坑:把事件当同步 RPC,忽略异步监听特性</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🎉 事件发布演示</h3>
|
||||
<p>模拟用户登录事件,观察事件发布和监听过程</p>
|
||||
<div style="margin: 15px 0;">
|
||||
<input type="text" id="userName" placeholder="用户名" value="张三" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 150px;">
|
||||
<input type="number" id="userId" placeholder="用户ID" value="1" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100px;">
|
||||
<button class="btn btn-primary" onclick="publishEvent()">发布登录事件</button>
|
||||
<button class="btn btn-warning" onclick="demoEventError()">演示参数错误</button>
|
||||
<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>
|
||||
<div class="result-box" id="eventResult">等待事件发布...</div>
|
||||
</div>
|
||||
|
||||
<h2>🔄 事件机制流程</h2>
|
||||
|
||||
<div class="event-flow">
|
||||
<span>发布者</span>
|
||||
<span class="arrow">→</span>
|
||||
<span>ApplicationEventPublisher</span>
|
||||
<span class="arrow">→</span>
|
||||
<span>事件</span>
|
||||
<span class="arrow">→</span>
|
||||
<span>监听者</span>
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
<strong>核心优势:</strong>
|
||||
<ul style="margin-top: 10px; padding-left: 20px;">
|
||||
<li><strong>解耦:</strong>发布者和监听者互不依赖</li>
|
||||
<li><strong>扩展:</strong>新增监听器无需修改发布者</li>
|
||||
<li><strong>异步:</strong>耗时操作不阻塞主流程</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>💻 代码实现</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>1. 定义事件</h3>
|
||||
<pre><code>public class UserEvent {
|
||||
public enum Type { CREATED, UPDATED, DELETED, LOGIN }
|
||||
|
||||
private Type type;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
// constructor, getters...
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>2. 发布事件</h3>
|
||||
<pre><code>@Component
|
||||
public class UserEventPublisher {
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public void publishUserLogin(Long userId, String userName) {
|
||||
UserEvent event = new UserEvent(
|
||||
UserEvent.Type.LOGIN,
|
||||
userId,
|
||||
userName,
|
||||
"用户登录成功"
|
||||
);
|
||||
eventPublisher.publishEvent(event);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>3. 监听事件</h3>
|
||||
<pre><code>@Component
|
||||
public class UserEventListener {
|
||||
|
||||
// 基础监听
|
||||
@EventListener
|
||||
public void handleUserEvent(UserEvent event) {
|
||||
System.out.println("收到事件: " + event.getType());
|
||||
}
|
||||
|
||||
// 条件监听 - 只处理登录事件
|
||||
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
|
||||
public void handleLogin(UserEvent event) {
|
||||
System.out.println("用户登录: " + event.getUserName());
|
||||
}
|
||||
|
||||
// 异步监听 - 不阻塞主流程
|
||||
@Async
|
||||
@EventListener
|
||||
public void sendWelcomeEmail(UserEvent event) {
|
||||
// 发送邮件...
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>4. 控制器中使用</h3>
|
||||
<pre><code>@RestController
|
||||
@RequestMapping("/aop")
|
||||
public class AopEventController {
|
||||
|
||||
@Autowired
|
||||
private UserEventPublisher eventPublisher;
|
||||
|
||||
@PostMapping("/event/publish")
|
||||
public Map<String, Object> publishEvent(
|
||||
@RequestParam Long userId,
|
||||
@RequestParam String userName) {
|
||||
|
||||
// 发布事件
|
||||
eventPublisher.publishUserLogin(userId, userName);
|
||||
|
||||
return Map.of(
|
||||
"message", "事件已发布",
|
||||
"userId", userId,
|
||||
"userName", userName
|
||||
);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>🎯 应用场景</h2>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>场景</th><th>事件类型</th><th>处理逻辑</th></tr>
|
||||
<tr><td>用户注册</td><td>UserCreatedEvent</td><td>发送欢迎邮件、初始化数据</td></tr>
|
||||
<tr><td>订单创建</td><td>OrderCreatedEvent</td><td>扣库存、发送通知</td></tr>
|
||||
<tr><td>支付成功</td><td>PaymentSuccessEvent</td><td>更新订单状态、发送短信</td></tr>
|
||||
<tr><td>用户登录</td><td>UserLoginEvent</td><td>记录登录日志、更新在线状态</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💡 最佳实践</h3>
|
||||
<ul style="line-height: 2; padding-left: 20px;">
|
||||
<li>事件类应该是<strong>不可变</strong>的(只读属性)</li>
|
||||
<li>使用 <code>@Async</code> 处理耗时操作</li>
|
||||
<li>避免在监听器中抛出异常</li>
|
||||
<li>使用 <code>condition</code> 过滤不需要的事件</li>
|
||||
<li>复杂场景考虑使用消息队列(RabbitMQ/Kafka)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px;"><a href="/">← 返回学习中心</a></p>
|
||||
|
||||
<script>
|
||||
const EVENT_TASK_KEY = 'task.event.done';
|
||||
</section>
|
||||
|
||||
function toggleEventTaskDone(el) {
|
||||
localStorage.setItem(EVENT_TASK_KEY, el.checked ? '1' : '0');
|
||||
<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 initEventTaskState() {
|
||||
const done = localStorage.getItem(EVENT_TASK_KEY) === '1';
|
||||
const checkbox = document.getElementById('eventTaskDone');
|
||||
if (checkbox) checkbox.checked = done;
|
||||
}
|
||||
function pageText() {
|
||||
return I18N[window.learningShell.getLanguage()] || I18N.zh;
|
||||
}
|
||||
|
||||
async function demoEventError() {
|
||||
const resultBox = document.getElementById('eventResult');
|
||||
try {
|
||||
const res = await fetch('/aop/event/publish?userName=', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
resultBox.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
resultBox.textContent = '错误: ' + e.message;
|
||||
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];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function publishEvent() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
const userName = document.getElementById('userName').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/aop/event/publish?userId=${userId}&userName=${encodeURIComponent(userName)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
const resultBox = document.getElementById('eventResult');
|
||||
resultBox.textContent = `[${new Date().toLocaleTimeString()}] 事件已发布\n\n` +
|
||||
JSON.stringify(data, null, 2) + '\n\n' +
|
||||
'📊 查看控制台日志可以看到监听器的输出:\n' +
|
||||
'[EventPublisher] 发布事件: LOGIN - ' + userName + '\n' +
|
||||
'[EventListener] 收到事件: LOGIN - 用户: ' + userName + '\n' +
|
||||
'[LoginTracker] 记录用户登录: ' + userName;
|
||||
} catch (e) {
|
||||
document.getElementById('eventResult').textContent = '错误: ' + e.message;
|
||||
}
|
||||
}
|
||||
function renderLanguage() {
|
||||
const text = pageText();
|
||||
applyTranslations(text);
|
||||
|
||||
initEventTaskState();
|
||||
</script>
|
||||
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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user