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 cb0b35eda fix(web): honour the declared table column widths (#4211)
cb0b35eda is described below

commit cb0b35eda7560d2321a4dd9ac8bd3763efba3e60
Author: lizhimins <[email protected]>
AuthorDate: Thu Sep 10 17:21:19 2026 +0800

    fix(web): honour the declared table column widths (#4211)
    
    Every list table passes a numeric `scroll.x` derived from its column 
widths, on the
    assumption that this makes those widths authoritative. It does not: 
rc-table only switches to
    `table-layout: fixed` when `scroll.y` is set, a column is `fixed`, or a 
column is `ellipsis`.
    Without one of those the table stays on `table-layout: auto`, where the 
declared widths are
    advisory and a single long value stretches its column past them. A 
45-character consumer group
    name rendered the Group column at 432px instead of the declared 190px and 
pushed the table from
    1154px to 1371px. The widths only looked correct because the tables were 
empty.
    
    Two further problems came out of fixing that:
    
    - Under `table-layout: fixed` the table still carries `min-width: 100%`, so 
a window wider than
      the table shares the surplus across every column in proportion to its 
width. On a 1482px
      container that inflated all columns by 28%: the expand-arrow column 
rendered at 62px and a
      `Push`/`Pull` tag column at 108px. Giving the primary text column 
`minWidth` instead of
      `width` keeps it the only column that grows, so the others hold their 
declared widths exactly
      while `minWidth` remains the floor on narrow windows. `tableScrollX` now 
counts `minWidth`
      towards the total, otherwise the unsized column falls back to the 120px 
default and the
      computed minimum comes out too small.
    - Concentrating the whole surplus in one column overshoots when the fixed 
columns are narrow —
      a K8s cluster name reached 492px. Tables with several long-text fields 
now share the surplus
      between two or three of them, and the flexible column is the field that 
benefits from the
      width (a certificate issuer DN, a DLQ topic, a message ID) rather than 
whichever column
      happened to come first.
    
    Columns that render two lines keep a fixed width: column-level `ellipsis` 
forces `nowrap` on
    the cell and would collapse them.
    
    `tableLayout="fixed"` is now passed explicitly at every `tableScrollX` call 
site so the
    invariant no longer depends on a column incidentally being ellipsized, and 
long-name columns
    get `ellipsis` plus a tooltip carrying the full value. The consumer group 
table regains the
    modification-time column that an earlier width-trimming attempt had 
dropped; with the surplus
    handled properly the eleven columns fit without a horizontal scrollbar.
    
    The delete action on the K8s certificate table was the only table action in 
the app styled as a
    link button; it now matches the outlined style used by the other tables.
---
 web/src/pages/cluster/certs.tsx              | 10 ++++---
 web/src/pages/cluster/clients.tsx            |  6 ++++-
 web/src/pages/cluster/index.tsx              |  8 ++++--
 web/src/pages/instance/acl.tsx               | 18 ++++++++-----
 web/src/pages/instance/consumer.tsx          | 26 ++++++++++++++++---
 web/src/pages/instance/dlq.tsx               | 22 +++++++++++-----
 web/src/pages/instance/message.tsx           |  8 +++---
 web/src/pages/instance/topic.tsx             |  9 +++++--
 web/src/pages/ops/AuditRiskInsights.tsx      |  2 ++
 web/src/pages/ops/alerts.tsx                 |  1 +
 web/src/pages/ops/audit.tsx                  |  1 +
 web/src/pages/ops/notificationDeliveries.tsx |  4 ++-
 web/src/pages/studio/UserManagement.tsx      |  1 +
 web/src/utils/table.test.ts                  | 10 +++++--
 web/src/utils/table.ts                       | 39 +++++++++++++++++++++++-----
 15 files changed, 127 insertions(+), 38 deletions(-)

diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index 10b6b78c7..75012e533 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -170,7 +170,9 @@ const K8sCertsPage = () => {
       title: '签发者',
       dataIndex: 'issuer',
       key: 'issuer',
-      width: 180,
+      // 唯一可伸展列:余量集中在此,其余列保持声明宽度。选签发者而非集群名,
+      // 是因为它的内容是 CN=...,OU=...,O=... 这种长 DN,真正需要额外宽度。
+      minWidth: 180,
       sorter: (a, b) => (a.issuer ?? '').localeCompare(b.issuer ?? ''),
       render: (issuer: string | null) => issuer || '-',
       ellipsis: true,
@@ -216,7 +218,7 @@ const K8sCertsPage = () => {
           expiring: { color: 'orange', label: '即将过期' },
           expired: { color: 'red', label: '已过期' },
         };
-        const cfg = status ? map[status] ?? { color: 'default', label: status 
} : null;
+        const cfg = status ? (map[status] ?? { color: 'default', label: status 
}) : null;
         return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : '-';
       },
     },
@@ -234,10 +236,9 @@ const K8sCertsPage = () => {
           okButtonProps={{ danger: true }}
         >
           <Button
-            type="link"
             size="small"
-            danger
             icon={<DeleteOutlined />}
+            style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
             loading={deletingId === cert.id}
           >
             删除
@@ -288,6 +289,7 @@ const K8sCertsPage = () => {
           loading={loading}
           pagination={{ pageSize: 20 }}
           size="small"
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(certColumns) }}
         />
       </Card>
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 6dcd69ae9..2e5022976 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -390,7 +390,8 @@ const ClientsPage = () => {
       title: t('clients.clientId'),
       dataIndex: 'clientId',
       key: 'clientId',
-      width: 260,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 260,
       ellipsis: true,
       render: (id?: string | null) => (
         <Text
@@ -862,6 +863,7 @@ const ClientsPage = () => {
           rowKey="id"
           pagination={false}
           size="small"
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(resourceColumns) }}
           locale={{ emptyText: t('common.noData') }}
           style={{ marginBottom: 12 }}
@@ -874,6 +876,7 @@ const ClientsPage = () => {
             rowKey="id"
             pagination={false}
             size="small"
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(issueColumns) }}
             style={{ marginBottom: 12 }}
           />
@@ -911,6 +914,7 @@ const ClientsPage = () => {
             setCurrentPage(pagination.current ?? 1);
             setPageSize(pagination.pageSize ?? 20);
           }}
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns) }}
           pagination={{
             current: clampedCurrentPage,
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index 021fcc67a..5c2ba31ad 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -1299,6 +1299,7 @@ const ClusterPage = () => {
             rowKey="addr"
             pagination={{ pageSize: 20 }}
             size="small"
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(brokerColumns) }}
           />
         </Card>
@@ -1518,6 +1519,7 @@ const ClusterPage = () => {
             rowKey="id"
             pagination={{ pageSize: 20 }}
             size="small"
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(registryColumns) }}
           />
         </Card>
@@ -1552,7 +1554,7 @@ const ClusterPage = () => {
         title: t('cluster.brokerClusterName'),
         dataIndex: 'nsClusterName',
         key: 'nsClusterName',
-        width: 160,
+        minWidth: 160,
         sorter: (a, b) => a.nsClusterName.localeCompare(b.nsClusterName),
         render: (name: string) => (
           <Text strong style={{ fontSize: 14 }}>
@@ -1564,7 +1566,8 @@ const ClusterPage = () => {
         title: t('cluster.proxyAddr'),
         dataIndex: 'addr',
         key: 'addr',
-        width: 200,
+        // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+        minWidth: 200,
         sorter: (a, b) => compareText(a.addr, b.addr),
         render: (addr: string | null) => <Text style={{ fontSize: 14 
}}>{safeText(addr)}</Text>,
       },
@@ -1673,6 +1676,7 @@ const ClusterPage = () => {
             rowKey={(r) => `${r.clusterName}-${r.addr}`}
             pagination={{ pageSize: 20 }}
             size="small"
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(proxyColumns) }}
           />
         </Card>
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index 0b511ffd6..2a366cf47 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -581,7 +581,8 @@ const AclPageContent = ({
       title: t('acl.principal'),
       dataIndex: 'principal',
       key: 'principal',
-      width: 200,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 200,
       sorter: (a, b) => a.principal.localeCompare(b.principal),
       render: (text: string) => (
         <Space size={6}>
@@ -599,7 +600,7 @@ const AclPageContent = ({
     {
       title: t('acl.resource'),
       key: 'resource',
-      width: 240,
+      minWidth: 240,
       sorter: (a, b) => a.resource.localeCompare(b.resource),
       render: (_: unknown, record: AclRule) => (
         <Space size={6}>
@@ -715,7 +716,8 @@ const AclPageContent = ({
       title: t('acl.username'),
       dataIndex: 'username',
       key: 'username',
-      width: 200,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 200,
       sorter: (a, b) => a.username.localeCompare(b.username),
       render: (text: string, record: NormalizedAclUser) => (
         <Space size={6}>
@@ -1014,7 +1016,7 @@ const AclPageContent = ({
     {
       title: t('acl.riskItem'),
       key: 'item',
-      width: 260,
+      minWidth: 260,
       render: (_: unknown, record) => (
         <Space direction="vertical" size={2}>
           <Typography.Text strong>{record.title}</Typography.Text>
@@ -1038,7 +1040,7 @@ const AclPageContent = ({
       title: t('acl.riskEvidence'),
       dataIndex: 'evidence',
       key: 'evidence',
-      width: 220,
+      minWidth: 220,
       render: (evidence: string[]) => (
         <Space size={4} wrap>
           {evidence.length === 0 ? (
@@ -1057,7 +1059,8 @@ const AclPageContent = ({
       title: t('acl.riskRecommendation'),
       dataIndex: 'recommendation',
       key: 'recommendation',
-      width: 280,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 280,
       render: (text: string) => <Typography.Text>{text}</Typography.Text>,
     },
   ];
@@ -1205,6 +1208,7 @@ const AclPageContent = ({
                       },
                     }}
                     size="small"
+                    tableLayout="fixed"
                     scroll={{ x: tableScrollX(ruleColumns) }}
                   />
                 </div>
@@ -1258,6 +1262,7 @@ const AclPageContent = ({
                       },
                     }}
                     size="small"
+                    tableLayout="fixed"
                     scroll={{ x: tableScrollX(userColumns) }}
                   />
                 </div>
@@ -1399,6 +1404,7 @@ const AclPageContent = ({
                               rowKey="id"
                               pagination={false}
                               size="small"
+                              tableLayout="fixed"
                               scroll={{ x: tableScrollX(aclRiskColumns) }}
                               style={{ marginBottom: 12 }}
                             />
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 08eb5c523..7f2a8661a 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -874,10 +874,15 @@ const ConsumerPageContent = ({
       title: 'Group 名称',
       dataIndex: 'name',
       key: 'name',
-      width: 190,
+      // `minWidth` rather than `width`: this is the one column allowed to 
grow, so a window
+      // wider than the table does not inflate every other column by the same 
proportion.
+      // 170 keeps the total at the container width of a 1560px window, so the 
table fits
+      // without a horizontal scrollbar there; on wider windows this column 
takes the surplus.
+      minWidth: 170,
+      ellipsis: true,
       sorter: (a, b) => a.name.localeCompare(b.name),
       render: (name: string) => (
-        <Tooltip title="点击复制名称">
+        <Tooltip title={`${name}(点击复制)`}>
           <Text
             strong
             style={{ fontSize: 14, cursor: 'pointer' }}
@@ -966,6 +971,8 @@ const ConsumerPageContent = ({
       title: '创建时间',
       dataIndex: 'gmtCreate',
       key: 'gmtCreate',
+      // 156 = the 140px `YYYY-MM-DD HH:mm:ss` label at 14px plus the 
small-table cell padding;
+      // anything narrower truncates the timestamp.
       width: 156,
       sorter: (a, b) => (a.gmtCreate ?? '').localeCompare(b.gmtCreate ?? ''),
       render: (d: string) => (
@@ -989,7 +996,7 @@ const ConsumerPageContent = ({
     {
       title: '操作',
       key: 'actions',
-      width: 232,
+      width: 248,
       render: (_: unknown, record: ConsumerGroup) => (
         <Flex gap={6} justify="flex-end">
           <Button
@@ -1552,6 +1559,7 @@ const ConsumerPageContent = ({
             },
           }}
           size="small"
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns, { selection: true, expandable: 
true }) }}
           expandable={{
             onExpand: (expanded, record) => {
@@ -1725,12 +1733,18 @@ const ConsumerPageContent = ({
                       <Descriptions.Item label="最大重试次数">
                         <Text strong>{selectedGroup.retryMaxTimes}</Text> 次
                       </Descriptions.Item>
-                      <Descriptions.Item label="创建时间" span={2}>
+                      <Descriptions.Item label="创建时间">
                         <Space size={4}>
                           <Clock size={13} color="#9CA3AF" />
                           <Text 
type="secondary">{selectedGroup.gmtCreate}</Text>
                         </Space>
                       </Descriptions.Item>
+                      <Descriptions.Item label="修改时间">
+                        <Space size={4}>
+                          <Clock size={13} color="#9CA3AF" />
+                          <Text 
type="secondary">{selectedGroup.gmtModified}</Text>
+                        </Space>
+                      </Descriptions.Item>
                       <Descriptions.Item label="订阅 Topic" span={2}>
                         <Space size={4} wrap>
                           {(selectedGroup.subscribedTopics ?? []).map((t) => (
@@ -1756,6 +1770,7 @@ const ConsumerPageContent = ({
                         rowKey="clientId"
                         pagination={false}
                         size="small"
+                        tableLayout="fixed"
                         scroll={{ x: tableScrollX(instanceColumns) }}
                       />
                     </div>
@@ -1958,6 +1973,7 @@ const ConsumerPageContent = ({
                         rowKey="id"
                         pagination={false}
                         size="small"
+                        tableLayout="fixed"
                         scroll={{ x: tableScrollX(healthIssueColumns) }}
                       />
                     ) : (
@@ -2057,6 +2073,7 @@ const ConsumerPageContent = ({
                       rowKey={(r) => `${r.topic}-${r.broker}-${r.queueId}`}
                       pagination={false}
                       size="small"
+                      tableLayout="fixed"
                       scroll={{ x: tableScrollX(queueColumns), y: 380 }}
                       locale={{ emptyText: '消费组不在线,暂无队列进度数据' }}
                     />
@@ -2653,6 +2670,7 @@ const ConsumerPageContent = ({
                   rowKey={(row) => `${row.topic}-${row.broker}-${row.queueId}`}
                   pagination={false}
                   size="small"
+                  tableLayout="fixed"
                   scroll={{ x: tableScrollX(resetPreviewColumns), y: 260 }}
                   locale={{ emptyText: '未找到可预览的 Queue 位点' }}
                 />
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 03fa56033..9884c47e3 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -27,6 +27,7 @@ import {
   Modal,
   Drawer,
   DatePicker,
+  Tooltip,
   Typography,
   message,
 } from 'antd';
@@ -415,21 +416,28 @@ const DLQPage = () => {
       title: 'Group 名称',
       dataIndex: 'groupName',
       key: 'groupName',
-      width: 200,
+      minWidth: 200,
+      ellipsis: true,
       sorter: (a, b) => a.groupName.localeCompare(b.groupName),
       render: (name: string) => (
-        <Text strong style={{ fontSize: 14 }}>
-          {name}
-        </Text>
+        <Tooltip title={name}>
+          <Text strong style={{ fontSize: 14 }}>
+            {name}
+          </Text>
+        </Tooltip>
       ),
     },
     {
       title: 'DLQ Topic',
       dataIndex: 'dlqTopic',
       key: 'dlqTopic',
-      width: 240,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 240,
+      ellipsis: true,
       render: (topic: string) => (
-        <Text style={{ fontSize: 14, fontFamily: 'monospace' }}>{topic}</Text>
+        <Tooltip title={topic}>
+          <Text style={{ fontSize: 14, fontFamily: 'monospace' 
}}>{topic}</Text>
+        </Tooltip>
       ),
     },
     {
@@ -672,6 +680,7 @@ const DLQPage = () => {
             },
           }}
           size="small"
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
       </Card>
@@ -920,6 +929,7 @@ const DLQPage = () => {
                   }
                 },
               }}
+              tableLayout="fixed"
               scroll={{ x: tableScrollX(detailColumns, { selection: true }) }}
               columns={detailColumns}
             />
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 45fab7e49..a09b3049a 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -718,7 +718,8 @@ const MessagePageContent = ({
       title: 'Topic',
       dataIndex: 'topic',
       key: 'topic',
-      width: 170,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 170,
       ellipsis: true,
       sorter: (a, b) => a.topic.localeCompare(b.topic),
       render: (topic: string) => (
@@ -738,7 +739,7 @@ const MessagePageContent = ({
       title: 'Key',
       dataIndex: 'key',
       key: 'key',
-      width: 120,
+      minWidth: 120,
       ellipsis: true,
       render: (key: string | null) => (
         <span style={{ fontFamily: 'monospace', fontSize: 14 }}>{key || 
'-'}</span>
@@ -748,7 +749,7 @@ const MessagePageContent = ({
       title: 'Message ID',
       dataIndex: 'msgId',
       key: 'msgId',
-      width: 260,
+      minWidth: 260,
       render: (id: string) => (
         <Text
           copyable={{ text: id }}
@@ -1205,6 +1206,7 @@ const MessagePageContent = ({
               },
             }}
             size="small"
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(columns) }}
           />
         </Card>
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index a10ddb9c2..e47d7af91 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -678,7 +678,9 @@ const TopicPage = () => {
       title: 'Topic 名称',
       dataIndex: 'name',
       key: 'name',
-      width: 220,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 220,
+      ellipsis: true,
       sorter: (a, b) => a.name.localeCompare(b.name),
       render: (name: string) => (
         <Text strong style={{ fontSize: 14, display: 'block' }} ellipsis={{ 
tooltip: name }}>
@@ -690,7 +692,8 @@ const TopicPage = () => {
       title: '备注',
       dataIndex: 'remark',
       key: 'remark',
-      width: 200,
+      minWidth: 200,
+      ellipsis: true,
       sorter: (a, b) => (a.remark ?? '').localeCompare(b.remark ?? ''),
       render: (remark: string) => (
         <Text
@@ -1076,6 +1079,7 @@ const TopicPage = () => {
           pagination={false}
           size="small"
           loading={detailLoading}
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(routeColumns) }}
         />
       </>
@@ -1597,6 +1601,7 @@ const TopicPage = () => {
             },
           }}
           size="small"
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns, { selection: true }) }}
           onRow={(record) => ({
             onClick: () => void openDetail(record),
diff --git a/web/src/pages/ops/AuditRiskInsights.tsx 
b/web/src/pages/ops/AuditRiskInsights.tsx
index f16a7cf77..a26828098 100644
--- a/web/src/pages/ops/AuditRiskInsights.tsx
+++ b/web/src/pages/ops/AuditRiskInsights.tsx
@@ -293,6 +293,7 @@ const AuditRiskInsights = ({ summary, records, loading }: 
Props) => {
                 size="small"
                 rowKey="key"
                 dataSource={insights.hotTargets}
+                tableLayout="fixed"
                 scroll={{ x: tableScrollX(hotTargetColumns) }}
                 columns={hotTargetColumns}
                 pagination={false}
@@ -315,6 +316,7 @@ const AuditRiskInsights = ({ summary, records, loading }: 
Props) => {
                 size="small"
                 rowKey="id"
                 dataSource={insights.riskyRecords}
+                tableLayout="fixed"
                 scroll={{ x: tableScrollX(riskyRecordColumns) }}
                 columns={riskyRecordColumns}
                 pagination={false}
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index 3280abedb..c675badf0 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -1005,6 +1005,7 @@ const AlertsPage = ({ domain = 'CLUSTER' }: 
AlertsPageProps) => {
           loading={loading}
           rowSelection={rowSelection}
           pagination={false}
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
         <Flex justify="flex-end" style={{ padding: '16px' }}>
diff --git a/web/src/pages/ops/audit.tsx b/web/src/pages/ops/audit.tsx
index c0d45c7db..63f43a916 100644
--- a/web/src/pages/ops/audit.tsx
+++ b/web/src/pages/ops/audit.tsx
@@ -500,6 +500,7 @@ const AuditPage: React.FC = () => {
           dataSource={records}
           rowKey="id"
           loading={loading}
+          tableLayout="fixed"
           scroll={{ x: tableScrollX(columns) }}
           pagination={{
             current: page,
diff --git a/web/src/pages/ops/notificationDeliveries.tsx 
b/web/src/pages/ops/notificationDeliveries.tsx
index 952883a04..4e335ec49 100644
--- a/web/src/pages/ops/notificationDeliveries.tsx
+++ b/web/src/pages/ops/notificationDeliveries.tsx
@@ -149,7 +149,8 @@ const NotificationDeliveriesPage = () => {
     {
       title: t('deliveries.alert'),
       dataIndex: 'alertTitle',
-      width: 300,
+      // 唯一可伸展列:容器比表宽时余量集中在此,其余列保持声明宽度
+      minWidth: 300,
       render: (title, record) => (
         <Flex vertical gap={2}>
           <Typography.Text ellipsis={{ tooltip: title 
}}>{title}</Typography.Text>
@@ -275,6 +276,7 @@ const NotificationDeliveriesPage = () => {
             columns={columns}
             dataSource={items}
             loading={loading}
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(columns) }}
             pagination={{
               current: page,
diff --git a/web/src/pages/studio/UserManagement.tsx 
b/web/src/pages/studio/UserManagement.tsx
index 636a26654..7201415a7 100644
--- a/web/src/pages/studio/UserManagement.tsx
+++ b/web/src/pages/studio/UserManagement.tsx
@@ -484,6 +484,7 @@ const UserManagementPage = () => {
             loading={loading}
             columns={columns}
             dataSource={users}
+            tableLayout="fixed"
             scroll={{ x: tableScrollX(columns) }}
             pagination={{
               current: page,
diff --git a/web/src/utils/table.test.ts b/web/src/utils/table.test.ts
index d0b3df110..61f7742f6 100644
--- a/web/src/utils/table.test.ts
+++ b/web/src/utils/table.test.ts
@@ -25,10 +25,10 @@ describe('tableScrollX', () => {
   });
 
   it('reserves room for the selection and expand columns', () => {
-    expect(tableScrollX([{ width: 100 }], { selection: true })).toBe(140);
+    expect(tableScrollX([{ width: 100 }], { selection: true })).toBe(132);
     expect(tableScrollX([{ width: 100 }], { expandable: true })).toBe(148);
     expect(tableScrollX([{ width: 100 }], { selection: true, expandable: true, 
extra: 12 })).toBe(
-      200,
+      192,
     );
   });
 
@@ -37,6 +37,12 @@ describe('tableScrollX', () => {
     expect(tableScrollX([{ width: '80px' }])).toBe(80);
   });
 
+  it('counts the minWidth floor of a flexible column that carries no width', 
() => {
+    expect(tableScrollX([{ width: 100 }, { minWidth: 190 }])).toBe(290);
+    // An explicit width still wins, so a column may declare both.
+    expect(tableScrollX([{ width: 100, minWidth: 190 }])).toBe(100);
+  });
+
   it('adds up grouped children and skips hidden columns', () => {
     expect(tableScrollX([{ children: [{ width: 100 }, { width: 60 }] }, { 
width: 40 }])).toBe(200);
     expect(tableScrollX([{ width: 100 }, { width: 999, hidden: true 
}])).toBe(100);
diff --git a/web/src/utils/table.ts b/web/src/utils/table.ts
index bf8e0a06e..ff3b23071 100644
--- a/web/src/utils/table.ts
+++ b/web/src/utils/table.ts
@@ -18,19 +18,39 @@
 /**
  * Minimum table width helpers.
  *
- * Without `scroll.x` antd keeps `table-layout: auto` and squeezes every 
column into the
- * viewport, so on a narrow window (or after zooming in) the declared column 
widths are
- * ignored and cells wrap. Passing a numeric `scroll.x` switches the table to
- * `table-layout: fixed` with `min-width: 100%`, which keeps the columns 
readable and shows
- * a horizontal scrollbar when the window is narrower, while still stretching 
to fill a
+ * Without `scroll.x` antd keeps the table squeezed into the viewport, so on a 
narrow window (or
+ * after zooming in) the declared column widths are ignored and cells wrap. 
Passing a numeric
+ * `scroll.x` sets `width` plus `min-width: 100%` on the table, which keeps 
the columns readable
+ * and shows a horizontal scrollbar when the window is narrower, while still 
stretching to fill a
  * wide window.
  *
+ * `scroll.x` on its own is NOT enough to make the declared widths 
authoritative: rc-table only
+ * switches to `table-layout: fixed` when `scroll.y` is set, a column is 
`fixed`, or a column is
+ * `ellipsis`. Otherwise the table stays on `table-layout: auto`, where the 
widths are merely
+ * advisory and a single long cell value (a long topic or group name) 
stretches its column and
+ * pushes the real table width past the value computed here. Every table 
therefore also passes
+ * `tableLayout="fixed"` explicitly.
+ *
+ * Under `table-layout: fixed` the table still carries `min-width: 100%`, so 
on a window wider
+ * than this value the surplus is shared out across every column in proportion 
to its width —
+ * inflating narrow columns (a checkbox, an expand arrow, a `Push`/`Pull` tag) 
far beyond what
+ * their content needs. To keep the surplus in one place, give the primary 
text column
+ * `minWidth` instead of `width`: a column with no fixed `width` is the only 
one that grows,
+ * every other column keeps its declared width exactly, and `minWidth` remains 
its floor on
+ * narrow windows. That is why `minWidth` counts towards the total below.
+ *
  * The value is derived from the column definitions instead of being 
hard-coded so it stays
  * correct when columns are added, removed or resized.
  */
 
-/** Width antd reserves for the leading checkbox column (not part of 
`columns`). */
-const SELECTION_COLUMN_WIDTH = 40;
+/**
+ * Width antd reserves for the leading checkbox column (not part of `columns`).
+ *
+ * antd renders this column at 32px for `size="small"` tables, which is what 
every list page
+ * here uses. Reserving the 40px of a default-size table over-estimates the 
total by 8px and
+ * that alone is enough to force a horizontal scrollbar on a table that would 
otherwise fit.
+ */
+const SELECTION_COLUMN_WIDTH = 32;
 
 /** Width antd reserves for the leading expand-icon column (not part of 
`columns`). */
 const EXPAND_COLUMN_WIDTH = 48;
@@ -40,6 +60,8 @@ const UNSIZED_COLUMN_WIDTH = 120;
 
 interface ColumnLike {
   width?: number | string;
+  /** Floor for a flexible column that carries no fixed `width`. */
+  minWidth?: number;
   children?: readonly ColumnLike[];
   hidden?: boolean;
 }
@@ -67,6 +89,9 @@ function columnWidth(column: ColumnLike): number {
     const parsed = Number.parseFloat(column.width);
     return Number.isFinite(parsed) && column.width.endsWith('px') ? parsed : 
UNSIZED_COLUMN_WIDTH;
   }
+  if (typeof column.minWidth === 'number') {
+    return column.minWidth;
+  }
   return UNSIZED_COLUMN_WIDTH;
 }
 

Reply via email to