diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js
index d195cc5..7526304 100644
--- a/resources/js/chat-room.js
+++ b/resources/js/chat-room.js
@@ -2,6 +2,7 @@
// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。
export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
+export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
export { bindChatBanner } from "./chat-room/banner.js";
export { bindGlobalDialogControls } from "./chat-room/dialog.js";
export { bindDailySignInControls } from "./chat-room/daily-sign-in.js";
@@ -79,6 +80,7 @@ export {
export { createMessageQueue } from "./chat-room/message-queue.js";
import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
+import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
import { bindChatBanner } from "./chat-room/banner.js";
import { bindGlobalDialogControls } from "./chat-room/dialog.js";
import { bindDailySignInControls } from "./chat-room/daily-sign-in.js";
@@ -160,6 +162,8 @@ if (typeof window !== "undefined") {
window.ChatRoomTools = {
escapeHtml,
escapeHtmlWithLineBreaks,
+ bindAppointmentAnnouncementControls,
+ showAppointmentBanner,
bindChatBanner,
bindGlobalDialogControls,
bindDailySignInControls,
@@ -273,6 +277,7 @@ if (typeof window !== "undefined") {
// 页面加载后立即注册事件委托,具体业务逻辑仍由各子模块负责。
bindChatBanner();
+ bindAppointmentAnnouncementControls();
bindGlobalDialogControls();
bindDailySignInControls();
bindChatFontSizeControl();
diff --git a/resources/js/chat-room/appointment-announcement.js b/resources/js/chat-room/appointment-announcement.js
new file mode 100644
index 0000000..dbd4c7c
--- /dev/null
+++ b/resources/js/chat-room/appointment-announcement.js
@@ -0,0 +1,181 @@
+// 聊天室任命/撤销公告监听,负责渲染大卡片和频道内系统提示。
+
+import { escapeHtml } from "./html.js";
+
+const APPOINTMENT_PHRASES = [
+ "望再接再厉,大展宏图,为大家服务!",
+ "期待在任期间带领大家更上一层楼!",
+ "众望所归,任重道远,加油!",
+ "新官上任,一展风采,前程似锦!",
+ "相信你能胜任,期待你的精彩表现!",
+];
+
+const REVOKE_PHRASES = [
+ "感谢在任期间的辛勤付出,辛苦了!",
+ "江湖路长,愿前程似锦,未来可期!",
+ "感谢您为大家的奉献,一路顺风!",
+ "在任一场,情谊长存,感谢付出!",
+ "相信以后还有更多精彩,继续加油!",
+];
+
+/**
+ * 获取当前聊天消息时间。
+ *
+ * @returns {string}
+ */
+function getChatTimeString() {
+ const now = new Date();
+
+ return [
+ now.getHours().toString().padStart(2, "0"),
+ now.getMinutes().toString().padStart(2, "0"),
+ now.getSeconds().toString().padStart(2, "0"),
+ ].join(":");
+}
+
+/**
+ * 从候选文案中随机挑选一条公告补充语。
+ *
+ * @param {string[]} phrases
+ * @returns {string}
+ */
+function pickAnnouncementPhrase(phrases) {
+ return phrases[Math.floor(Math.random() * phrases.length)];
+}
+
+/**
+ * 显示任命或撤销的大卡片公告。
+ *
+ * @param {object} data
+ * @returns {void}
+ */
+export function showAppointmentBanner(data) {
+ if (!window.chatBanner) {
+ return;
+ }
+
+ const department = data.department_name ? `${escapeHtml(data.department_name)} · ` : "";
+ const isRevoke = data.type === "revoke";
+
+ if (isRevoke) {
+ window.chatBanner.show({
+ id: "appointment-banner",
+ icon: "📋",
+ title: "职务撤销",
+ name: `${escapeHtml(data.position_icon)} ${escapeHtml(data.target_username)}`,
+ body: `${department}${escapeHtml(data.position_name)} 职务已被撤销`,
+ sub: `由 ${escapeHtml(data.operator_name)} 执行`,
+ gradient: ["#374151", "#4b5563", "#6b7280"],
+ titleColor: "#d1d5db",
+ autoClose: 4500,
+ });
+ return;
+ }
+
+ window.chatBanner.show({
+ id: "appointment-banner",
+ icon: "🎊🎖️🎊",
+ title: "任命公告",
+ name: `${escapeHtml(data.position_icon)} ${escapeHtml(data.target_username)}`,
+ body: `荣任 ${department}${escapeHtml(data.position_name)}`,
+ sub: `由 ${escapeHtml(data.operator_name)} 任命`,
+ gradient: ["#4f46e5", "#7c3aed", "#db2777"],
+ titleColor: "#fde68a",
+ autoClose: 4500,
+ });
+}
+
+/**
+ * 创建任命/撤销频道内系统消息节点。
+ *
+ * @param {object} data
+ * @param {boolean} isRevoke
+ * @param {string} department
+ * @param {string} phrase
+ * @param {string} timeString
+ * @returns {HTMLDivElement}
+ */
+function createAppointmentMessage(data, isRevoke, department, phrase, timeString) {
+ const message = document.createElement("div");
+ message.className = "msg-line";
+
+ if (isRevoke) {
+ message.style.cssText = "background:#f3f4f6; border-left:3px solid #9ca3af; border-radius:4px; padding:4px 10px; margin:2px 0;";
+ message.innerHTML = `
+ 📋
+ ${escapeHtml(data.target_username)} 的 ${escapeHtml(data.position_icon)} ${department}${escapeHtml(data.position_name)} 职务已被 ${escapeHtml(data.operator_name)} 撤销。${phrase}
+ (${timeString})
+ `;
+ return message;
+ }
+
+ message.style.cssText = "background:#f5f3ff; border-left:3px solid #7c3aed; border-radius:4px; padding:4px 10px; margin:2px 0;";
+ message.innerHTML = `
+ 🎖️
+ 恭喜 ${escapeHtml(data.target_username)} 荣任 ${escapeHtml(data.position_icon)} ${department}${escapeHtml(data.position_name)},由 ${escapeHtml(data.operator_name)} 任命。${phrase}
+ (${timeString})
+ `;
+
+ return message;
+}
+
+/**
+ * 将系统消息追加到指定聊天窗格并滚动到底部。
+ *
+ * @param {HTMLElement|null} container
+ * @param {HTMLDivElement} message
+ * @returns {void}
+ */
+function appendAppointmentMessage(container, message) {
+ if (!container) {
+ return;
+ }
+
+ container.appendChild(message);
+ container.scrollTop = container.scrollHeight;
+}
+
+/**
+ * 处理任命/撤销公告事件。
+ *
+ * @param {CustomEvent} event
+ * @returns {void}
+ */
+function handleAppointmentAnnouncement(event) {
+ const data = event.detail || {};
+ const isRevoke = data.type === "revoke";
+ const department = data.department_name ? `${escapeHtml(data.department_name)} · ` : "";
+
+ // 任命公告才播放礼花,撤销只显示灰色公告卡片。
+ if (!isRevoke) {
+ window.EffectManager?.play?.("fireworks");
+ }
+
+ showAppointmentBanner(data);
+
+ const phrase = pickAnnouncementPhrase(isRevoke ? REVOKE_PHRASES : APPOINTMENT_PHRASES);
+ const myName = window.chatContext?.username ?? "";
+ const isInvolved = myName === data.operator_name || myName === data.target_username;
+ const targetContainer = isInvolved
+ ? document.getElementById("chat-messages-container2")
+ : document.getElementById("chat-messages-container");
+
+ // 操作者和被操作者写入私聊面板,其余用户写入公屏。
+ appendAppointmentMessage(
+ targetContainer,
+ createAppointmentMessage(data, isRevoke, department, phrase, getChatTimeString()),
+ );
+}
+
+/**
+ * 绑定任命/撤销公告浏览器事件。
+ *
+ * @returns {void}
+ */
+export function bindAppointmentAnnouncementControls() {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.addEventListener("chat:appointment-announced", handleAppointmentAnnouncement);
+}
diff --git a/resources/views/chat/partials/system-events.blade.php b/resources/views/chat/partials/system-events.blade.php
index 77f8a82..e19f23c 100644
--- a/resources/views/chat/partials/system-events.blade.php
+++ b/resources/views/chat/partials/system-events.blade.php
@@ -1,128 +1,6 @@
-
+ 任命/撤销公告监听已迁移到 resources/js/chat-room/appointment-announcement.js,
+ 由 resources/js/chat-room.js 统一通过 Vite 加载并绑定 chat:appointment-announced 事件。
+--}}