feat(chat): markdown 表格改横线风格并按宽度智能换行/滚动

- 表格去除外框/圆角/底色/竖线,仅保留横线(表头 1.5px --border-strong、行 1px --border-default),首列与正文对齐
- 新增 utils/tableLayout.ts:测量表格内容自然宽度,≤ 容器×1.35 时换行显示,超出退化为整表横向滚动;ResizeObserver 随容器宽度变化重判
- MarkdownRenderer 渲染后挂载判定(onMounted/onUpdated/watch content)
- 多智能体气泡表格同步同一风格;dark/light 变体移除表格背景/边框补偿

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
This commit is contained in:
JOJO 2026-09-09 19:42:33 +08:00
parent 1135afd654
commit a50a793306
3 changed files with 197 additions and 70 deletions

View File

@ -23,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, nextTick, onMounted, onUpdated, ref, watch } from 'vue'; import { computed, inject, nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from 'vue';
import CodeBlock from './CodeBlock.vue'; import CodeBlock from './CodeBlock.vue';
import { import {
parseMarkdownSegments, parseMarkdownSegments,
@ -32,6 +32,7 @@ import {
type MarkdownSegment type MarkdownSegment
} from '@/composables/useMarkdownRenderer'; } from '@/composables/useMarkdownRenderer';
import { chunkRenderedHtml, type HtmlChunk } from '@/utils/htmlChunks'; import { chunkRenderedHtml, type HtmlChunk } from '@/utils/htmlChunks';
import { classifyMarkdownTables, observeMarkdownTables } from '@/utils/tableLayout';
import { enhanceCitationChips, type CitationAnnotation } from './citationChips'; import { enhanceCitationChips, type CitationAnnotation } from './citationChips';
defineOptions({ name: 'MarkdownRenderer' }); defineOptions({ name: 'MarkdownRenderer' });
@ -105,6 +106,27 @@ function enhanceCitations() {
onMounted(enhanceCitations); onMounted(enhanceCitations);
onUpdated(enhanceCitations); onUpdated(enhanceCitations);
watch(() => [props.citations, props.citationsFinal], enhanceCitations); watch(() => [props.citations, props.citationsFinal], enhanceCitations);
// /
let stopTableObserver: (() => void) | null = null;
function classifyTables() {
nextTick(() => {
const el = containerRef.value;
if (!el) return;
classifyMarkdownTables(el);
stopTableObserver?.();
stopTableObserver = observeMarkdownTables(el);
});
}
onMounted(classifyTables);
onUpdated(classifyTables);
watch(() => props.content, classifyTables);
onBeforeUnmount(() => {
stopTableObserver?.();
stopTableObserver = null;
});
</script> </script>
<style scoped> <style scoped>

View File

@ -872,17 +872,22 @@
background: transparent; background: transparent;
} }
/* 与正文表格同一横线风格无外框/无竖线/无底色块
布局模式换行/横向滚动 utils/tableLayout.ts 判定 */
.user-message.user-message--multi-agent .bubble-text .md-table-scroll, .user-message.user-message--multi-agent .bubble-text .md-table-scroll,
.user-message.user-message--multi-agent .bubble-text [data-md-table-scroll='1'] { .user-message.user-message--multi-agent .bubble-text [data-md-table-scroll='1'] {
display: block; display: block;
width: auto; width: auto;
max-width: none; max-width: none;
margin: 10px 0; margin: 10px 0;
overflow: hidden;
}
.user-message.user-message--multi-agent .bubble-text .md-table-scroll[data-md-table-mode='scroll'],
.user-message.user-message--multi-agent .bubble-text [data-md-table-scroll='1'][data-md-table-mode='scroll'] {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
border: 1px solid var(--border-default); -webkit-overflow-scrolling: touch;
border-radius: 8px;
background: color-mix(in srgb, var(--text-primary) 4%, var(--chip-bg));
} }
.user-message.user-message--multi-agent .bubble-text .md-table-scroll > table, .user-message.user-message--multi-agent .bubble-text .md-table-scroll > table,
@ -890,18 +895,38 @@
margin: 0; margin: 0;
} }
.user-message.user-message--multi-agent .bubble-text [data-md-table-mode='scroll'] > table {
width: max-content;
min-width: 100%;
}
.user-message.user-message--multi-agent .bubble-text [data-md-table-mode='scroll'] > table td {
white-space: nowrap;
}
.user-message.user-message--multi-agent .bubble-text th, .user-message.user-message--multi-agent .bubble-text th,
.user-message.user-message--multi-agent .bubble-text td { .user-message.user-message--multi-agent .bubble-text td {
border: 1px solid var(--border-default); border: none;
padding: 6px 10px; padding: 8px 14px 8px 0;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: top;
font-size: 14px; font-size: 14px;
} }
.user-message.user-message--multi-agent .bubble-text th { .user-message.user-message--multi-agent .bubble-text th:last-child,
.user-message.user-message--multi-agent .bubble-text td:last-child {
padding-right: 0;
}
.user-message.user-message--multi-agent .bubble-text thead th {
font-weight: 600; font-weight: 600;
background: color-mix(in srgb, var(--text-primary) 7%, var(--chip-bg)); white-space: nowrap;
border-bottom: 1.5px solid var(--border-strong);
}
.user-message.user-message--multi-agent .bubble-text tbody td {
border-bottom: 1px solid var(--border-default);
overflow-wrap: break-word;
} }
.message-block--last .message-block--last
@ -2187,12 +2212,14 @@ show-html[data-rendered='1'] {
animation: none !important; /* 防止外部动画样式影响代码块,导致视觉抖动 */ animation: none !important; /* 防止外部动画样式影响代码块,导致视觉抖动 */
} }
/* 表格清爽横线风格无外框/无竖线/无底色块
布局模式由 utils/tableLayout.ts 按内容自然宽度判定
默认换行模式表格占满内容区单元格折行
自然宽度超过阈值时容器打 data-md-table-mode='scroll'退化为整表横向滚动 */
.text-output .text-content table { .text-output .text-content table {
width: max-content; width: 100%;
min-width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 0; margin: 0;
background: var(--theme-surface-soft);
} }
.text-output .text-content .md-table-scroll, .text-output .text-content .md-table-scroll,
@ -2200,14 +2227,16 @@ show-html[data-rendered='1'] {
display: block; display: block;
width: auto; width: auto;
max-width: none; max-width: none;
margin: 16px calc(var(--chat-content-x) * -1); margin: 12px calc(var(--chat-content-x) * -1);
padding: 0 var(--chat-content-x);
overflow: hidden;
}
.text-output .text-content .md-table-scroll[data-md-table-mode='scroll'],
.text-output .text-content [data-md-table-scroll='1'][data-md-table-mode='scroll'] {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
border: 1px solid var(--border-default);
border-radius: 12px;
background: var(--theme-surface-soft);
box-shadow: none;
} }
.text-output .text-content .md-table-scroll > table, .text-output .text-content .md-table-scroll > table,
@ -2215,47 +2244,62 @@ show-html[data-rendered='1'] {
margin: 0; margin: 0;
} }
.text-output .text-content .md-table-scroll::-webkit-scrollbar, .text-output .text-content [data-md-table-mode='scroll'] > table {
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar { width: max-content;
height: 10px; min-width: 100%;
} }
.text-output .text-content .md-table-scroll::-webkit-scrollbar-track, .text-output .text-content [data-md-table-mode='scroll'] > table td {
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar-track { white-space: nowrap;
background: color-mix(in srgb, var(--theme-surface-muted) 70%, transparent);
border-radius: 999px;
margin: 0 8px 6px;
} }
.text-output .text-content .md-table-scroll::-webkit-scrollbar-thumb, /* 细滚动条仅在滚动模式出现 */
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar-thumb { .text-output .text-content .md-table-scroll[data-md-table-mode='scroll']::-webkit-scrollbar,
background: color-mix(in srgb, var(--text-secondary) 55%, transparent); .text-output .text-content [data-md-table-scroll='1'][data-md-table-mode='scroll']::-webkit-scrollbar {
height: 8px;
}
.text-output .text-content .md-table-scroll[data-md-table-mode='scroll']::-webkit-scrollbar-track,
.text-output .text-content [data-md-table-scroll='1'][data-md-table-mode='scroll']::-webkit-scrollbar-track {
background: transparent;
}
.text-output .text-content .md-table-scroll[data-md-table-mode='scroll']::-webkit-scrollbar-thumb,
.text-output .text-content [data-md-table-scroll='1'][data-md-table-mode='scroll']::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--text-secondary) 45%, transparent);
border-radius: 999px; border-radius: 999px;
} }
.text-output .text-content .md-table-scroll, .text-output .text-content .md-table-scroll[data-md-table-mode='scroll'],
.text-output .text-content [data-md-table-scroll='1'] { .text-output .text-content [data-md-table-scroll='1'][data-md-table-mode='scroll'] {
scrollbar-color: color-mix(in srgb, var(--text-secondary) 55%, transparent) scrollbar-color: color-mix(in srgb, var(--text-secondary) 45%, transparent) transparent;
color-mix(in srgb, var(--theme-surface-muted) 70%, transparent);
scrollbar-width: thin; scrollbar-width: thin;
} }
.text-output .text-content thead {
background: var(--theme-chip-bg);
}
.text-output .text-content th, .text-output .text-content th,
.text-output .text-content td { .text-output .text-content td {
border: 1px solid var(--border-default); border: none;
padding: 10px 14px; padding: 11px 18px 11px 0;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: top;
font-size: 14px; font-size: 14px;
} }
.text-output .text-content th { .text-output .text-content th:last-child,
.text-output .text-content td:last-child {
padding-right: 0;
}
.text-output .text-content thead th {
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap;
border-bottom: 1.5px solid var(--border-strong);
}
.text-output .text-content tbody td {
border-bottom: 1px solid var(--border-default);
overflow-wrap: break-word;
} }
.text-output .text-content hr { .text-output .text-content hr {
@ -2690,14 +2734,7 @@ body[data-theme='dark'] {
color: var(--text-primary); color: var(--text-primary);
} }
/* 表格 */ /* 表格为无底色横线风格,三主题通用,无需覆盖 */
.text-output .text-content table {
background: var(--badge-bg);
}
.text-output .text-content thead {
background: var(--surface-panel);
}
/* 滚动锁定按钮 */ /* 滚动锁定按钮 */
.scroll-lock-btn { .scroll-lock-btn {
@ -2748,9 +2785,10 @@ body[data-theme='dark'] {
/* ========== 明亮主题变体(对话区组件) ========== */ /* ========== 明亮主题变体(对话区组件) ========== */
/* 背景浅色主题表面 token 阶梯近乎纯平base/card/raised 全为 #ffffff /* 背景浅色主题表面 token 阶梯近乎纯平base/card/raised 全为 #ffffff
基础样式中代码块白底落白底表格 #f9f9f9 落白底均不可见 基础样式中代码块白底落白底不可见
参照上方深色覆盖块的做法按用户确认的方向做组件级补偿 参照上方深色覆盖块的做法按用户确认的方向做组件级补偿
代码块仅头部着色代码区与对话底保持同色表格白体+浅灰表头+可见边框 */ 代码块仅头部着色代码区与对话底保持同色
表格为无底色横线风格三主题通用无需补偿 */
body[data-theme='light'] { body[data-theme='light'] {
/* 代码块 - 头部浅灰、代码区与对话底同为白色,外框加强到可见 */ /* 代码块 - 头部浅灰、代码区与对话底同为白色,外框加强到可见 */
.code-block-wrapper { .code-block-wrapper {
@ -2761,24 +2799,4 @@ body[data-theme='light'] {
background: var(--surface-muted); background: var(--surface-muted);
border-bottom-color: var(--border-strong); border-bottom-color: var(--border-strong);
} }
/* 表格 - 白体 + 浅灰表头 + 可见边框 */
.text-output .text-content table {
background: var(--surface-raised);
}
.text-output .text-content thead {
background: var(--surface-muted);
}
.text-output .text-content th,
.text-output .text-content td {
border-color: var(--border-strong);
}
.text-output .text-content .md-table-scroll,
.text-output .text-content [data-md-table-scroll='1'] {
border-color: var(--border-strong);
background: var(--surface-raised);
}
} }

View File

@ -0,0 +1,87 @@
/**
* Markdown /
*
*
* - ×
* - > × data-md-table-mode="scroll"退
*
* styles/components/chat/_chat-area.scss
*/
/** 自然宽度超出可用宽度的该倍数以内时,仍用换行模式吸收(避免略宽一点的表被迫滚动) */
const SCROLL_RATIO_THRESHOLD = 1.35;
const MODE_ATTR = 'data-md-table-mode';
const TABLE_SCROLLER_SELECTOR = '.md-table-scroll, [data-md-table-scroll="1"]';
function measureNaturalWidth(table: HTMLTableElement): number {
// 同一帧内临时切到「自然宽度 + 全部不折行」布局测量,读完立即恢复——
// 浏览器不会在两次样式变更之间绘制,无闪烁
const prevWidth = table.style.width;
table.style.width = 'max-content';
const cells = table.querySelectorAll('th, td');
const savedWhiteSpace: string[] = [];
cells.forEach((cell, i) => {
const el = cell as HTMLElement;
savedWhiteSpace[i] = el.style.whiteSpace;
el.style.whiteSpace = 'nowrap';
});
const width = table.offsetWidth;
table.style.width = prevWidth;
cells.forEach((cell, i) => {
(cell as HTMLElement).style.whiteSpace = savedWhiteSpace[i];
});
return width;
}
function availableWidth(scroller: HTMLElement): number {
const style = getComputedStyle(scroller);
return (
scroller.clientWidth -
parseFloat(style.paddingLeft || '0') -
parseFloat(style.paddingRight || '0')
);
}
function classifyOne(scroller: HTMLElement): void {
const table = scroller.querySelector('table');
if (!table) return;
const avail = availableWidth(scroller);
// 容器不可见(折叠块/未激活标签页内)时宽度为 0跳过等可见后由 ResizeObserver 补判
if (avail <= 0) return;
const natural = measureNaturalWidth(table);
const mode = natural <= avail * SCROLL_RATIO_THRESHOLD ? 'wrap' : 'scroll';
if (scroller.getAttribute(MODE_ATTR) !== mode) {
scroller.setAttribute(MODE_ATTR, mode);
}
}
/** 扫描 root 下所有 markdown 表格滚动容器,按当前可用宽度重新判定布局模式(幂等) */
export function classifyMarkdownTables(root: ParentNode): void {
root.querySelectorAll<HTMLElement>(TABLE_SCROLLER_SELECTOR).forEach(classifyOne);
}
/**
* root /
*
*/
export function observeMarkdownTables(root: ParentNode): () => void {
if (typeof ResizeObserver === 'undefined') return () => {};
const scrollers = Array.from(root.querySelectorAll<HTMLElement>(TABLE_SCROLLER_SELECTOR));
if (!scrollers.length) return () => {};
const lastWidths = new WeakMap<HTMLElement, number>();
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const el = entry.target as HTMLElement;
const width = entry.contentRect.width;
const last = lastWidths.get(el);
if (last === undefined || Math.abs(width - last) > 1) {
lastWidths.set(el, width);
classifyOne(el);
}
}
});
scrollers.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}