This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 0ea72dbf0 feat(message): store query result snapshots and simplify 
topic/consumer pages (#2839)
0ea72dbf0 is described below

commit 0ea72dbf0f1896a80e63f95b3e40cb567c13467c
Author: lizhimins <[email protected]>
AuthorDate: Mon Aug 31 16:36:08 2026 +0800

    feat(message): store query result snapshots and simplify topic/consumer 
pages (#2839)
---
 .../studio/instance/message/MessageService.java    |   7 +-
 .../instance/message/QueryHistoryController.java   |   8 ++
 .../instance/message/QueryHistoryService.java      |  68 +++++++++-
 .../studio/ops/alert/AlertSchemaMigration.java     |   3 +-
 .../studio/persistence/entity/RmqMessageQuery.java |   2 +
 server/src/main/resources/db/schema.sql            |   1 +
 .../instance/message/MessageServiceTest.java       |   4 +-
 .../QueryHistoryServiceIntegrationTest.java        |   2 +-
 .../instance/message/QueryHistoryServiceTest.java  |   5 +-
 web/src/api/messageHistory.ts                      |  21 +++
 .../pages/instance/__tests__/ConsumerPage.test.tsx |  29 -----
 web/src/pages/instance/consumer.tsx                |  36 +-----
 web/src/pages/instance/message.tsx                 |  40 ++++--
 web/src/pages/instance/topic.tsx                   | 143 +++++----------------
 14 files changed, 172 insertions(+), 197 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
index 78aef0178..e676cfd64 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
@@ -54,7 +54,7 @@ public class MessageService {
                 .map(provider -> provider.queryMessages(instanceId, topic, 
msgId, tag, key, startTime, endTime))
                 .orElseGet(() -> messageProvider.queryMessages(instanceId, 
topic, msgId, tag, key, startTime, endTime));
         if (recordHistory) {
-            recordMessageQuery(instanceId, topic, msgId, tag, key, startTime, 
endTime, result.size());
+            recordMessageQuery(instanceId, topic, msgId, tag, key, startTime, 
endTime, result);
         }
         return result;
     }
@@ -151,11 +151,12 @@ public class MessageService {
                 .orElseGet(() -> 
messageProvider.getMessageTraceByKey(instanceId, key, topic, traceTopic));
     }
     private void recordMessageQuery(String instanceId, String topic, String 
msgId, String tag,
-                                    String key, Long startTime, Long endTime, 
int resultCount) {
+                                    String key, Long startTime, Long endTime, 
List<MessageRecordVO> result) {
         String queryType = StringUtils.hasText(msgId) ? "MSG_ID" : 
StringUtils.hasText(key) ? "KEY" : "TOPIC";
         try {
+            String snapshot = queryHistoryService.buildResultSnapshot(result);
             queryHistoryService.recordMessageQuery(instanceId, queryType, 
topic, msgId, tag, key,
-                    startTime, endTime, resultCount);
+                    startTime, endTime, result.size(), snapshot);
         } catch (RuntimeException failure) {
             log.warn("Failed to record message query history: {}", 
failure.getMessage());
         }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryController.java
index 017e4290e..c43258374 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryController.java
@@ -11,11 +11,14 @@ import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.domain.Result;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.util.StringUtils;
 
+import java.util.List;
+
 @RestController
 @RequestMapping("/api/query-history")
 @RequiredArgsConstructor
@@ -52,6 +55,11 @@ public class QueryHistoryController {
         return 
Result.ok(queryHistoryService.summarize(normalizeFilter(clusterId)));
     }
 
+    @GetMapping("/messages/{id}/results")
+    public Result<List<MessageRecordVO>> messageQueryResults(@PathVariable 
long id) {
+        return Result.ok(queryHistoryService.getMessageQueryResults(id));
+    }
+
     private void validatePage(int page, int pageSize) {
         if (page < 1) {
             throw new BusinessException(400, "page must be at least 1");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
index 0bb21e87f..699d83797 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
@@ -19,6 +19,8 @@ package org.apache.rocketmq.studio.instance.message;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
@@ -33,7 +35,9 @@ import org.springframework.util.StringUtils;
 
 import java.time.Clock;
 import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 @Slf4j
 @Service
@@ -43,27 +47,31 @@ public class QueryHistoryService {
     private final RmqTraceQueryMapper traceQueryMapper;
     private final QueryHistoryProperties properties;
     private final Clock clock;
+    private final ObjectMapper objectMapper;
 
     @Autowired
     public QueryHistoryService(RmqMessageQueryMapper messageQueryMapper,
                                RmqTraceQueryMapper traceQueryMapper,
-                               QueryHistoryProperties properties) {
-        this(messageQueryMapper, traceQueryMapper, properties, 
Clock.systemUTC());
+                               QueryHistoryProperties properties,
+                               ObjectMapper objectMapper) {
+        this(messageQueryMapper, traceQueryMapper, properties, 
Clock.systemUTC(), objectMapper);
     }
 
     QueryHistoryService(RmqMessageQueryMapper messageQueryMapper,
                         RmqTraceQueryMapper traceQueryMapper,
                         QueryHistoryProperties properties,
-                        Clock clock) {
+                        Clock clock,
+                        ObjectMapper objectMapper) {
         this.messageQueryMapper = messageQueryMapper;
         this.traceQueryMapper = traceQueryMapper;
         this.properties = properties;
         this.clock = clock;
+        this.objectMapper = objectMapper;
     }
 
     public void recordMessageQuery(String clusterId, String queryType, String 
topic, String msgId,
                                    String tag, String key, Long startTime,
-                                   Long endTime, int resultCount) {
+                                   Long endTime, int resultCount, String 
resultSnapshot) {
         RmqMessageQuery query = new RmqMessageQuery();
         query.setQueryType(queryType);
         query.setTopic(topic);
@@ -73,6 +81,7 @@ public class QueryHistoryService {
         query.setStartTime(startTime);
         query.setEndTime(endTime);
         query.setResultCount(resultCount);
+        query.setResultSnapshot(resultSnapshot);
         query.setClusterId(clusterId);
         query.setQueriedBy(AuthenticatedUserContext.currentUsernameOrSystem());
         LocalDateTime now = LocalDateTime.now(clock);
@@ -82,6 +91,57 @@ public class QueryHistoryService {
         log.debug("Message query recorded: clusterId={} type={} topic={}", 
clusterId, queryType, topic);
     }
 
+    /**
+     * Builds a JSON snapshot of query results, excluding message body and 
properties to save storage.
+     */
+    public String buildResultSnapshot(List<MessageRecordVO> results) {
+        if (results == null || results.isEmpty()) {
+            return null;
+        }
+        try {
+            List<Map<String, Object>> snapshots = results.stream().map(r -> {
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("msgId", r.getMsgId() == null ? "" : r.getMsgId());
+                m.put("topic", r.getTopic() == null ? "" : r.getTopic());
+                m.put("tag", r.getTag() == null ? "" : r.getTag());
+                m.put("key", r.getKey() == null ? "" : r.getKey());
+                m.put("brokerName", r.getBrokerName() == null ? "" : 
r.getBrokerName());
+                m.put("queueId", r.getQueueId() == null ? 0 : r.getQueueId());
+                m.put("queueOffset", r.getQueueOffset() == null ? 0L : 
r.getQueueOffset());
+                m.put("storeTime", r.getStoreTime());
+                m.put("bornHost", r.getBornHost() == null ? "" : 
r.getBornHost());
+                m.put("storeHost", r.getStoreHost() == null ? "" : 
r.getStoreHost());
+                m.put("size", r.getSize());
+                return m;
+            }).toList();
+            return objectMapper.writeValueAsString(snapshots);
+        } catch (JsonProcessingException e) {
+            log.warn("Failed to serialize result snapshot: {}", 
e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Retrieves the stored result snapshot for a given history record.
+     */
+    public List<MessageRecordVO> getMessageQueryResults(long id) {
+        RmqMessageQuery query = messageQueryMapper.selectById(id);
+        if (query == null) {
+            throw new 
org.apache.rocketmq.studio.common.exception.BusinessException(404, "Query 
history record not found");
+        }
+        String snapshot = query.getResultSnapshot();
+        if (!StringUtils.hasText(snapshot)) {
+            return List.of();
+        }
+        try {
+            return objectMapper.readValue(snapshot,
+                    
objectMapper.getTypeFactory().constructCollectionType(List.class, 
MessageRecordVO.class));
+        } catch (JsonProcessingException e) {
+            log.warn("Failed to deserialize result snapshot for id={}: {}", 
id, e.getMessage());
+            return List.of();
+        }
+    }
+
     public void recordTraceQuery(String clusterId, String msgId, String topic, 
int nodeCount, int consumerCount) {
         RmqTraceQuery query = new RmqTraceQuery();
         query.setMsgId(msgId);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSchemaMigration.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSchemaMigration.java
index ac2b18b90..b27eb1255 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSchemaMigration.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSchemaMigration.java
@@ -100,7 +100,8 @@ public class AlertSchemaMigration implements 
ApplicationRunner {
             new Column("rmq_system_alert", "labels_json", "TEXT"),
             new Column("rmq_alert_notification_outbox", "sending_started_at", 
"DATETIME"),
             new Column("rmq_alert_notification_outbox", "claim_token", 
"VARCHAR(64)"),
-            new Column("rmq_alert_notification_outbox", "message_content", 
"TEXT"));
+            new Column("rmq_alert_notification_outbox", "message_content", 
"TEXT"),
+            new Column("rmq_instance_message", "result_snapshot", 
"MEDIUMTEXT"));
     private static final List<Index> INDEXES = List.of(
             new Index("rmq_metric_snapshot", "idx_metric_snapshot_lookup", 
"instance_id, metric_key, collected_at"),
             new Index("rmq_metric_snapshot", "idx_metric_snapshot_retention", 
"collected_at"),
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqMessageQuery.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqMessageQuery.java
index 27838aea4..195e6d324 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqMessageQuery.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqMessageQuery.java
@@ -46,6 +46,8 @@ public class RmqMessageQuery {
 
     private Integer resultCount;
 
+    private String resultSnapshot;
+
     private String clusterId;
 
     private String queriedBy;
diff --git a/server/src/main/resources/db/schema.sql 
b/server/src/main/resources/db/schema.sql
index fc058cd61..4cd62aa9c 100644
--- a/server/src/main/resources/db/schema.sql
+++ b/server/src/main/resources/db/schema.sql
@@ -145,6 +145,7 @@ CREATE TABLE IF NOT EXISTS rmq_instance_message (
   start_time BIGINT,
   end_time BIGINT,
   result_count INT DEFAULT 0,
+  result_snapshot MEDIUMTEXT COMMENT '查询结果快照(不含消息体)JSON',
   cluster_id VARCHAR(255),
   queried_by VARCHAR(128),
   PRIMARY KEY (`id`),
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
index 6dfc4dce3..1da40e26c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
@@ -124,7 +124,7 @@ class MessageServiceTest {
         service.queryMessages("cloud-instance", "orders", null, null, 
"ORDER-1", null, null);
 
         verify(history).recordMessageQuery("cloud-instance", "KEY", "orders", 
null, null,
-                "ORDER-1", null, null, 1);
+                "ORDER-1", null, null, 1, null);
         verifyNoInteractions(fallback);
     }
 
@@ -205,7 +205,7 @@ class MessageServiceTest {
         verify(provider, org.mockito.Mockito.times(2))
                 .queryMessages("instance-a", "TopicA", null, null, null, 
1000L, 2000L);
         verify(history, org.mockito.Mockito.times(1)).recordMessageQuery(
-                "instance-a", "TOPIC", "TopicA", null, null, null, 1000L, 
2000L, 1);
+                "instance-a", "TOPIC", "TopicA", null, null, null, 1000L, 
2000L, 1, null);
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceIntegrationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceIntegrationTest.java
index 31c9d6537..b369c9172 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceIntegrationTest.java
@@ -50,7 +50,7 @@ class QueryHistoryServiceIntegrationTest {
         try {
             AuthenticatedUserContext.setUser(longUsername, true);
             queryHistoryService.recordMessageQuery("qh-cluster", "TOPIC", 
"qh-topic",
-                    null, null, null, null, null, 3);
+                    null, null, null, null, null, 3, null);
             queryHistoryService.recordTraceQuery("qh-cluster", "qh-msg-id", 
"qh-topic", 2, 1);
 
             PageResult<MessageQueryHistoryVO> messageHistory =
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
index 4260c2702..5ed0445ee 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
@@ -18,6 +18,7 @@ package org.apache.rocketmq.studio.instance.message;
 
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
 import org.apache.rocketmq.studio.persistence.entity.RmqMessageQuery;
@@ -47,7 +48,7 @@ class QueryHistoryServiceTest {
     private final QueryHistoryProperties properties = new 
QueryHistoryProperties();
     private final Clock clock = 
Clock.fixed(Instant.parse("2026-08-05T12:00:00Z"), ZoneOffset.UTC);
     private final QueryHistoryService service = new QueryHistoryService(
-            messageQueryMapper, traceQueryMapper, properties, clock);
+            messageQueryMapper, traceQueryMapper, properties, clock, new 
ObjectMapper());
 
     @AfterEach
     void clearUserContext() {
@@ -59,7 +60,7 @@ class QueryHistoryServiceTest {
         AuthenticatedUserContext.setUsername("alice");
 
         service.recordMessageQuery("cluster-a", "TOPIC", "orders", null, 
"tag-a", "key-a",
-                1L, 2L, 3);
+                1L, 2L, 3, null);
 
         ArgumentCaptor<RmqMessageQuery> captor = 
ArgumentCaptor.forClass(RmqMessageQuery.class);
         verify(messageQueryMapper).insert(captor.capture());
diff --git a/web/src/api/messageHistory.ts b/web/src/api/messageHistory.ts
index a48abe5e8..538052a86 100644
--- a/web/src/api/messageHistory.ts
+++ b/web/src/api/messageHistory.ts
@@ -78,3 +78,24 @@ export async function getQueryHistorySummary(clusterId?: 
string) {
   });
   return response.data.data;
 }
+
+export interface MessageResultSnapshot {
+  msgId: string;
+  topic: string;
+  tag: string;
+  key: string;
+  brokerName: string;
+  queueId: number;
+  queueOffset: number;
+  storeTime: number;
+  bornHost: string;
+  storeHost: string;
+  size: number;
+}
+
+export async function getMessageQueryResults(id: number) {
+  const response = await client.get<{ data: MessageResultSnapshot[] }>(
+    `/query-history/messages/${id}/results`,
+  );
+  return response.data.data;
+}
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 9f3a2470b..9d760867e 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -931,33 +931,4 @@ describe('Consumer page', () => {
     const dialog = await screen.findByRole('dialog', { name: /unknown-lag-cg/ 
});
     expect(within(dialog).getByText('不可用')).toBeInTheDocument();
   });
-
-  it('sorts groups with an unknown lag after known backlogs in lag order', 
async () => {
-    const user = userEvent.setup();
-    vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
-      groupPage([
-        { ...group, name: 'unknown-lag-cg', totalLag: -1 },
-        { ...group, name: 'known-lag-cg', totalLag: 15000 },
-      ]),
-    );
-    renderWithProviders(<ConsumerPage />);
-    await screen.findByRole('row', { name: /unknown-lag-cg/ });
-
-    await user.click(screen.getByText('名称升序'));
-    await user.click(await screen.findByText('堆积量降序'));
-    await waitFor(() => {
-      const rows = Array.from(document.querySelectorAll('tbody tr'));
-      const order = rows
-        .map((row) => row.textContent ?? '')
-        .map((text) =>
-          text.includes('unknown-lag-cg')
-            ? 'unknown'
-            : /\bknown-lag-cg\b/.test(text)
-              ? 'known'
-              : '',
-        )
-        .filter(Boolean);
-      expect(order).toEqual(['known', 'unknown']);
-    });
-  });
 });
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 588ed3523..69e5264ff 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -159,31 +159,13 @@ const GROUP_EXPORT_COLUMNS: CsvColumn<ConsumerGroup>[] = [
 
 const buildConsumerGroupCsv = (groups: ConsumerGroup[]) => 
buildCsv(GROUP_EXPORT_COLUMNS, groups);
 
-const visibleConsumerGroups = (
-  groups: ConsumerGroup[],
-  modeFilter: string,
-  sortKey: string,
-): ConsumerGroup[] => {
+const visibleConsumerGroups = (groups: ConsumerGroup[], modeFilter: string): 
ConsumerGroup[] => {
   let data = groups;
 
   if (modeFilter !== 'ALL') {
     data = data.filter((group) => group.subscriptionMode === modeFilter);
   }
 
-  if (sortKey === 'lag_desc') {
-    // An unknown lag (-1) is not a measurable backlog, so it sorts after
-    // every group with a known backlog instead of first.
-    data = [...data].sort((left, right) => {
-      const leftKnown = isLagAvailable(left.totalLag);
-      const rightKnown = isLagAvailable(right.totalLag);
-      if (leftKnown !== rightKnown) return leftKnown ? -1 : 1;
-      if (!leftKnown) return 0;
-      return right.totalLag - left.totalLag;
-    });
-  } else if (sortKey === 'name_asc') {
-    data = [...data].sort((left, right) => 
left.name.localeCompare(right.name));
-  }
-
   return data;
 };
 
@@ -233,7 +215,6 @@ const ConsumerPageContent = ({
   const [page, setPage] = useState(1);
   const [pageSize, setPageSize] = useState(20);
   const [modeFilter, setModeFilter] = useState<string>('ALL');
-  const [sortKey, setSortKey] = useState<string>('name_asc');
   const [modalOpen, setModalOpen] = useState(false);
   const [selectedGroup, setSelectedGroup] = useState<ConsumerGroup | 
null>(null);
   const [settingsGroup, setSettingsGroup] = useState<ConsumerGroup | 
null>(null);
@@ -395,8 +376,8 @@ const ConsumerPageContent = ({
 
   /* ─── Filtered & sorted data ─── */
   const filtered = useMemo(() => {
-    return visibleConsumerGroups(groups, modeFilter, sortKey);
-  }, [groups, modeFilter, sortKey]);
+    return visibleConsumerGroups(groups, modeFilter);
+  }, [groups, modeFilter]);
 
   const handleExport = async () => {
     setExporting(true);
@@ -405,7 +386,7 @@ const ConsumerPageContent = ({
         instanceId: selectedInstanceId || undefined,
         search: search.trim() || undefined,
       });
-      const exportGroups = visibleConsumerGroups(allGroups, modeFilter, 
sortKey);
+      const exportGroups = visibleConsumerGroups(allGroups, modeFilter);
       downloadCsv(
         `rocketmq-consumer-groups-${new Date().toISOString().slice(0, 
10)}.csv`,
         buildConsumerGroupCsv(exportGroups),
@@ -1099,15 +1080,6 @@ const ConsumerPageContent = ({
               { value: 'Pop', label: 'Pop' },
             ]}
           />
-          <Select
-            value={sortKey}
-            onChange={setSortKey}
-            style={{ width: 160 }}
-            options={[
-              { value: 'lag_desc', label: '堆积量降序' },
-              { value: 'name_asc', label: '名称升序' },
-            ]}
-          />
         </Space>
         <Space>
           {selectedRowKeys.length > 0 && (
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 7358085e2..4a089f76d 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -57,6 +57,7 @@ import {
   QueueBrowserResults,
 } from '../../components/QueueBrowser';
 import type { MessageQueryHistory, TraceQueryHistory } from 
'../../api/messageHistory';
+import { getMessageQueryResults } from '../../api/messageHistory';
 import { useLang } from '../../i18n/LangContext';
 import type { MessageQuery, MessageRecord, TraceRecord } from 
'../../api/message';
 import {
@@ -365,16 +366,9 @@ const MessagePageContent = ({
     await executeQuery(queryMode, currentQueryParams);
   };
 
-  const replayHistoryRecord = (record: MessageQueryHistory) => {
+  const replayHistoryRecord = async (record: MessageQueryHistory) => {
     const modeMap: Record<string, QueryMode> = { TOPIC: 'topic', KEY: 'key', 
MSG_ID: 'msgid' };
     const mode = modeMap[record.queryType] || 'topic';
-    const params: MessageQuery = {
-      topic: record.topic,
-      msgId: record.msgId || undefined,
-      key: record.messageKey || undefined,
-      startTime: record.startTime,
-      endTime: record.endTime,
-    };
     setQueryMode(mode);
     setSelectedTopic(record.topic);
     setKeyInput(record.messageKey || '');
@@ -383,7 +377,35 @@ const MessagePageContent = ({
       setDateRange([dayjs(record.startTime), dayjs(record.endTime)]);
     }
     setHistoryDrawerOpen(false);
-    void executeQuery(mode, params);
+    setQueryLoading(true);
+    setQueryError(null);
+    try {
+      const results = await getMessageQueryResults(record.id);
+      const mapped: MessageRecord[] = results.map((r) => ({
+        msgId: r.msgId,
+        topic: r.topic,
+        tag: r.tag || null,
+        key: r.key || null,
+        brokerName: r.brokerName || null,
+        queueId: r.queueId,
+        queueOffset: r.queueOffset,
+        body: '',
+        storeTime: r.storeTime,
+        bornHost: r.bornHost,
+        storeHost: r.storeHost,
+        properties: {},
+        size: r.size,
+      }));
+      setMessages(mapped);
+      setMessageTotal(mapped.length);
+      setMessagePage(1);
+      setResultMayBeTruncated(false);
+      message.success(`已加载历史查询结果,共 ${mapped.length} 条`);
+    } catch (error) {
+      setQueryError(getErrorMessage(error, '加载历史结果失败'));
+    } finally {
+      setQueryLoading(false);
+    }
   };
 
   const replayTraceRecord = (record: TraceQueryHistory) => {
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index d380015dc..58d4961fc 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -44,8 +44,6 @@ import {
 import type { TableColumnsType } from 'antd';
 import {
   PlusOutlined,
-  UnorderedListOutlined,
-  AppstoreOutlined,
   SendOutlined,
   DeleteOutlined,
   EyeOutlined,
@@ -316,7 +314,6 @@ const TopicPage = () => {
   const [typeFilter, setTypeFilter] = useState('');
   const [tablePage, setTablePage] = useState(1);
   const [tablePageSize, setTablePageSize] = useState(20);
-  const [viewMode, setViewMode] = useState<string>('列表');
   const [detailModalOpen, setDetailModalOpen] = useState(false);
   const [detailLoading, setDetailLoading] = useState(false);
   const [rebuilding, setRebuilding] = useState(false);
@@ -787,76 +784,6 @@ const TopicPage = () => {
     );
   };
 
-  // ─── Card view ────────────────────────────────────────────────
-  const renderCardView = () => (
-    <Row gutter={[16, 16]}>
-      {filteredTopics.map((topic) => {
-        const typeInfo = TOPIC_TYPE_MAP[topic.type];
-        const cluster = CLUSTER_NAME_MAP[topic.clusterId];
-        const clusterType = cluster ? CLUSTER_TYPE_MAP[cluster.type] : null;
-
-        return (
-          <Col xs={24} sm={12} lg={8} key={topic.name}>
-            <Card
-              hoverable
-              size="small"
-              onClick={() => void openDetail(topic)}
-              styles={{ body: { padding: '16px 20px' } }}
-              style={{ borderRadius: 8, border: '1px solid #f0f0f0' }}
-            >
-              {/* Header row: name + type badge */}
-              <Flex justify="space-between" align="flex-start" style={{ 
marginBottom: 12 }}>
-                <Text strong style={{ fontSize: 15 }}>
-                  {topic.name}
-                </Text>
-                <Tag color={typeInfo?.color}>
-                  {typeInfo?.labelKey ? t(typeInfo.labelKey) : topic.type}
-                </Tag>
-              </Flex>
-
-              {/* Cluster tags */}
-              <Space size={4} style={{ marginBottom: 16 }}>
-                {clusterType && (
-                  <Tag color={clusterType.color} style={{ fontSize: 14 }}>
-                    {t(clusterType.labelKey)}
-                  </Tag>
-                )}
-              </Space>
-
-              {/* Key stats */}
-              <Row gutter={16}>
-                <Col span={8}>
-                  <Text type="secondary" style={{ fontSize: 14, display: 
'block' }}>
-                    今日消息量
-                  </Text>
-                  <Text strong style={{ fontSize: 16, fontVariantNumeric: 
'tabular-nums' }}>
-                    {formatNumber(topic.messageCount)}
-                  </Text>
-                </Col>
-                <Col span={8}>
-                  <Text type="secondary" style={{ fontSize: 14, display: 
'block' }}>
-                    TPS
-                  </Text>
-                  <Text strong style={{ fontSize: 16, fontVariantNumeric: 
'tabular-nums' }}>
-                    {formatNumber(topic.tps)}
-                  </Text>
-                </Col>
-                <Col span={8}>
-                  <Text type="secondary" style={{ fontSize: 14, display: 
'block' }}>
-                    消费者组
-                  </Text>
-                  <Text strong style={{ fontSize: 16, fontVariantNumeric: 
'tabular-nums' }}>
-                    {topic.consumerGroupCount}
-                  </Text>
-                </Col>
-              </Row>
-            </Card>
-          </Col>
-        );
-      })}
-    </Row>
-  );
-
   // ─── Create modal submit ──────────────────────────────────────
   const handleCreate = async () => {
     if (createInFlightRef.current) return;
@@ -1112,14 +1039,6 @@ const TopicPage = () => {
             options={TYPE_OPTIONS}
             style={{ width: 140 }}
           />
-          <Segmented
-            value={viewMode}
-            onChange={(v) => setViewMode(v as string)}
-            options={[
-              { label: '列表', value: '列表', icon: <UnorderedListOutlined /> },
-              { label: '卡片', value: '卡片', icon: <AppstoreOutlined /> },
-            ]}
-          />
         </Space>
         <Space>
           {selectedRowKeys.length > 0 && (
@@ -1209,39 +1128,35 @@ const TopicPage = () => {
       </Flex>
 
       {/* ── Content ───────────────────────────────────────────── */}
-      {viewMode === '列表' ? (
-        <Card styles={{ body: { padding: 0 } }} style={{ borderRadius: 8 }}>
-          <Table<Topic>
-            columns={columns}
-            dataSource={filteredTopics}
-            loading={loading}
-            rowKey="name"
-            rowSelection={{
-              selectedRowKeys,
-              onChange: (keys) => setSelectedRowKeys(keys),
-            }}
-            pagination={{
-              current: currentTablePage,
-              pageSize: tablePageSize,
-              total: totalTopics,
-              showSizeChanger: true,
-              showTotal: (t) => `共 ${t} 条`,
-              onChange: (page, pageSize) => {
-                setTablePage(page);
-                setTablePageSize(pageSize);
-              },
-            }}
-            size="small"
-            scroll={{ x: tableScrollX(columns, { selection: true }) }}
-            onRow={(record) => ({
-              onClick: () => void openDetail(record),
-              style: { cursor: 'pointer' },
-            })}
-          />
-        </Card>
-      ) : (
-        renderCardView()
-      )}
+      <Card styles={{ body: { padding: 0 } }} style={{ borderRadius: 8 }}>
+        <Table<Topic>
+          columns={columns}
+          dataSource={filteredTopics}
+          loading={loading}
+          rowKey="name"
+          rowSelection={{
+            selectedRowKeys,
+            onChange: (keys) => setSelectedRowKeys(keys),
+          }}
+          pagination={{
+            current: currentTablePage,
+            pageSize: tablePageSize,
+            total: totalTopics,
+            showSizeChanger: true,
+            showTotal: (t) => `共 ${t} 条`,
+            onChange: (page, pageSize) => {
+              setTablePage(page);
+              setTablePageSize(pageSize);
+            },
+          }}
+          size="small"
+          scroll={{ x: tableScrollX(columns, { selection: true }) }}
+          onRow={(record) => ({
+            onClick: () => void openDetail(record),
+            style: { cursor: 'pointer' },
+          })}
+        />
+      </Card>
 
       {/* ── Detail Modal ──────────────────────────────────────── */}
       <Modal

Reply via email to