Compare commits

...

2 Commits

Author SHA1 Message Date
a50a793306 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>
2026-09-09 19:42:33 +08:00
1135afd654 docs(AGENTS): 构建/lint 等命令沙箱失败时须申请权限,禁止绕过 2026-09-09 18:56:49 +08:00
4 changed files with 198 additions and 70 deletions

View File

@ -171,6 +171,7 @@
- 提交前至少做与改动相关的最小验证(命令输出或手工步骤要可复现)。
- 视觉验证默认由用户完成:除非用户特别说明,所有需要视觉确认的修改(尤其是动画/过渡效果),构建/lint 通过后交由用户亲自查看确认,不要用 Playwright 截图或浏览器自动化代替用户验收。
- 运行根目录前端构建时,默认使用 `npm run build --silent 2>&1 | tail -n 5`
- 构建/安装/Lint 类命令(如 `npm run build`、`npm install`、`npm run lint`)因沙箱权限失败时(如报 `EPERM` / `Operation not permitted` / 沙箱拒绝写入),直接向用户说明失败原因并申请权限或调整路径授权,禁止尝试任何绕过沙箱的做法(如换执行方式规避限制、向其他路径写入等)。
### CLI 当前交互约束2026-05-15

View File

@ -23,7 +23,7 @@
</template>
<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 {
parseMarkdownSegments,
@ -32,6 +32,7 @@ import {
type MarkdownSegment
} from '@/composables/useMarkdownRenderer';
import { chunkRenderedHtml, type HtmlChunk } from '@/utils/htmlChunks';
import { classifyMarkdownTables, observeMarkdownTables } from '@/utils/tableLayout';
import { enhanceCitationChips, type CitationAnnotation } from './citationChips';
defineOptions({ name: 'MarkdownRenderer' });
@ -105,6 +106,27 @@ function enhanceCitations() {
onMounted(enhanceCitations);
onUpdated(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>
<style scoped>

View File

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