diff --git a/package.json b/package.json
index 6490fa2..26bd975 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@kne-components/components-admin",
- "version": "1.1.34",
+ "version": "1.1.35",
"description": "用于实现一个后台管理系统的必要组件",
"scripts": {
"init": "husky",
diff --git a/prompts/README.md b/prompts/README.md
new file mode 100644
index 0000000..979f126
--- /dev/null
+++ b/prompts/README.md
@@ -0,0 +1,173 @@
+# Prompts 文档集合
+
+本项目包含多个 AI prompts 文档,用于指导生成前端组件库相关的代码模块、文档和示例。
+
+---
+
+## 文档集合列表
+
+### 1. BizUnit 使用指南
+
+**功能**: 生成基于 BizUnit 架构模式的完整前端业务模块
+
+**适用场景**:
+- 需要生成包含完整 CRUD 功能的前端业务模块
+- 需要国际化支持和可复用组件结构
+- 生成符合规范的目录结构和文档示例
+
+**核心内容**:
+- 模块目录结构规范(List、Detail、FormInner、TabDetail、Actions 等组件)
+- 核心组件实现规范(根组件、列表页、表单组件、详情页、Tab 详情页)
+- 国际化文件规范(中英文语言包)
+- API 集成规范
+- 文档示例规范
+
+**所属集合**: `prompts-remote-components/`
+
+---
+
+### 2. RemoteLoader 使用指南
+
+**功能**: 远程模块加载库的使用指南,基于 Webpack 5 Module Federation
+
+**适用场景**:
+- 构建微前端架构
+- 需要在运行时动态加载远程模块
+- 多团队独立开发部署模块的场景
+
+**核心内容**:
+- 四种使用方式:RemoteLoader 组件、withRemoteLoader HOC、useLoader Hook、createWithRemoteLoader
+- 模块标记格式详解
+- API 参考(preset、loadModule、safeLoadModule、parseToken 等)
+- 缓存机制
+- 错误处理和调试
+- 性能优化
+
+**所属集合**: `prompts-remote-components/`
+
+---
+
+### 3. FormInfo 使用指南
+
+**功能**: 基于 React 和 Ant Design 的企业级表单组件库
+
+**适用场景**:
+- 构建复杂的表单页面
+- 需要表单验证、动态字段、弹窗/抽屉表单
+- 分步表单向导
+
+**核心内容**:
+- 核心组件:Form、FormInfo、SubmitButton、CancelButton
+- 字段类型:Input、TextArea、Select、DatePicker、Upload 等
+- 校验规则配置
+- 列表组件:List(卡片式)、TableList(表格)
+- 弹窗与抽屉:FormModal、FormDrawer
+- 分步表单:FormSteps、FormStepModal
+- 表单 Hook:useFormModal、useFormDrawer、useFormStepModal
+- 国际化支持
+
+**所属集合**: 根目录、`prompts-remote-components/`(内容相同)
+
+---
+
+### 4. 国际化
+
+**功能**: 指导组件完成国际化改造
+
+**适用场景**:
+- 需要为组件添加多语言支持
+- 创建语言包和国际化上下文
+- 修改组件以支持国际化
+
+**核心内容**:
+- 国际化文件创建(withLocale.js、locale/zh-CN.js、locale/en-US.js)
+- 组件修改模式(主组件、FormInner、getColumns、Action)
+- useIntl Hook 和 withLocale HOC 使用方式
+- createWithRemoteLoader 组件的国际化包裹规范
+- 语言包 key 命名规范
+- 检查要点清单
+
+**所属集合**: `prompts-remote-components/`
+
+---
+
+### 5. 生成文档
+
+**功能**: 根据代码实现自动生成项目文档(summary.md 和 api.md)
+
+**适用场景**:
+- 需要为组件生成规范化的项目概述文档
+- 需要生成 API 属性表格文档
+- 组件开发完成后需要补充文档
+
+**核心内容**:
+- 项目概述文档(doc/summary.md)格式规范
+- API 文档(doc/api.md)格式规范
+- 文档生成流程(分析代码结构、提取 API 信息)
+- 格式约束(标题级别、表格格式、无示例代码)
+
+**所属集合**: `prompts-remote-components/`
+
+---
+
+### 6. 组件示例编写提示词
+
+**功能**: 指导编写规范的组件示例代码和配置
+
+**适用场景**:
+- 为组件编写可运行的示例代码
+- 配置 example.json 示例配置文件
+- 编写覆盖 API 的完整示例
+
+**核心内容**:
+- 文件结构规范(doc/ 目录、子组件示例规则)
+- example.json 配置结构
+- 示例代码规范(scope 依赖声明、导入方式)
+- 示例内容设计原则(API 覆盖率、真实业务场景、数据真实性)
+- FormInfo 组件示例特殊规则
+- Mock 数据规范和使用方式
+- 示例完整性检查清单
+
+**所属集合**: 根目录、`prompts-remote-components/`(内容略有差异)
+
+---
+
+### 7. BizUnit 业务模块生成提示词
+
+**功能**: 快速生成业务模块 action 的代码模板
+
+**适用场景**:
+- 需要快速生成业务表单操作按钮
+- 创建新建、编辑类业务组件
+
+**核心内容**:
+- 组件命名规范(大写字母开头英文名称)
+- 代码模板结构
+- 成功提示语配置
+- 表单弹窗集成
+
+**所属集合**: 根目录
+
+---
+
+## 快速选择指南
+
+| 需求 | 推荐文档 | 所属集合 |
+|------|---------|---------|
+| 生成完整的业务模块(列表+表单+详情) | BizUnit 使用指南 | prompts-remote-components/ |
+| 加载远程组件/微前端 | RemoteLoader 使用指南 | prompts-remote-components/ |
+| 构建表单页面(验证、动态字段、弹窗) | FormInfo 使用指南 | 两者都有 |
+| 为组件添加多语言支持 | 国际化 | prompts-remote-components/ |
+| 为组件生成项目概述和 API 文档 | 生成文档 | prompts-remote-components/ |
+| 编写组件示例代码和配置 | 组件示例编写提示词 | 两者都有 |
+| 快速生成业务 Action 组件 | BizUnit 业务模块生成提示词 | 根目录 |
+
+---
+
+## 版本信息
+
+```json
+{
+ "@kne/prompts-remote-components": "1.0.2"
+}
+```
diff --git a/src/components/Apis/getApis.js b/src/components/Apis/getApis.js
index aefb7c6..8c99da5 100644
--- a/src/components/Apis/getApis.js
+++ b/src/components/Apis/getApis.js
@@ -481,6 +481,58 @@ const getApis = options => {
method: 'POST'
}
}
+ },
+ mq: {
+ message: {
+ publish: {
+ url: `${prefix}/mq/message/publish`,
+ method: 'POST'
+ },
+ list: {
+ url: `${prefix}/mq/message/list`,
+ method: 'GET'
+ }
+ },
+ deadLetter: {
+ list: {
+ url: `${prefix}/mq/dlq/list`,
+ method: 'GET'
+ },
+ replay: {
+ url: `${prefix}/mq/dlq/replay`,
+ method: 'POST'
+ }
+ },
+ trace: {
+ list: {
+ url: `${prefix}/mq/trace/list`,
+ method: 'GET'
+ },
+ detail: {
+ url: `${prefix}/mq/trace/detail`,
+ method: 'GET'
+ }
+ },
+ dashboard: {
+ getData: {
+ url: `${prefix}/mq/dashboard`,
+ method: 'GET'
+ },
+ sse: {
+ url: `${prefix}/mq/dashboard/sse`,
+ method: 'GET'
+ }
+ },
+ queue: {
+ depth: {
+ url: `${prefix}/mq/queue/depth`,
+ method: 'GET'
+ },
+ cleanup: {
+ url: `${prefix}/mq/queue/cleanup`,
+ method: 'POST'
+ }
+ }
}
};
};
diff --git a/src/components/MessageQueue/Actions/DeadLetterActions.js b/src/components/MessageQueue/Actions/DeadLetterActions.js
new file mode 100644
index 0000000..839d341
--- /dev/null
+++ b/src/components/MessageQueue/Actions/DeadLetterActions.js
@@ -0,0 +1,42 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+import DeadLetterReplay from './DeadLetterReplay';
+import MessageDetail from './MessageDetail';
+
+const DeadLetterActionsInner = createWithRemoteLoader({
+ modules: ['components-core:ButtonGroup']
+})(({ remoteModules, children, data, onSuccess, moreType = 'link', itemClassName, ...props }) => {
+ const [ButtonGroup] = remoteModules;
+ const { formatMessage } = useIntl();
+ const list = [];
+
+ if (data && !data.replayed) {
+ list.push({
+ ...props,
+ buttonComponent: DeadLetterReplay,
+ data,
+ children: formatMessage({ id: 'Replay' }),
+ onSuccess
+ });
+ }
+
+ if (data) {
+ list.push({
+ ...props,
+ buttonComponent: MessageDetail,
+ data,
+ title: formatMessage({ id: 'DeadLetterList' }),
+ children: formatMessage({ id: 'ViewDetail' })
+ });
+ }
+
+ if (typeof children === 'function') {
+ return children({ list });
+ }
+
+ return ;
+});
+
+const DeadLetterActions = withLocale(DeadLetterActionsInner);
+export default DeadLetterActions;
diff --git a/src/components/MessageQueue/Actions/DeadLetterReplay.js b/src/components/MessageQueue/Actions/DeadLetterReplay.js
new file mode 100644
index 0000000..47d10c4
--- /dev/null
+++ b/src/components/MessageQueue/Actions/DeadLetterReplay.js
@@ -0,0 +1,43 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { App } from 'antd';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+
+const DeadLetterReplay = createWithRemoteLoader({
+ modules: ['components-core:Global@usePreset', 'components-core:ConfirmButton']
+})(
+ withLocale(({ remoteModules, data, ids, onSuccess, message: propsMessage, ...props }) => {
+ const [usePreset, ConfirmButton] = remoteModules;
+ const { ajax, apis } = usePreset();
+ const { message } = App.useApp();
+ const { formatMessage } = useIntl();
+ const replayIds = ids || (data?.id ? [data.id] : []);
+
+ if (data?.replayed) {
+ return null;
+ }
+
+ return (
+ 1 ? formatMessage({ id: 'BatchReplayConfirm' }) : formatMessage({ id: 'ReplayMessageConfirm' }))}
+ onClick={async () => {
+ const { data: resData } = await ajax(
+ Object.assign({}, apis.mq.deadLetter.replay, {
+ data: replayIds.length > 1 ? { ids: replayIds } : { id: replayIds[0] }
+ })
+ );
+ if (resData.code !== 0) {
+ return;
+ }
+ message.success(formatMessage({ id: 'ReplaySuccess' }));
+ onSuccess && onSuccess();
+ }}
+ />
+ );
+ })
+);
+
+export default DeadLetterReplay;
diff --git a/src/components/MessageQueue/Actions/MessageDetail.js b/src/components/MessageQueue/Actions/MessageDetail.js
new file mode 100644
index 0000000..2ddfb8f
--- /dev/null
+++ b/src/components/MessageQueue/Actions/MessageDetail.js
@@ -0,0 +1,48 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { Button, Descriptions } from 'antd';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+import JsonView from '@kne/json-view';
+import '@kne/json-view/dist/index.css';
+
+const MessageDetail = createWithRemoteLoader({
+ modules: ['components-core:Modal@useModal']
+})(
+ withLocale(({ remoteModules, data, title, ...props }) => {
+ const [useModal] = remoteModules;
+ const modal = useModal();
+ const { formatMessage } = useIntl();
+
+ return (
+
+
+ }
+ children={
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `${formatRate(value)}/s` },
+ { title: formatMessage({ id: 'FailureRate' }), dataIndex: 'failureRate', render: value => `${formatRate(value)}/s` },
+ { title: formatMessage({ id: 'SuccessRatio' }), dataIndex: 'successRatio', render: formatPercent }
+ ]}
+ />
+ >
+ }
+ />
+ );
+});
+
+const Dashboard = createWithRemoteLoader({
+ modules: ['components-core:Global@usePreset', 'components-core:Layout@Page']
+})(
+ withLocale(({ remoteModules, baseUrl, pageProps = {} }) => {
+ const [usePreset, Page] = remoteModules;
+ const { apis } = usePreset();
+
+ return (
+ {
+ return ;
+ }}
+ />
+ );
+ })
+);
+
+export default Dashboard;
diff --git a/src/components/MessageQueue/DeadLetterList/index.js b/src/components/MessageQueue/DeadLetterList/index.js
new file mode 100644
index 0000000..26829a9
--- /dev/null
+++ b/src/components/MessageQueue/DeadLetterList/index.js
@@ -0,0 +1,120 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { useRef, useState } from 'react';
+import { Space } from 'antd';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+
+import getDeadLetterColumns from '../getDeadLetterColumns';
+import DeadLetterActions from '../Actions/DeadLetterActions';
+import DeadLetterReplay from '../Actions/DeadLetterReplay';
+import Menu from '../Menu';
+import { buildListParams } from '../utils';
+
+const DeadLetterList = createWithRemoteLoader({
+ modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter']
+})(
+ withLocale(({ remoteModules, baseUrl, pageProps = {} }) => {
+ const [TablePage, usePreset, Filter] = remoteModules;
+ const { formatMessage } = useIntl();
+ const { apis } = usePreset();
+ const { SearchInput, getFilterValue, fields: filterFields } = Filter;
+ const { InputFilterItem, AdvancedSelectFilterItem } = filterFields;
+ const ref = useRef(null);
+ const [filter, setFilter] = useState([]);
+ const [selected, setSelected] = useState({
+ selectedRowKeys: [],
+ selectedRows: []
+ });
+ const filterValue = getFilterValue(filter);
+
+ return (
+ {
+ return {
+ children: (
+ {
+ ref.current?.reload?.();
+ }}
+ />
+ )
+ };
+ }
+ }
+ ]}
+ rowSelection={{
+ type: 'checkbox',
+ selectedRowKeys: selected.selectedRowKeys,
+ onChange: (selectedRowKeys, selectedRows) => {
+ setSelected({ selectedRowKeys, selectedRows });
+ },
+ getCheckboxProps: record => {
+ return {
+ disabled: record.replayed
+ };
+ }
+ }}
+ topArea={
+
+ {formatMessage({ id: 'SelectedCount' }, { count: selected.selectedRowKeys.length })}
+ {
+ setSelected({ selectedRowKeys: [], selectedRows: [] });
+ ref.current?.reload?.();
+ }}>
+ {formatMessage({ id: 'BatchReplay' })}
+
+
+ }
+ page={{
+ filter: {
+ value: filter,
+ onChange: setFilter,
+ list: [
+ [
+ ,
+ ({
+ pageData: [
+ { label: formatMessage({ id: 'Yes' }), value: true },
+ { label: formatMessage({ id: 'No' }), value: false }
+ ]
+ })
+ }}
+ />
+ ]
+ ]
+ },
+ titleExtra: ,
+ menu: ,
+ ...pageProps
+ }}
+ />
+ );
+ })
+);
+
+export default DeadLetterList;
diff --git a/src/components/MessageQueue/Menu.js b/src/components/MessageQueue/Menu.js
new file mode 100644
index 0000000..b33c421
--- /dev/null
+++ b/src/components/MessageQueue/Menu.js
@@ -0,0 +1,27 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import withLocale from './withLocale';
+import { useIntl } from '@kne/react-intl';
+
+const Menu = createWithRemoteLoader({
+ modules: ['components-core:Menu']
+})(
+ withLocale(({ remoteModules, baseUrl }) => {
+ const [Menu] = remoteModules;
+ const { formatMessage } = useIntl();
+ const rootPath = baseUrl || '';
+
+ return (
+
+ );
+ })
+);
+
+export default Menu;
diff --git a/src/components/MessageQueue/MessageList/index.js b/src/components/MessageQueue/MessageList/index.js
new file mode 100644
index 0000000..df5891a
--- /dev/null
+++ b/src/components/MessageQueue/MessageList/index.js
@@ -0,0 +1,98 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Space } from 'antd';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+
+import getColumns from '../getColumns';
+import Actions from '../Actions';
+import Menu from '../Menu';
+import PublishMessage from '../PublishMessage';
+import { buildListParams } from '../utils';
+
+const MessageList = createWithRemoteLoader({
+ modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter', 'components-core:Enum']
+})(
+ withLocale(({ remoteModules, baseUrl, pageProps = {} }) => {
+ const [TablePage, usePreset, Filter, Enum] = remoteModules;
+ const { formatMessage } = useIntl();
+ const { apis } = usePreset();
+ const { SearchInput, getFilterValue, fields: filterFields } = Filter;
+ const { InputFilterItem, SuperSelectFilterItem } = filterFields;
+ const navigate = useNavigate();
+ const ref = useRef(null);
+ const [filter, setFilter] = useState([]);
+ const filterValue = getFilterValue(filter);
+
+ return (
+ {
+ return {
+ children: (
+ {
+ navigate(`${baseUrl}/traces?messageId=${encodeURIComponent(item.messageId || item.id)}`);
+ }}
+ onSuccess={() => {
+ ref.current?.reload?.();
+ }}
+ />
+ )
+ };
+ }
+ }
+ ]}
+ page={{
+ filter: {
+ value: filter,
+ onChange: setFilter,
+ list: [
+ [
+ ,
+ ,
+ {
+ return (
+
+ {options => children({ options })}
+
+ );
+ }}
+ />
+ ]
+ ]
+ },
+ titleExtra: (
+
+
+ ref.current?.reload?.()} />
+
+ ),
+ menu: ,
+ ...pageProps
+ }}
+ />
+ );
+ })
+);
+
+export default MessageList;
diff --git a/src/components/MessageQueue/PublishMessage/index.js b/src/components/MessageQueue/PublishMessage/index.js
new file mode 100644
index 0000000..bc674c2
--- /dev/null
+++ b/src/components/MessageQueue/PublishMessage/index.js
@@ -0,0 +1,108 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { App, Button } from 'antd';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+import { parseIsoDateTimeInput, parseJsonInput, parseNumberInput } from '../utils';
+
+const PublishMessage = createWithRemoteLoader({
+ modules: [
+ 'components-core:FormInfo',
+ 'components-core:FormInfo@useFormModal',
+ 'components-core:Global@usePreset',
+ 'components-thirdparty:JSONEditor'
+ ]
+})(
+ withLocale(({ remoteModules, onSuccess, ...props }) => {
+ const [FormInfo, useFormModal, usePreset, JSONEditor] = remoteModules;
+ const { Input, InputNumber,DatePicker } = FormInfo.fields;
+ const formModal = useFormModal();
+ const { ajax, apis } = usePreset();
+ const { message } = App.useApp();
+ const { formatMessage } = useIntl();
+
+ return (
+ {
+ const modalApi = formModal({
+ title: formatMessage({ id: 'PublishMessage' }),
+ size: 'small',
+ formProps: {
+ data: {
+ priority: 0,
+ maxRetries: 3,
+ payload: '{}',
+ meta: '{}'
+ },
+ onSubmit: async formData => {
+ let payload;
+ let meta;
+ let priority;
+ let maxRetries;
+ let executeAt;
+ try {
+ payload = parseJsonInput(formData.payload, 'payload');
+ meta = parseJsonInput(formData.meta, 'meta');
+ priority = parseNumberInput(formData.priority, 'priority', 0);
+ maxRetries = parseNumberInput(formData.maxRetries, 'maxRetries', 3);
+ executeAt = parseIsoDateTimeInput(formData.executeAt, 'executeAt');
+ } catch (error) {
+ message.error(error.message);
+ return false;
+ }
+
+ const requestData = {
+ topic: formData.topic,
+ payload,
+ priority,
+ maxRetries
+ };
+
+ if (executeAt) {
+ requestData.executeAt = executeAt;
+ }
+ if (formData.traceId) {
+ requestData.traceId = formData.traceId;
+ }
+ if (meta && Object.keys(meta).length > 0) {
+ requestData.meta = meta;
+ }
+
+ const { data: resData } = await ajax(
+ Object.assign({}, apis.mq.message.publish, {
+ data: requestData
+ })
+ );
+
+ if (resData.code !== 0) {
+ return false;
+ }
+ message.success(formatMessage({ id: 'PublishSuccess' }));
+ onSuccess && onSuccess();
+ modalApi.close();
+ }
+ },
+ children: (
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+
+ ]}
+ />
+ )
+ });
+ }}>
+ {formatMessage({ id: 'PublishMessage' })}
+
+ );
+ })
+);
+
+export default PublishMessage;
diff --git a/src/components/MessageQueue/QueueTools/index.js b/src/components/MessageQueue/QueueTools/index.js
new file mode 100644
index 0000000..bc7b53f
--- /dev/null
+++ b/src/components/MessageQueue/QueueTools/index.js
@@ -0,0 +1,92 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { App, Button, Card, Input, Select, Space, Statistic } from 'antd';
+import { useState } from 'react';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+import Menu from '../Menu';
+
+const QueueTools = createWithRemoteLoader({
+ modules: ['components-core:Global@usePreset', 'components-core:Layout@Page', 'components-core:ConfirmButton']
+})(
+ withLocale(({ remoteModules, baseUrl, pageProps = {} }) => {
+ const [usePreset, Page, ConfirmButton] = remoteModules;
+ const { ajax, apis } = usePreset();
+ const { message } = App.useApp();
+ const { formatMessage } = useIntl();
+ const [topic, setTopic] = useState('');
+ const [depth, setDepth] = useState(null);
+ const [cleanupStatus, setCleanupStatus] = useState('COMPLETED');
+ const [olderThan, setOlderThan] = useState('');
+
+ const queryDepth = async () => {
+ const { data: resData } = await ajax(
+ Object.assign({}, apis.mq.queue.depth, {
+ params: topic ? { topic } : {}
+ })
+ );
+ if (resData.code !== 0) {
+ return;
+ }
+ setDepth(resData.data?.depth || 0);
+ };
+
+ return (
+ }
+ children={
+
+
+
+ setTopic(event.target.value)} placeholder={formatMessage({ id: 'TopicPlaceholder' })} style={{ width: 280 }} />
+
+ {formatMessage({ id: 'QueryDepth' })}
+
+
+ {depth !== null && }
+
+
+
+
+ setOlderThan(event.target.value)} placeholder={`${formatMessage({ id: 'OlderThan' })}: 2026-05-01T00:00:00.000Z`} style={{ width: 320 }} />
+ {
+ const { data: resData } = await ajax(
+ Object.assign({}, apis.mq.queue.cleanup, {
+ data: Object.assign(
+ {
+ status: cleanupStatus
+ },
+ olderThan ? { olderThan } : {}
+ )
+ })
+ );
+ if (resData.code !== 0) {
+ return;
+ }
+ message.success(formatMessage({ id: 'CleanupSuccess' }));
+ }}>
+ {formatMessage({ id: 'CleanupMessages' })}
+
+
+
+
+ }
+ />
+ );
+ })
+);
+
+export default QueueTools;
diff --git a/src/components/MessageQueue/README.md b/src/components/MessageQueue/README.md
new file mode 100644
index 0000000..3425034
--- /dev/null
+++ b/src/components/MessageQueue/README.md
@@ -0,0 +1,232 @@
+# MessageQueue
+
+### 概述
+
+MessageQueue 是面向 `fastify-mq` 的消息队列管理端组件,提供队列运行概览、消息发布与查询、死信处理、轨迹追踪和队列维护工具。
+
+组件直接对齐 `fastify-mq` 的接口契约,列表筛选使用后端支持的扁平查询参数,死信支持单条与批量重放,Dashboard 展示队列深度、消费速率、失败率、死信速率和成功率等关键指标。
+
+适用于需要在后台管理系统中观察异步消息处理状态、排查失败消息、追踪消息生命周期,以及执行基础队列运维操作的业务场景。
+
+
+### 示例(全屏)
+
+#### 示例代码
+
+- 基础用法
+- 完整展示 MessageQueue 的仪表盘、消息列表、死信队列、轨迹追踪和队列工具页。
+- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),reactRouterDom(react-router-dom),antd(antd)
+
+```jsx
+const { default: MessageQueue } = _MessageQueue;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { useNavigate, Navigate, Route, Routes } = reactRouterDom;
+const { Button, Flex } = antd;
+
+const BaseExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+ navigate('/MessageQueue/mq')}>仪表盘
+ navigate('/MessageQueue/mq/messages')}>消息列表
+ navigate('/MessageQueue/mq/dead-letter')}>死信队列
+ navigate('/MessageQueue/mq/traces')}>轨迹追踪
+ navigate('/MessageQueue/mq/tools')}>队列工具
+
+
+ }
+ />
+ } />
+
+
+
+ );
+});
+
+render();
+
+```
+
+- 消息详情
+- 展示独立使用消息操作按钮查看消息详情。
+- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd)
+
+```jsx
+const { Actions } = _MessageQueue;
+const { default: mockPreset, messageQueueList } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space } = antd;
+
+const MessageDetailExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const data = messageQueueList.pageData[0];
+ return (
+
+
+
+ {data.topic}
+
+
+
+
+ );
+});
+
+render();
+
+```
+
+- 死信重放
+- 展示独立使用死信操作按钮提交重放。
+- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd)
+
+```jsx
+const { DeadLetterActions } = _MessageQueue;
+const { default: mockPreset, deadLetterList } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space } = antd;
+
+const DeadLetterReplayExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const data = deadLetterList.pageData.find(item => !item.replayed);
+ return (
+
+
+
+ {data.topic}
+
+
+
+
+ );
+});
+
+render();
+
+```
+
+- 枚举值
+- 展示消息状态和轨迹事件枚举。
+- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd)
+
+```jsx
+const { enums } = _MessageQueue;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space, Tag } = antd;
+
+const EnumsExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const getColor = type => ({ success: 'green', danger: 'red', progress: 'blue', info: 'default' })[type] || 'default';
+ return (
+
+
+
+
+ {enums.messageStatus.map(item => (
+ {item.value} - {item.description}
+ ))}
+
+
+
+
+ {enums.traceEvent.map(item => (
+ {item.value} - {item.description}
+ ))}
+
+
+
+
+ );
+});
+
+render();
+
+```
+
+### API
+
+### MessageQueue
+
+消息队列管理主组件,内部包含 Dashboard、消息列表、死信列表、轨迹列表和队列工具页。
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| baseUrl | 组件挂载的基础路由,例如 `/MessageQueue/mq` | string | - |
+| pageProps | 传给内部页面组件的布局配置 | object | `{}` |
+| children | 自定义顶部导航或附加内容 | ReactNode | - |
+
+### 子组件
+
+| 导出名 | 说明 |
+| --- | --- |
+| Dashboard | 队列指标概览页 |
+| MessageList | 消息列表页,支持发布消息和查看详情 |
+| DeadLetterList | 死信列表页,支持单条和批量重放 |
+| TraceList | 消息轨迹列表页 |
+| QueueTools | 队列深度查询和消息清理工具页 |
+| PublishMessage | 发布消息按钮组件 |
+| Actions | 消息列表操作按钮 |
+| DeadLetterActions | 死信列表操作按钮 |
+
+### Actions
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| data | 消息记录 | object | - |
+| onTrace | 点击查看轨迹时触发 | `(data) => void` | - |
+| onSuccess | 操作成功后的回调 | `() => void` | - |
+| type | 按钮类型 | string | `default` |
+
+### DeadLetterActions
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| data | 死信记录 | object | - |
+| onSuccess | 重放成功后的回调 | `() => void` | - |
+| type | 按钮类型 | string | `default` |
+
+### PublishMessage
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| onSuccess | 发布成功后的回调 | `() => void` | - |
+
+### 枚举
+
+| 枚举名 | 值 |
+| --- | --- |
+| messageStatus | `PENDING`、`PROCESSING`、`COMPLETED`、`FAILED` |
+| traceEvent | `PUBLISHED`、`PROCESSING`、`COMPLETED`、`FAILED`、`MOVED_TO_DLQ`、`REPLAYED`、`LOCK_RECOVERED` |
+| mqBoolean | `true`、`false` |
+
+### fastify-mq 接口
+
+| 功能 | 方法 | 路径 | 参数 |
+| --- | --- | --- | --- |
+| 发布消息 | POST | `/mq/message/publish` | `topic`、`payload`、`priority`、`executeAt`、`maxRetries`、`traceId`、`meta` |
+| 消息列表 | GET | `/mq/message/list` | `topic`、`status`、`traceId`、`perPage`、`currentPage` |
+| 死信列表 | GET | `/mq/dlq/list` | `topic`、`replayed`、`perPage`、`currentPage` |
+| 重放死信 | POST | `/mq/dlq/replay` | `id` 或 `ids` |
+| 轨迹列表 | GET | `/mq/trace/list` | `topic`、`messageId`、`event`、`perPage`、`currentPage` |
+| 轨迹详情 | GET | `/mq/trace/detail` | `traceId` |
+| Dashboard | GET | `/mq/dashboard` | `window`、`step` |
+| 队列深度 | GET | `/mq/queue/depth` | `topic` |
+| 清理消息 | POST | `/mq/queue/cleanup` | `status`、`olderThan` |
diff --git a/src/components/MessageQueue/TraceList/index.js b/src/components/MessageQueue/TraceList/index.js
new file mode 100644
index 0000000..fc291de
--- /dev/null
+++ b/src/components/MessageQueue/TraceList/index.js
@@ -0,0 +1,91 @@
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { useRef, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import withLocale from '../withLocale';
+import { useIntl } from '@kne/react-intl';
+
+import getTraceColumns from '../getTraceColumns';
+import MessageDetail from '../Actions/MessageDetail';
+import Menu from '../Menu';
+import { buildListParams } from '../utils';
+
+const TraceList = createWithRemoteLoader({
+ modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter', 'components-core:Enum']
+})(
+ withLocale(({ remoteModules, baseUrl, pageProps = {} }) => {
+ const [TablePage, usePreset, Filter, Enum] = remoteModules;
+ const { formatMessage } = useIntl();
+ const { apis } = usePreset();
+ const { SearchInput, getFilterValue, fields: filterFields } = Filter;
+ const { InputFilterItem, SuperSelectFilterItem } = filterFields;
+ const [searchParams] = useSearchParams();
+ const initialMessageId = searchParams.get('messageId');
+ const ref = useRef(null);
+ const [filter, setFilter] = useState(
+ initialMessageId
+ ? [
+ {
+ name: 'messageId',
+ label: formatMessage({ id: 'MessageId' }),
+ value: initialMessageId
+ }
+ ]
+ : []
+ );
+ const filterValue = getFilterValue(filter);
+
+ return (
+ {
+ return {
+ children: {formatMessage({ id: 'ViewDetail' })}
+ };
+ }
+ }
+ ]}
+ page={{
+ filter: {
+ value: filter,
+ onChange: setFilter,
+ list: [
+ [
+ ,
+ ,
+ {
+ return (
+
+ {options => children({ options })}
+
+ );
+ }}
+ />
+ ]
+ ]
+ },
+ titleExtra: ,
+ menu: ,
+ ...pageProps
+ }}
+ />
+ );
+ })
+);
+
+export default TraceList;
diff --git a/src/components/MessageQueue/doc/api.md b/src/components/MessageQueue/doc/api.md
new file mode 100644
index 0000000..1401c70
--- /dev/null
+++ b/src/components/MessageQueue/doc/api.md
@@ -0,0 +1,67 @@
+### MessageQueue
+
+消息队列管理主组件,内部包含 Dashboard、消息列表、死信列表、轨迹列表和队列工具页。
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| baseUrl | 组件挂载的基础路由,例如 `/MessageQueue/mq` | string | - |
+| pageProps | 传给内部页面组件的布局配置 | object | `{}` |
+| children | 自定义顶部导航或附加内容 | ReactNode | - |
+
+### 子组件
+
+| 导出名 | 说明 |
+| --- | --- |
+| Dashboard | 队列指标概览页 |
+| MessageList | 消息列表页,支持发布消息和查看详情 |
+| DeadLetterList | 死信列表页,支持单条和批量重放 |
+| TraceList | 消息轨迹列表页 |
+| QueueTools | 队列深度查询和消息清理工具页 |
+| PublishMessage | 发布消息按钮组件 |
+| Actions | 消息列表操作按钮 |
+| DeadLetterActions | 死信列表操作按钮 |
+
+### Actions
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| data | 消息记录 | object | - |
+| onTrace | 点击查看轨迹时触发 | `(data) => void` | - |
+| onSuccess | 操作成功后的回调 | `() => void` | - |
+| type | 按钮类型 | string | `default` |
+
+### DeadLetterActions
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| data | 死信记录 | object | - |
+| onSuccess | 重放成功后的回调 | `() => void` | - |
+| type | 按钮类型 | string | `default` |
+
+### PublishMessage
+
+| 属性名 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| onSuccess | 发布成功后的回调 | `() => void` | - |
+
+### 枚举
+
+| 枚举名 | 值 |
+| --- | --- |
+| messageStatus | `PENDING`、`PROCESSING`、`COMPLETED`、`FAILED` |
+| traceEvent | `PUBLISHED`、`PROCESSING`、`COMPLETED`、`FAILED`、`MOVED_TO_DLQ`、`REPLAYED`、`LOCK_RECOVERED` |
+| mqBoolean | `true`、`false` |
+
+### fastify-mq 接口
+
+| 功能 | 方法 | 路径 | 参数 |
+| --- | --- | --- | --- |
+| 发布消息 | POST | `/mq/message/publish` | `topic`、`payload`、`priority`、`executeAt`、`maxRetries`、`traceId`、`meta` |
+| 消息列表 | GET | `/mq/message/list` | `topic`、`status`、`traceId`、`perPage`、`currentPage` |
+| 死信列表 | GET | `/mq/dlq/list` | `topic`、`replayed`、`perPage`、`currentPage` |
+| 重放死信 | POST | `/mq/dlq/replay` | `id` 或 `ids` |
+| 轨迹列表 | GET | `/mq/trace/list` | `topic`、`messageId`、`event`、`perPage`、`currentPage` |
+| 轨迹详情 | GET | `/mq/trace/detail` | `traceId` |
+| Dashboard | GET | `/mq/dashboard` | `window`、`step` |
+| 队列深度 | GET | `/mq/queue/depth` | `topic` |
+| 清理消息 | POST | `/mq/queue/cleanup` | `status`、`olderThan` |
diff --git a/src/components/MessageQueue/doc/base.js b/src/components/MessageQueue/doc/base.js
new file mode 100644
index 0000000..f304754
--- /dev/null
+++ b/src/components/MessageQueue/doc/base.js
@@ -0,0 +1,37 @@
+const { default: MessageQueue } = _MessageQueue;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { useNavigate, Navigate, Route, Routes } = reactRouterDom;
+const { Button, Flex } = antd;
+
+const BaseExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+ navigate('/MessageQueue/mq')}>仪表盘
+ navigate('/MessageQueue/mq/messages')}>消息列表
+ navigate('/MessageQueue/mq/dead-letter')}>死信队列
+ navigate('/MessageQueue/mq/traces')}>轨迹追踪
+ navigate('/MessageQueue/mq/tools')}>队列工具
+
+
+ }
+ />
+ } />
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/MessageQueue/doc/dead-letter-replay.js b/src/components/MessageQueue/doc/dead-letter-replay.js
new file mode 100644
index 0000000..43f840b
--- /dev/null
+++ b/src/components/MessageQueue/doc/dead-letter-replay.js
@@ -0,0 +1,23 @@
+const { DeadLetterActions } = _MessageQueue;
+const { default: mockPreset, deadLetterList } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space } = antd;
+
+const DeadLetterReplayExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const data = deadLetterList.pageData.find(item => !item.replayed);
+ return (
+
+
+
+ {data.topic}
+
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/MessageQueue/doc/enums.js b/src/components/MessageQueue/doc/enums.js
new file mode 100644
index 0000000..7d5af9b
--- /dev/null
+++ b/src/components/MessageQueue/doc/enums.js
@@ -0,0 +1,33 @@
+const { enums } = _MessageQueue;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space, Tag } = antd;
+
+const EnumsExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const getColor = type => ({ success: 'green', danger: 'red', progress: 'blue', info: 'default' })[type] || 'default';
+ return (
+
+
+
+
+ {enums.messageStatus.map(item => (
+ {item.value} - {item.description}
+ ))}
+
+
+
+
+ {enums.traceEvent.map(item => (
+ {item.value} - {item.description}
+ ))}
+
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/MessageQueue/doc/example.json b/src/components/MessageQueue/doc/example.json
new file mode 100644
index 0000000..64a22ed
--- /dev/null
+++ b/src/components/MessageQueue/doc/example.json
@@ -0,0 +1,50 @@
+{
+ "isFull": true,
+ "list": [
+ {
+ "title": "基础用法",
+ "description": "完整展示 MessageQueue 的仪表盘、消息列表、死信队列、轨迹追踪和队列工具页。",
+ "code": "./base.js",
+ "scope": [
+ { "name": "_MessageQueue", "packageName": "@components/MessageQueue" },
+ { "name": "_mockPreset", "packageName": "@root/mockPreset" },
+ { "name": "remoteLoader", "packageName": "@kne/remote-loader" },
+ { "name": "reactRouterDom", "packageName": "react-router-dom" },
+ { "name": "antd", "packageName": "antd" }
+ ]
+ },
+ {
+ "title": "消息详情",
+ "description": "展示独立使用消息操作按钮查看消息详情。",
+ "code": "./message-detail.js",
+ "scope": [
+ { "name": "_MessageQueue", "packageName": "@components/MessageQueue" },
+ { "name": "_mockPreset", "packageName": "@root/mockPreset" },
+ { "name": "remoteLoader", "packageName": "@kne/remote-loader" },
+ { "name": "antd", "packageName": "antd" }
+ ]
+ },
+ {
+ "title": "死信重放",
+ "description": "展示独立使用死信操作按钮提交重放。",
+ "code": "./dead-letter-replay.js",
+ "scope": [
+ { "name": "_MessageQueue", "packageName": "@components/MessageQueue" },
+ { "name": "_mockPreset", "packageName": "@root/mockPreset" },
+ { "name": "remoteLoader", "packageName": "@kne/remote-loader" },
+ { "name": "antd", "packageName": "antd" }
+ ]
+ },
+ {
+ "title": "枚举值",
+ "description": "展示消息状态和轨迹事件枚举。",
+ "code": "./enums.js",
+ "scope": [
+ { "name": "_MessageQueue", "packageName": "@components/MessageQueue" },
+ { "name": "_mockPreset", "packageName": "@root/mockPreset" },
+ { "name": "remoteLoader", "packageName": "@kne/remote-loader" },
+ { "name": "antd", "packageName": "antd" }
+ ]
+ }
+ ]
+}
diff --git a/src/components/MessageQueue/doc/message-detail.js b/src/components/MessageQueue/doc/message-detail.js
new file mode 100644
index 0000000..ad80a07
--- /dev/null
+++ b/src/components/MessageQueue/doc/message-detail.js
@@ -0,0 +1,23 @@
+const { Actions } = _MessageQueue;
+const { default: mockPreset, messageQueueList } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Card, Space } = antd;
+
+const MessageDetailExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ const data = messageQueueList.pageData[0];
+ return (
+
+
+
+ {data.topic}
+
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/MessageQueue/doc/style.scss b/src/components/MessageQueue/doc/style.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/MessageQueue/doc/summary.md b/src/components/MessageQueue/doc/summary.md
new file mode 100644
index 0000000..50f92dd
--- /dev/null
+++ b/src/components/MessageQueue/doc/summary.md
@@ -0,0 +1,5 @@
+MessageQueue 是面向 `fastify-mq` 的消息队列管理端组件,提供队列运行概览、消息发布与查询、死信处理、轨迹追踪和队列维护工具。
+
+组件直接对齐 `fastify-mq` 的接口契约,列表筛选使用后端支持的扁平查询参数,死信支持单条与批量重放,Dashboard 展示队列深度、消费速率、失败率、死信速率和成功率等关键指标。
+
+适用于需要在后台管理系统中观察异步消息处理状态、排查失败消息、追踪消息生命周期,以及执行基础队列运维操作的业务场景。
diff --git a/src/components/MessageQueue/enums.js b/src/components/MessageQueue/enums.js
new file mode 100644
index 0000000..124f3f1
--- /dev/null
+++ b/src/components/MessageQueue/enums.js
@@ -0,0 +1,29 @@
+const MESSAGE_STATUS_ENUM = [
+ { value: 'PENDING', description: '等待执行', type: 'info' },
+ { value: 'PROCESSING', description: '处理中', type: 'progress' },
+ { value: 'COMPLETED', description: '已完成', type: 'success' },
+ { value: 'FAILED', description: '失败', type: 'danger' }
+];
+
+const TRACE_EVENT_ENUM = [
+ { value: 'PUBLISHED', description: '消息发布', type: 'info' },
+ { value: 'PROCESSING', description: '开始处理', type: 'progress' },
+ { value: 'COMPLETED', description: '处理完成', type: 'success' },
+ { value: 'FAILED', description: '处理失败', type: 'danger' },
+ { value: 'MOVED_TO_DLQ', description: '进入死信', type: 'danger' },
+ { value: 'REPLAYED', description: '死信重放', type: 'success' },
+ { value: 'LOCK_RECOVERED', description: '锁定恢复', type: 'info' }
+];
+
+const YES_NO_ENUM = [
+ { value: true, description: '是', type: 'success' },
+ { value: false, description: '否', type: 'info' }
+];
+
+const enums = {
+ messageStatus: MESSAGE_STATUS_ENUM,
+ traceEvent: TRACE_EVENT_ENUM,
+ mqBoolean: YES_NO_ENUM
+};
+
+export default enums;
diff --git a/src/components/MessageQueue/getColumns.js b/src/components/MessageQueue/getColumns.js
new file mode 100644
index 0000000..400a418
--- /dev/null
+++ b/src/components/MessageQueue/getColumns.js
@@ -0,0 +1,99 @@
+import withLocale from './withLocale';
+import { useIntl } from '@kne/react-intl';
+import { stringifyJson } from './utils';
+
+const getColumns = ({ formatMessage }) => {
+ return [
+ {
+ name: 'id',
+ title: formatMessage({ id: 'ID' }),
+ type: 'serialNumber'
+ },
+ {
+ name: 'topic',
+ title: formatMessage({ id: 'Topic' }),
+ type: 'tag',
+ ellipsis: true,
+ valueOf: ({ topic }) =>
+ topic && {
+ type: 'info',
+ text: topic
+ }
+ },
+ {
+ name: 'status',
+ title: formatMessage({ id: 'Status' }),
+ type: 'tag',
+ valueOf: ({ status }) =>
+ status && {
+ isEnum: true,
+ moduleName: 'messageStatus',
+ name: status
+ }
+ },
+ {
+ name: 'payload',
+ title: formatMessage({ id: 'Payload' }),
+ ellipsis: true,
+ valueOf: ({ payload }) => stringifyJson(payload)
+ },
+ {
+ name: 'retryCount',
+ title: formatMessage({ id: 'RetryCount' }),
+ width: 100
+ },
+ {
+ name: 'maxRetries',
+ title: formatMessage({ id: 'MaxRetries' }),
+ width: 100
+ },
+ {
+ name: 'priority',
+ title: formatMessage({ id: 'Priority' }),
+ width: 80
+ },
+ {
+ name: 'traceId',
+ title: formatMessage({ id: 'TraceId' }),
+ ellipsis: true,
+ copyable: true
+ },
+ {
+ name: 'consumerId',
+ title: formatMessage({ id: 'ConsumerId' }),
+ ellipsis: true
+ },
+ {
+ name: 'executeAt',
+ title: formatMessage({ id: 'ExecuteAt' }),
+ type: 'datetime'
+ },
+ {
+ name: 'nextRetryAt',
+ title: formatMessage({ id: 'NextRetryAt' }),
+ type: 'datetime'
+ },
+ {
+ name: 'lockedAt',
+ title: formatMessage({ id: 'LockedAt' }),
+ type: 'datetime'
+ },
+ {
+ name: 'createdAt',
+ title: formatMessage({ id: 'CreatedAt' }),
+ type: 'datetime'
+ },
+ {
+ name: 'updatedAt',
+ title: formatMessage({ id: 'UpdatedAt' }),
+ type: 'datetime'
+ }
+ ];
+};
+
+export const ColumnsLoader = withLocale(({ children }) => {
+ const { formatMessage } = useIntl();
+ return children(props => getColumns(Object.assign({}, props, { formatMessage })));
+});
+
+export default getColumns;
diff --git a/src/components/MessageQueue/getDeadLetterColumns.js b/src/components/MessageQueue/getDeadLetterColumns.js
new file mode 100644
index 0000000..bee61de
--- /dev/null
+++ b/src/components/MessageQueue/getDeadLetterColumns.js
@@ -0,0 +1,69 @@
+import withLocale from './withLocale';
+import { useIntl } from '@kne/react-intl';
+import { stringifyJson } from './utils';
+
+const getDeadLetterColumns = ({ formatMessage }) => {
+ return [
+ {
+ name: 'id',
+ title: formatMessage({ id: 'ID' }),
+ type: 'serialNumber'
+ },
+ {
+ name: 'topic',
+ title: formatMessage({ id: 'Topic' }),
+ type: 'tag',
+ ellipsis: true,
+ valueOf: ({ topic }) =>
+ topic && {
+ type: 'info',
+ text: topic
+ }
+ },
+ {
+ name: 'originalId',
+ title: formatMessage({ id: 'OriginalMessageId' }),
+ ellipsis: true,
+ copyable: true
+ },
+ {
+ name: 'errorMessage',
+ title: formatMessage({ id: 'ErrorMessage' }),
+ ellipsis: true,
+ width: 200
+ },
+ {
+ name: 'payload',
+ title: formatMessage({ id: 'Payload' }),
+ ellipsis: true,
+ valueOf: ({ payload }) => stringifyJson(payload)
+ },
+ {
+ name: 'replayed',
+ title: formatMessage({ id: 'Replayed' }),
+ type: 'tag',
+ valueOf: ({ replayed }) => ({
+ isEnum: true,
+ moduleName: 'mqBoolean',
+ name: !!replayed
+ })
+ },
+ {
+ name: 'replayedAt',
+ title: formatMessage({ id: 'ReplayedAt' }),
+ type: 'datetime'
+ },
+ {
+ name: 'createdAt',
+ title: formatMessage({ id: 'CreatedAt' }),
+ type: 'datetime'
+ }
+ ];
+};
+
+export const DeadLetterColumnsLoader = withLocale(({ children }) => {
+ const { formatMessage } = useIntl();
+ return children(props => getDeadLetterColumns(Object.assign({}, props, { formatMessage })));
+});
+
+export default getDeadLetterColumns;
diff --git a/src/components/MessageQueue/getTraceColumns.js b/src/components/MessageQueue/getTraceColumns.js
new file mode 100644
index 0000000..5b25e37
--- /dev/null
+++ b/src/components/MessageQueue/getTraceColumns.js
@@ -0,0 +1,64 @@
+import withLocale from './withLocale';
+import { useIntl } from '@kne/react-intl';
+import { stringifyJson } from './utils';
+
+const getTraceColumns = ({ formatMessage }) => {
+ return [
+ {
+ name: 'id',
+ title: formatMessage({ id: 'ID' }),
+ type: 'serialNumber'
+ },
+ {
+ name: 'traceId',
+ title: formatMessage({ id: 'TraceId' }),
+ ellipsis: true,
+ copyable: true
+ },
+ {
+ name: 'topic',
+ title: formatMessage({ id: 'Topic' }),
+ type: 'tag',
+ valueOf: ({ topic }) =>
+ topic && {
+ type: 'info',
+ text: topic
+ }
+ },
+ {
+ name: 'event',
+ title: formatMessage({ id: 'Event' }),
+ type: 'tag',
+ valueOf: ({ event }) => ({
+ isEnum: true,
+ moduleName: 'traceEvent',
+ name: event
+ })
+ },
+ {
+ name: 'messageId',
+ title: formatMessage({ id: 'MessageId' }),
+ ellipsis: true,
+ copyable: true
+ },
+ {
+ name: 'detail',
+ title: formatMessage({ id: 'Detail' }),
+ ellipsis: true,
+ width: 200,
+ valueOf: ({ detail }) => stringifyJson(detail)
+ },
+ {
+ name: 'createdAt',
+ title: formatMessage({ id: 'CreatedAt' }),
+ type: 'datetime'
+ }
+ ];
+};
+
+export const TraceColumnsLoader = withLocale(({ children }) => {
+ const { formatMessage } = useIntl();
+ return children(props => getTraceColumns(Object.assign({}, props, { formatMessage })));
+});
+
+export default getTraceColumns;
diff --git a/src/components/MessageQueue/index.js b/src/components/MessageQueue/index.js
new file mode 100644
index 0000000..565d9fb
--- /dev/null
+++ b/src/components/MessageQueue/index.js
@@ -0,0 +1,49 @@
+import AppChildrenRouter from '@kne/app-children-router';
+import Dashboard from './Dashboard';
+import MessageList from './MessageList';
+import DeadLetterList from './DeadLetterList';
+import TraceList from './TraceList';
+import QueueTools from './QueueTools';
+
+const MessageQueue = ({ baseUrl, children, ...props }) => {
+ return (
+
+ },
+ {
+ path: 'messages',
+ element:
+ },
+ {
+ path: 'dead-letter',
+ element:
+ },
+ {
+ path: 'traces',
+ element:
+ },
+ {
+ path: 'tools',
+ element:
+ }
+ ]}>
+ {children}
+
+ );
+};
+
+export default MessageQueue;
+
+export { default as enums } from './enums';
+export { default as getColumns, ColumnsLoader } from './getColumns';
+export { default as getDeadLetterColumns, DeadLetterColumnsLoader } from './getDeadLetterColumns';
+export { default as getTraceColumns, TraceColumnsLoader } from './getTraceColumns';
+export { default as Actions } from './Actions';
+export { default as DeadLetterActions } from './Actions/DeadLetterActions';
+export { default as Dashboard } from './Dashboard';
+export { default as PublishMessage } from './PublishMessage';
+export { default as QueueTools } from './QueueTools';
+export { MessageList, DeadLetterList, TraceList };
diff --git a/src/components/MessageQueue/locale/en-US.js b/src/components/MessageQueue/locale/en-US.js
new file mode 100644
index 0000000..06e035f
--- /dev/null
+++ b/src/components/MessageQueue/locale/en-US.js
@@ -0,0 +1,80 @@
+const messages = {
+ ID: 'ID',
+ Topic: 'Topic',
+ Status: 'Status',
+ Payload: 'Payload',
+ Options: 'Options',
+ ConsumerId: 'Consumer',
+ LockedAt: 'Locked At',
+ NextRetryAt: 'Next Retry At',
+ RetryCount: 'Retry Count',
+ MaxRetries: 'Max Retries',
+ Priority: 'Priority',
+ TraceId: 'Trace ID',
+ MessageId: 'Message ID',
+ ExecuteAt: 'Execute At',
+ CreatedAt: 'Created At',
+ UpdatedAt: 'Updated At',
+ CompletedAt: 'Completed At',
+ Operation: 'Operation',
+ ViewDetail: 'View Detail',
+ ViewTrace: 'View Trace',
+ MessageDetail: 'Message Detail',
+ MessageList: 'Message List',
+ DeadLetterList: 'Dead Letter List',
+ TraceList: 'Trace List',
+ Dashboard: 'Dashboard',
+ Tools: 'Tools',
+ PublishMessage: 'Publish Message',
+ PublishSuccess: 'Message published',
+ QueueTools: 'Queue Tools',
+ Refresh: 'Refresh',
+ RealtimeConnected: 'Realtime connected',
+ RealtimeDisconnected: 'Realtime disconnected',
+ LastUpdatedAt: 'Last Updated At',
+ QueryDepth: 'Query Depth',
+ CleanupMessages: 'Cleanup Messages',
+ CleanupSuccess: 'Messages cleaned up',
+ CleanupConfirm: 'Are you sure you want to clean up matching messages?',
+ DepthResult: 'Current Queue Depth',
+ QueueDepth: 'Queue Depth',
+ ConsumedTotal: 'Consumed Total',
+ FailedTotal: 'Failed Total',
+ DLQTotal: 'Dead Letter Total',
+ ConsumeRate: 'Consume Rate',
+ FailureRate: 'Failure Rate',
+ DLQRate: 'DLQ Rate',
+ SuccessRatio: 'Success Ratio',
+ Replay: 'Replay',
+ ReplayMessage: 'Replay Message',
+ ReplayMessageConfirm: 'Are you sure you want to replay this message?',
+ Replayed: 'Replayed',
+ ErrorMessage: 'Error Message',
+ OriginalMessageId: 'Original Message ID',
+ ReplayedAt: 'Replayed At',
+ BatchReplay: 'Batch Replay',
+ BatchReplayConfirm: 'Replay selected dead-letter messages?',
+ ReplaySuccess: 'Dead-letter replay submitted',
+ SelectedCount: '{count} selected',
+ Event: 'Event',
+ Detail: 'Detail',
+ Meta: 'Meta',
+ Window: 'Window',
+ Step: 'Step',
+ OlderThan: 'Older Than',
+ JsonInvalid: '{name} must be valid JSON',
+ InputJsonPlaceholder: 'Input a JSON object',
+ TopicPlaceholder: 'Input topic',
+ PENDING: 'Pending',
+ PROCESSING: 'Processing',
+ COMPLETED: 'Completed',
+ FAILED: 'Failed',
+ PUBLISHED: 'Published',
+ MOVED_TO_DLQ: 'Moved to DLQ',
+ REPLAYED: 'Replayed',
+ LOCK_RECOVERED: 'Lock Recovered',
+ Yes: 'Yes',
+ No: 'No'
+};
+
+export default messages;
diff --git a/src/components/MessageQueue/locale/zh-CN.js b/src/components/MessageQueue/locale/zh-CN.js
new file mode 100644
index 0000000..e756a8f
--- /dev/null
+++ b/src/components/MessageQueue/locale/zh-CN.js
@@ -0,0 +1,80 @@
+const messages = {
+ ID: '编号',
+ Topic: '主题',
+ Status: '状态',
+ Payload: '消息内容',
+ Options: '扩展字段',
+ ConsumerId: '消费者',
+ LockedAt: '锁定时间',
+ NextRetryAt: '下次重试',
+ RetryCount: '重试次数',
+ MaxRetries: '最大重试',
+ Priority: '优先级',
+ TraceId: '追踪ID',
+ MessageId: '消息ID',
+ ExecuteAt: '执行时间',
+ CreatedAt: '创建时间',
+ UpdatedAt: '更新时间',
+ CompletedAt: '完成时间',
+ Operation: '操作',
+ ViewDetail: '查看详情',
+ ViewTrace: '查看轨迹',
+ MessageDetail: '消息详情',
+ MessageList: '消息列表',
+ DeadLetterList: '死信列表',
+ TraceList: '轨迹列表',
+ Dashboard: '仪表盘',
+ Tools: '工具',
+ PublishMessage: '发布消息',
+ PublishSuccess: '消息发布成功',
+ QueueTools: '队列工具',
+ Refresh: '刷新',
+ RealtimeConnected: '实时连接中',
+ RealtimeDisconnected: '实时连接已断开',
+ LastUpdatedAt: '最后更新',
+ QueryDepth: '查询深度',
+ CleanupMessages: '清理消息',
+ CleanupSuccess: '消息清理成功',
+ CleanupConfirm: '确定要清理符合条件的消息吗?',
+ DepthResult: '当前队列深度',
+ QueueDepth: '队列深度',
+ ConsumedTotal: '累计消费',
+ FailedTotal: '累计失败',
+ DLQTotal: '累计死信',
+ ConsumeRate: '消费速率',
+ FailureRate: '失败速率',
+ DLQRate: '死信速率',
+ SuccessRatio: '成功率',
+ Replay: '重放',
+ ReplayMessage: '重放消息',
+ ReplayMessageConfirm: '确定要重放这条消息吗?',
+ Replayed: '已重放',
+ ErrorMessage: '错误信息',
+ OriginalMessageId: '原始消息ID',
+ ReplayedAt: '重放时间',
+ BatchReplay: '批量重放',
+ BatchReplayConfirm: '确定要重放选中的死信消息吗?',
+ ReplaySuccess: '死信已提交重放',
+ SelectedCount: '已选择 {count} 条',
+ Event: '事件',
+ Detail: '详情',
+ Meta: '元数据',
+ Window: '统计窗口',
+ Step: '采样步长',
+ OlderThan: '早于时间',
+ JsonInvalid: '{name} 必须是合法 JSON',
+ InputJsonPlaceholder: '请输入 JSON 对象',
+ TopicPlaceholder: '请输入消息主题',
+ PENDING: '等待执行',
+ PROCESSING: '处理中',
+ COMPLETED: '已完成',
+ FAILED: '失败',
+ PUBLISHED: '消息发布',
+ MOVED_TO_DLQ: '进入死信',
+ REPLAYED: '死信重放',
+ LOCK_RECOVERED: '锁定恢复',
+ Yes: '是',
+ No: '否'
+};
+
+export default messages;
diff --git a/src/components/MessageQueue/style.module.scss b/src/components/MessageQueue/style.module.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/MessageQueue/utils.js b/src/components/MessageQueue/utils.js
new file mode 100644
index 0000000..dd8976d
--- /dev/null
+++ b/src/components/MessageQueue/utils.js
@@ -0,0 +1,151 @@
+const isPlainObject = value => Object.prototype.toString.call(value) === '[object Object]';
+
+export const unwrapFilterValue = value => {
+ if (value === undefined || value === null || value === '') {
+ return undefined;
+ }
+
+ if (Array.isArray(value)) {
+ const list = value.map(item => unwrapFilterValue(item)).filter(item => item !== undefined);
+ return list.length > 0 ? list : undefined;
+ }
+
+ if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, 'value')) {
+ return unwrapFilterValue(value.value);
+ }
+
+ return value;
+};
+
+export const normalizeFilterValue = filterValue => {
+ return Object.keys(filterValue || {}).reduce((result, key) => {
+ const value = unwrapFilterValue(filterValue[key]);
+ if (value !== undefined) {
+ result[key] = value;
+ }
+ return result;
+ }, {});
+};
+
+export const buildListParams = (filterValue, allowedKeys) => {
+ const normalized = normalizeFilterValue(filterValue);
+ return (allowedKeys || Object.keys(normalized)).reduce((result, key) => {
+ if (normalized[key] !== undefined) {
+ result[key] = normalized[key];
+ }
+ return result;
+ }, {});
+};
+
+export const parseJsonInput = (value, fieldName, defaultValue = {}) => {
+ if (value === undefined || value === null || value === '') {
+ return defaultValue;
+ }
+
+ if (typeof value === 'object') {
+ return value;
+ }
+
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ throw new Error(`${fieldName} must be valid JSON`);
+ }
+};
+
+export const parseNumberInput = (value, fieldName, defaultValue) => {
+ if (value === undefined || value === null || value === '') {
+ return defaultValue;
+ }
+
+ const numberValue = Number(value);
+ if (Number.isNaN(numberValue)) {
+ throw new Error(`${fieldName} must be a valid number`);
+ }
+
+ return numberValue;
+};
+
+export const parseIsoDateTimeInput = (value, fieldName) => {
+ if (value === undefined || value === null || value === '') {
+ return undefined;
+ }
+
+ const dateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
+ if (!dateTimePattern.test(value) || Number.isNaN(Date.parse(value))) {
+ throw new Error(`${fieldName} must be a valid ISO date-time`);
+ }
+
+ return value;
+};
+
+export const stringifyJson = value => {
+ if (value === undefined || value === null || value === '') {
+ return '';
+ }
+
+ if (typeof value === 'string') {
+ return value;
+ }
+
+ return JSON.stringify(value, null, 2);
+};
+
+export const formatRate = value => {
+ const numberValue = Number(value || 0);
+ return numberValue.toFixed(2);
+};
+
+export const formatPercent = value => {
+ if (value === undefined || value === null || Number.isNaN(Number(value))) {
+ return '-';
+ }
+
+ return `${(Number(value) * 100).toFixed(2)}%`;
+};
+
+export const buildUrlWithParams = (url, params = {}) => {
+ const query = Object.keys(params)
+ .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
+ .join('&');
+
+ if (!query) {
+ return url;
+ }
+
+ return `${url}${url.includes('?') ? '&' : '?'}${query}`;
+};
+
+export const getMetricTotal = value => {
+ if (!value) {
+ return 0;
+ }
+
+ if (typeof value.total === 'number') {
+ return value.total;
+ }
+
+ return Object.values(value.byTopic || {}).reduce((total, item) => total + Number(item || 0), 0);
+};
+
+export const filterPageData = ({ pageData = [], params = {}, filters = {} }) => {
+ const normalizedParams = Object.assign({}, params, params.filter);
+ const filtered = pageData.filter(item => {
+ return Object.keys(filters).every(key => {
+ const value = unwrapFilterValue(normalizedParams[key]);
+ if (value === undefined) {
+ return true;
+ }
+ return filters[key](item, value);
+ });
+ });
+ const currentPage = Number(normalizedParams.currentPage || 1);
+ const perPage = Number(normalizedParams.perPage || filtered.length || 20);
+ const start = perPage * (currentPage - 1);
+
+ return {
+ pageData: filtered.slice(start, start + perPage),
+ totalCount: filtered.length
+ };
+};
diff --git a/src/components/MessageQueue/utils.test.js b/src/components/MessageQueue/utils.test.js
new file mode 100644
index 0000000..7404fef
--- /dev/null
+++ b/src/components/MessageQueue/utils.test.js
@@ -0,0 +1,88 @@
+import {
+ buildListParams,
+ buildUrlWithParams,
+ filterPageData,
+ formatPercent,
+ formatRate,
+ parseIsoDateTimeInput,
+ parseJsonInput,
+ parseNumberInput
+} from './utils';
+
+describe('MessageQueue utils', () => {
+ test('builds flat query params from Filter values for fastify-mq list APIs', () => {
+ const params = buildListParams(
+ {
+ topic: { value: 'order.created', label: 'order.created' },
+ status: { value: { value: 'FAILED', label: 'Failed' } },
+ traceId: 'trace_001',
+ empty: '',
+ ignored: 'ignored'
+ },
+ ['topic', 'status', 'traceId']
+ );
+
+ expect(params).toEqual({
+ topic: 'order.created',
+ status: 'FAILED',
+ traceId: 'trace_001'
+ });
+ });
+
+ test('keeps boolean values when building dead-letter filters', () => {
+ const params = buildListParams(
+ {
+ replayed: { value: false, label: 'No' },
+ topic: { value: 'email.send' }
+ },
+ ['topic', 'replayed']
+ );
+
+ expect(params).toEqual({
+ topic: 'email.send',
+ replayed: false
+ });
+ });
+
+ test('parses JSON form input and reports invalid JSON', () => {
+ expect(parseJsonInput('{"orderId":"ord_001"}', 'payload')).toEqual({ orderId: 'ord_001' });
+ expect(parseJsonInput({ orderId: 'ord_002' }, 'payload')).toEqual({ orderId: 'ord_002' });
+ expect(parseJsonInput('', 'meta')).toEqual({});
+ expect(() => parseJsonInput('{bad json}', 'payload')).toThrow('payload');
+ });
+
+ test('parses numeric and ISO date-time form input before publishing', () => {
+ expect(parseNumberInput('3', 'maxRetries')).toBe(3);
+ expect(parseNumberInput('', 'priority', 0)).toBe(0);
+ expect(() => parseNumberInput('abc', 'priority')).toThrow('priority');
+ expect(parseIsoDateTimeInput('2026-05-11T09:00:00.000Z', 'executeAt')).toBe('2026-05-11T09:00:00.000Z');
+ expect(parseIsoDateTimeInput('', 'executeAt')).toBeUndefined();
+ expect(() => parseIsoDateTimeInput('2026-05-11', 'executeAt')).toThrow('executeAt');
+ });
+
+ test('filters and paginates mock page data', () => {
+ const result = filterPageData({
+ pageData: [{ topic: 'a' }, { topic: 'a' }, { topic: 'a' }, { topic: 'b' }],
+ params: { topic: 'a', currentPage: 2, perPage: 2 },
+ filters: {
+ topic: (item, value) => item.topic === value
+ }
+ });
+
+ expect(result).toEqual({
+ pageData: [{ topic: 'a' }],
+ totalCount: 3
+ });
+ });
+
+ test('formats rate and percent values for dashboard display', () => {
+ expect(formatRate(1.23456)).toBe('1.23');
+ expect(formatPercent(0.9632)).toBe('96.32%');
+ expect(formatPercent(null)).toBe('-');
+ });
+
+ test('builds dashboard SSE url with query params', () => {
+ expect(buildUrlWithParams('/api/v1/mq/dashboard/sse', { interval: 1000, window: 300000 })).toBe('/api/v1/mq/dashboard/sse?interval=1000&window=300000');
+ expect(buildUrlWithParams('/api/v1/mq/dashboard/sse?foo=bar', { interval: 1000 })).toBe('/api/v1/mq/dashboard/sse?foo=bar&interval=1000');
+ });
+});
diff --git a/src/components/MessageQueue/withLocale.js b/src/components/MessageQueue/withLocale.js
new file mode 100644
index 0000000..64d5e41
--- /dev/null
+++ b/src/components/MessageQueue/withLocale.js
@@ -0,0 +1,14 @@
+import { createWithIntlProvider } from '@kne/react-intl';
+import zhCN from './locale/zh-CN';
+import enUS from './locale/en-US';
+
+const withLocale = createWithIntlProvider({
+ defaultLocale: 'zh-CN',
+ messages: {
+ 'zh-CN': zhCN,
+ 'en-US': enUS
+ },
+ namespace: 'components-admin:MessageQueue'
+});
+
+export default withLocale;
diff --git a/src/mockPreset/dead-letter-list.json b/src/mockPreset/dead-letter-list.json
new file mode 100644
index 0000000..e8258b9
--- /dev/null
+++ b/src/mockPreset/dead-letter-list.json
@@ -0,0 +1,60 @@
+{
+ "pageData": [
+ {
+ "id": "dlq_001",
+ "originalId": "msg_004",
+ "topic": "email.send",
+ "payload": { "to": "user@example.com", "template": "welcome", "vars": { "name": "John" } },
+ "errorMessage": "SMTP connection timeout after 30000ms",
+ "replayed": false,
+ "replayedAt": null,
+ "createdAt": "2026-05-10T09:50:00Z",
+ "updatedAt": "2026-05-10T09:50:00Z"
+ },
+ {
+ "id": "dlq_002",
+ "originalId": "msg_008",
+ "topic": "file.process",
+ "payload": { "fileId": "file_555", "type": "image", "operations": ["resize", "compress"] },
+ "errorMessage": "File too large: exceeds 10MB limit",
+ "replayed": false,
+ "replayedAt": null,
+ "createdAt": "2026-05-10T09:40:00Z",
+ "updatedAt": "2026-05-10T09:40:00Z"
+ },
+ {
+ "id": "dlq_003",
+ "originalId": "msg_010",
+ "topic": "payment.process",
+ "payload": { "paymentId": "pay_456", "amount": 2500.00 },
+ "errorMessage": "Payment gateway error: invalid merchant ID",
+ "replayed": true,
+ "replayedAt": "2026-05-10T08:30:00Z",
+ "createdAt": "2026-05-10T08:00:00Z",
+ "updatedAt": "2026-05-10T08:30:00Z"
+ },
+ {
+ "id": "dlq_004",
+ "originalId": "msg_012",
+ "topic": "sms.send",
+ "payload": { "phone": "+1234567890", "message": "Your code is 123456" },
+ "errorMessage": "Invalid phone number format",
+ "replayed": false,
+ "replayedAt": null,
+ "createdAt": "2026-05-09T15:20:00Z",
+ "updatedAt": "2026-05-09T15:20:00Z"
+ },
+ {
+ "id": "dlq_005",
+ "originalId": "msg_015",
+ "topic": "webhook.invoke",
+ "payload": { "url": "https://example.com/webhook", "event": "order.created" },
+ "errorMessage": "Webhook endpoint returned 503 Service Unavailable",
+ "replayed": true,
+ "replayedAt": "2026-05-09T12:00:00Z",
+ "createdAt": "2026-05-09T11:30:00Z",
+ "updatedAt": "2026-05-09T12:00:00Z"
+ }
+ ],
+ "totalCount": 5
+}
diff --git a/src/mockPreset/index.js b/src/mockPreset/index.js
index 740e0d3..6c71608 100644
--- a/src/mockPreset/index.js
+++ b/src/mockPreset/index.js
@@ -3,6 +3,7 @@ import { getApis } from '@components/Apis';
import { enums as taskEnums } from '@components/Task';
import { enums as intlAdminEnums } from '@components/IntlAdmin';
import merge from 'lodash/merge';
+import { filterPageData } from '@components/MessageQueue/utils';
import taskList from './task-list.json';
import signatureList from './signature-list.json';
@@ -13,8 +14,11 @@ import superAdminInfo from './super-admin-info.json';
import groupList from './group-list.json';
import tenantData from './tenant-data.json';
import tenantAdminData from './tenant-admin-data.json';
+import messageQueueList from './message-queue-list.json';
+import deadLetterList from './dead-letter-list.json';
+import traceList from './trace-list.json';
-export { taskList, signatureList, intlAdminData, adminUserList, userInfo, superAdminInfo, groupList, tenantData, tenantAdminData };
+export { taskList, signatureList, intlAdminData, adminUserList, userInfo, superAdminInfo, groupList, tenantData, tenantAdminData, messageQueueList, deadLetterList, traceList };
const apis = merge({}, getApis(), {
task: {
@@ -433,6 +437,141 @@ const apis = merge({}, getApis(), {
removeCustomComponent: {
loader: () => ({ code: 0 })
}
+ },
+ mq: {
+ message: {
+ publish: {
+ loader: ({ data }) => ({
+ id: `msg_${Date.now()}`,
+ traceId: data?.traceId || `trace_${Date.now()}`,
+ status: 'PENDING',
+ ...data
+ })
+ },
+ list: {
+ loader: ({ params }) => {
+ return filterPageData({
+ pageData: messageQueueList.pageData,
+ params,
+ filters: {
+ topic: (item, value) => item.topic === value,
+ status: (item, value) => item.status === value,
+ traceId: (item, value) => item.traceId === value
+ }
+ });
+ }
+ }
+ },
+ deadLetter: {
+ list: {
+ loader: ({ params }) => {
+ return filterPageData({
+ pageData: deadLetterList.pageData,
+ params,
+ filters: {
+ topic: (item, value) => item.topic === value,
+ replayed: (item, value) => item.replayed === value
+ }
+ });
+ }
+ },
+ replay: {
+ loader: ({ data }) => {
+ if (Array.isArray(data?.ids) && data.ids.length > 0) {
+ return data.ids.map(id => ({ id, success: true, messageId: `msg_replay_${Date.now()}` }));
+ }
+ return {
+ id: `msg_replay_${Date.now()}`,
+ originalDeadLetterId: data?.id,
+ status: 'PENDING'
+ };
+ }
+ }
+ },
+ trace: {
+ list: {
+ loader: ({ params }) => {
+ return filterPageData({
+ pageData: traceList.pageData,
+ params,
+ filters: {
+ topic: (item, value) => item.topic === value,
+ messageId: (item, value) => item.messageId === value,
+ event: (item, value) => item.event === value
+ }
+ });
+ }
+ },
+ detail: {
+ loader: ({ params }) => {
+ const traceId = params?.traceId || 'trace_abc123';
+ return traceList.pageData.filter(item => item.traceId === traceId);
+ }
+ }
+ },
+ dashboard: {
+ getData: {
+ loader: () => ({
+ timestamp: Date.now(),
+ current: {
+ queueDepth: { byTopic: { 'order.created': 5, 'user.registered': 2, 'payment.completed': 1 }, total: 8 },
+ consumedTotal: { byTopic: { 'order.created': 100, 'user.registered': 50 }, total: 150 },
+ failedTotal: { byTopic: { 'email.send': 2, 'file.process': 1 }, total: 3 },
+ dlqTotal: { byTopic: { 'email.send': 2, 'file.process': 1 }, total: 3 },
+ consumeRate: { byTopic: { 'order.created': 0.5, 'user.registered': 0.3 }, total: 0.8 },
+ failureRate: { byTopic: { 'email.send': 0.02 }, total: 0.02 },
+ dlqRate: { byTopic: { 'email.send': 0.01 }, total: 0.01 },
+ successRatio: 0.96,
+ successRatioByTopic: { 'order.created': 0.96, 'user.registered': 0.98 }
+ },
+ timeSeries: {
+ queueDepth: [
+ { timestamp: Date.now() - 300000, 'order.created': 8, 'user.registered': 3 },
+ { timestamp: Date.now() - 240000, 'order.created': 7, 'user.registered': 2 },
+ { timestamp: Date.now() - 180000, 'order.created': 6, 'user.registered': 2 },
+ { timestamp: Date.now() - 120000, 'order.created': 5, 'user.registered': 2 },
+ { timestamp: Date.now() - 60000, 'order.created': 5, 'user.registered': 1 },
+ { timestamp: Date.now(), 'order.created': 5, 'user.registered': 2 }
+ ],
+ consumeRate: [
+ { timestamp: Date.now() - 300000, 'order.created': 0.5, 'user.registered': 0.3 },
+ { timestamp: Date.now() - 240000, 'order.created': 0.6, 'user.registered': 0.2 },
+ { timestamp: Date.now() - 180000, 'order.created': 0.4, 'user.registered': 0.4 },
+ { timestamp: Date.now() - 120000, 'order.created': 0.5, 'user.registered': 0.3 },
+ { timestamp: Date.now() - 60000, 'order.created': 0.5, 'user.registered': 0.3 },
+ { timestamp: Date.now(), 'order.created': 0.5, 'user.registered': 0.3 }
+ ],
+ failureRate: [
+ { timestamp: Date.now() - 300000, 'email.send': 0.02 },
+ { timestamp: Date.now() - 240000, 'email.send': 0.01 },
+ { timestamp: Date.now() - 180000, 'email.send': 0.03 },
+ { timestamp: Date.now() - 120000, 'email.send': 0.02 },
+ { timestamp: Date.now() - 60000, 'email.send': 0.01 },
+ { timestamp: Date.now(), 'email.send': 0.02 }
+ ],
+ dlqRate: [
+ { timestamp: Date.now() - 300000, 'email.send': 0.01 },
+ { timestamp: Date.now() - 240000, 'email.send': 0.005 },
+ { timestamp: Date.now() - 180000, 'email.send': 0.015 },
+ { timestamp: Date.now() - 120000, 'email.send': 0.01 },
+ { timestamp: Date.now() - 60000, 'email.send': 0.005 },
+ { timestamp: Date.now(), 'email.send': 0.01 }
+ ]
+ }
+ })
+ }
+ },
+ queue: {
+ depth: {
+ loader: ({ params }) => {
+ const list = messageQueueList.pageData.filter(item => item.status === 'PENDING' && (!params?.topic || item.topic === params.topic));
+ return { depth: list.length };
+ }
+ },
+ cleanup: {
+ loader: () => ({ deleted: 3 })
+ }
+ }
}
});
@@ -444,6 +583,29 @@ const enums = Object.assign({}, taskEnums, intlAdminEnums, {
{ value: 'video_processing', description: '视频处理' },
{ value: 'data_sync', description: '数据同步' },
{ value: 'report_generation', description: '报表生成' }
+ ],
+ messageStatus: [
+ { value: 'PENDING', description: '等待执行', type: 'info' },
+ { value: 'PROCESSING', description: '处理中', type: 'progress' },
+ { value: 'COMPLETED', description: '已完成', type: 'success' },
+ { value: 'FAILED', description: '失败', type: 'danger' }
+ ],
+ traceEvent: [
+ { value: 'PUBLISHED', description: '消息发布', type: 'info' },
+ { value: 'PROCESSING', description: '开始处理', type: 'progress' },
+ { value: 'COMPLETED', description: '处理完成', type: 'success' },
+ { value: 'FAILED', description: '处理失败', type: 'danger' },
+ { value: 'MOVED_TO_DLQ', description: '进入死信', type: 'danger' },
+ { value: 'REPLAYED', description: '死信重放', type: 'success' },
+ { value: 'LOCK_RECOVERED', description: '锁定恢复', type: 'info' }
+ ],
+ mqBoolean: [
+ { value: true, description: '是', type: 'success' },
+ { value: false, description: '否', type: 'info' }
+ ],
+ yesNo: [
+ { value: 'yes', description: '是' },
+ { value: 'no', description: '否' }
]
});
@@ -453,7 +615,7 @@ const preset = {
const { ajax } = await globalInit();
return ajax({ loader, ...props });
}
- return Promise.resolve({ data: loader ? { code: 0, data: loader() } : { code: 0, data: {} } });
+ return Promise.resolve({ data: loader ? { code: 0, data: loader(props) } : { code: 0, data: {} } });
},
apis,
enums,
diff --git a/src/mockPreset/message-queue-list.json b/src/mockPreset/message-queue-list.json
new file mode 100644
index 0000000..077a2a2
--- /dev/null
+++ b/src/mockPreset/message-queue-list.json
@@ -0,0 +1,141 @@
+{
+ "pageData": [
+ {
+ "id": "msg_001",
+ "topic": "order.created",
+ "payload": { "orderId": "ord_12345", "amount": 299.99, "customerId": "cust_001" },
+ "status": "COMPLETED",
+ "retryCount": 0,
+ "maxRetries": 3,
+ "priority": 1,
+ "executeAt": "2026-05-10T10:00:00Z",
+ "nextRetryAt": null,
+ "consumerId": "consumer_01",
+ "lockedAt": "2026-05-10T10:00:01Z",
+ "traceId": "trace_abc123",
+ "options": {},
+ "createdAt": "2026-05-10T10:00:00Z",
+ "updatedAt": "2026-05-10T10:00:05Z"
+ },
+ {
+ "id": "msg_002",
+ "topic": "user.registered",
+ "payload": { "userId": "user_54321", "email": "test@example.com" },
+ "status": "PROCESSING",
+ "retryCount": 1,
+ "maxRetries": 3,
+ "priority": 2,
+ "executeAt": "2026-05-10T10:05:00Z",
+ "nextRetryAt": "2026-05-10T10:06:00Z",
+ "consumerId": "consumer_02",
+ "lockedAt": "2026-05-10T10:05:01Z",
+ "traceId": "trace_def456",
+ "options": { "timeout": 30000 },
+ "createdAt": "2026-05-10T10:05:00Z",
+ "updatedAt": "2026-05-10T10:05:30Z"
+ },
+ {
+ "id": "msg_003",
+ "topic": "payment.completed",
+ "payload": { "paymentId": "pay_789", "amount": 1500.00, "method": "credit_card" },
+ "status": "PENDING",
+ "retryCount": 0,
+ "maxRetries": 3,
+ "priority": 3,
+ "executeAt": "2026-05-10T10:10:00Z",
+ "nextRetryAt": null,
+ "consumerId": null,
+ "lockedAt": null,
+ "traceId": "trace_ghi789",
+ "options": {},
+ "createdAt": "2026-05-10T10:10:00Z",
+ "updatedAt": "2026-05-10T10:10:00Z"
+ },
+ {
+ "id": "msg_004",
+ "topic": "email.send",
+ "payload": { "to": "user@example.com", "template": "welcome", "vars": { "name": "John" } },
+ "status": "FAILED",
+ "retryCount": 3,
+ "maxRetries": 3,
+ "priority": 0,
+ "executeAt": "2026-05-10T09:30:00Z",
+ "nextRetryAt": "2026-05-10T09:35:00Z",
+ "consumerId": "consumer_03",
+ "lockedAt": "2026-05-10T09:30:05Z",
+ "traceId": "trace_jkl012",
+ "options": { "error": "SMTP connection timeout" },
+ "createdAt": "2026-05-10T09:30:00Z",
+ "updatedAt": "2026-05-10T09:50:00Z"
+ },
+ {
+ "id": "msg_005",
+ "topic": "order.created",
+ "payload": { "orderId": "ord_67890", "amount": 89.50, "customerId": "cust_002" },
+ "status": "COMPLETED",
+ "retryCount": 0,
+ "maxRetries": 3,
+ "priority": 1,
+ "executeAt": "2026-05-10T10:15:00Z",
+ "nextRetryAt": null,
+ "consumerId": "consumer_01",
+ "lockedAt": "2026-05-10T10:15:01Z",
+ "traceId": "trace_mno345",
+ "options": {},
+ "createdAt": "2026-05-10T10:15:00Z",
+ "updatedAt": "2026-05-10T10:15:08Z"
+ },
+ {
+ "id": "msg_006",
+ "topic": "inventory.update",
+ "payload": { "sku": "PROD-001", "quantity": -5, "warehouse": "WH-EAST" },
+ "status": "PROCESSING",
+ "retryCount": 0,
+ "maxRetries": 3,
+ "priority": 2,
+ "executeAt": "2026-05-10T10:20:00Z",
+ "nextRetryAt": null,
+ "consumerId": "consumer_04",
+ "lockedAt": "2026-05-10T10:20:01Z",
+ "traceId": "trace_pqr678",
+ "options": {},
+ "createdAt": "2026-05-10T10:20:00Z",
+ "updatedAt": "2026-05-10T10:20:10Z"
+ },
+ {
+ "id": "msg_007",
+ "topic": "notification.push",
+ "payload": { "userId": "user_111", "title": "New Order", "body": "Your order has been shipped" },
+ "status": "PENDING",
+ "retryCount": 0,
+ "maxRetries": 3,
+ "priority": 1,
+ "executeAt": "2026-05-10T10:25:00Z",
+ "nextRetryAt": null,
+ "consumerId": null,
+ "lockedAt": null,
+ "traceId": "trace_stu901",
+ "options": {},
+ "createdAt": "2026-05-10T10:25:00Z",
+ "updatedAt": "2026-05-10T10:25:00Z"
+ },
+ {
+ "id": "msg_008",
+ "topic": "file.process",
+ "payload": { "fileId": "file_555", "type": "image", "operations": ["resize", "compress"] },
+ "status": "FAILED",
+ "retryCount": 2,
+ "maxRetries": 3,
+ "priority": 0,
+ "executeAt": "2026-05-10T09:00:00Z",
+ "nextRetryAt": "2026-05-10T09:10:00Z",
+ "consumerId": "consumer_05",
+ "lockedAt": "2026-05-10T09:00:05Z",
+ "traceId": "trace_vwx234",
+ "options": { "error": "File too large" },
+ "createdAt": "2026-05-10T09:00:00Z",
+ "updatedAt": "2026-05-10T09:40:00Z"
+ }
+ ],
+ "totalCount": 8
+}
diff --git a/src/mockPreset/trace-list.json b/src/mockPreset/trace-list.json
new file mode 100644
index 0000000..8285a4a
--- /dev/null
+++ b/src/mockPreset/trace-list.json
@@ -0,0 +1,86 @@
+{
+ "pageData": [
+ {
+ "id": "trace_001",
+ "traceId": "trace_abc123",
+ "topic": "order.created",
+ "event": "PUBLISHED",
+ "detail": { "orderId": "ord_12345" },
+ "messageId": "msg_001",
+ "createdAt": "2026-05-10T10:00:00Z"
+ },
+ {
+ "id": "trace_002",
+ "traceId": "trace_abc123",
+ "topic": "order.created",
+ "event": "PROCESSING",
+ "detail": { "consumerId": "consumer_01" },
+ "messageId": "msg_001",
+ "createdAt": "2026-05-10T10:00:01Z"
+ },
+ {
+ "id": "trace_003",
+ "traceId": "trace_abc123",
+ "topic": "order.created",
+ "event": "PROCESSING",
+ "detail": { "startTime": "2026-05-10T10:00:01Z" },
+ "messageId": "msg_001",
+ "createdAt": "2026-05-10T10:00:01Z"
+ },
+ {
+ "id": "trace_004",
+ "traceId": "trace_abc123",
+ "topic": "order.created",
+ "event": "COMPLETED",
+ "detail": { "duration": 4000, "result": "success" },
+ "messageId": "msg_001",
+ "createdAt": "2026-05-10T10:00:05Z"
+ },
+ {
+ "id": "trace_005",
+ "traceId": "trace_def456",
+ "topic": "user.registered",
+ "event": "PUBLISHED",
+ "detail": { "userId": "user_54321" },
+ "messageId": "msg_002",
+ "createdAt": "2026-05-10T10:05:00Z"
+ },
+ {
+ "id": "trace_006",
+ "traceId": "trace_def456",
+ "topic": "user.registered",
+ "event": "PROCESSING",
+ "detail": { "consumerId": "consumer_02" },
+ "messageId": "msg_002",
+ "createdAt": "2026-05-10T10:05:01Z"
+ },
+ {
+ "id": "trace_007",
+ "traceId": "trace_def456",
+ "topic": "user.registered",
+ "event": "PROCESSING",
+ "detail": { "startTime": "2026-05-10T10:05:01Z" },
+ "messageId": "msg_002",
+ "createdAt": "2026-05-10T10:05:01Z"
+ },
+ {
+ "id": "trace_008",
+ "traceId": "trace_def456",
+ "topic": "user.registered",
+ "event": "FAILED",
+ "detail": { "error": "Database connection failed", "retryCount": 1 },
+ "messageId": "msg_002",
+ "createdAt": "2026-05-10T10:05:30Z"
+ },
+ {
+ "id": "trace_009",
+ "traceId": "trace_def456",
+ "topic": "user.registered",
+ "event": "FAILED",
+ "detail": { "nextRetryAt": "2026-05-10T10:06:00Z" },
+ "messageId": "msg_002",
+ "createdAt": "2026-05-10T10:05:30Z"
+ }
+ ],
+ "totalCount": 9
+}
diff --git a/src/preset.js b/src/preset.js
index e3bffb0..6927b81 100644
--- a/src/preset.js
+++ b/src/preset.js
@@ -97,7 +97,7 @@ export const globalInit = async () => {
//url: 'http://localhost:3010',
//tpl: '{{url}}',
remote: 'components-thirdparty',
- defaultVersion: '0.1.12'
+ defaultVersion: '0.1.17'
},
'components-admin':
process.env.NODE_ENV === 'development'