mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(docs): 修复长表滚动与筛选评审问题
This commit is contained in:
1 parent
e4804c305b
commit
761d5de69f
8 files changed
+84
-20
No files matched your search
@@ -1,6 +1,6 @@
|
||||
/** Default documentation theme with Markdown actions, long-table controls, and image and diagram viewers. */
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { onContentUpdated, useData, useRoute, type Theme } from 'vitepress'
|
||||
import { getScrollOffset, onContentUpdated, useData, useRoute, type Theme } from 'vitepress'
|
||||
import { defineComponent, h, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import type { MermaidViewer } from './mermaid-viewer.ts'
|
||||
import type { ImageViewer } from './image-viewer.ts'
|
||||
@@ -23,9 +23,18 @@ export default {
|
||||
let images: ImageViewer | undefined
|
||||
let media: MediaViewer | undefined
|
||||
let tables: DocumentTables | undefined
|
||||
let initialScroll: number | undefined
|
||||
let disposed = false
|
||||
onMounted(async () => {
|
||||
tables = new DocumentTables(document, () => lang.value)
|
||||
// VitePress computes the initial hash position before hydration inserts table controls.
|
||||
const anchor = document.querySelector(':target')
|
||||
if (anchor) initialScroll = window.requestAnimationFrame(() => {
|
||||
initialScroll = undefined
|
||||
if (!anchor.isConnected) return
|
||||
const top = window.scrollY + anchor.getBoundingClientRect().top - getScrollOffset()
|
||||
window.scrollTo(0, top + parseFloat(window.getComputedStyle(anchor).paddingTop))
|
||||
})
|
||||
const [{ installMermaidViewer }, { ImageViewer }, { MediaViewer }] = await Promise.all([
|
||||
import('./mermaid-viewer.ts'), import('./image-viewer.ts'), import('./media-viewer.ts'),
|
||||
])
|
||||
@@ -35,13 +44,13 @@ export default {
|
||||
images = new ImageViewer(document, () => lang.value, media)
|
||||
})
|
||||
onContentUpdated(() => { tables?.refresh() })
|
||||
watch(lang, () => { tables?.refresh() }, { flush: 'post' })
|
||||
watch([() => route.path, lang, isDark], () => {
|
||||
viewer?.refresh()
|
||||
images?.refresh()
|
||||
}, { flush: 'post' })
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true
|
||||
if (initialScroll !== undefined) window.cancelAnimationFrame(initialScroll)
|
||||
tables?.dispose()
|
||||
tables = undefined
|
||||
viewer?.dispose()
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
}
|
||||
|
||||
.dsh-table-toolbar button span {
|
||||
font-size: 18px;
|
||||
transform: rotate(45deg);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dsh-table-toolbar input:focus-visible,
|
||||
@@ -71,6 +71,14 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vp-doc .dsh-filterable-table tbody tr {
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.vp-doc .dsh-filterable-table tbody tr:nth-child(2n of :not([hidden])) {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dsh-filterable-table > thead {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Client-only filtering and page-scroll headers for original Markdown tables. */
|
||||
const messages = {
|
||||
en: { filter: 'Filter table', clear: 'Clear', empty: 'No matching rows.' },
|
||||
zh: { filter: '筛选表格', clear: '清空', empty: '没有匹配的行。' },
|
||||
en: { placeholder: 'Search...', filter: 'Filter table', clear: 'Clear', empty: 'No matching rows.' },
|
||||
zh: { placeholder: 'Search...', filter: '筛选表格', clear: '清空', empty: '没有匹配的行。' },
|
||||
}
|
||||
|
||||
class FilterableTable {
|
||||
@@ -35,7 +35,7 @@ class FilterableTable {
|
||||
this.#clear = doc.createElement('button')
|
||||
this.#clear.type = 'button'
|
||||
const clearIcon = doc.createElement('span')
|
||||
clearIcon.className = 'vpi-plus'
|
||||
clearIcon.textContent = '×'
|
||||
clearIcon.setAttribute('aria-hidden', 'true')
|
||||
this.#clear.append(clearIcon)
|
||||
this.#status = doc.createElement('p')
|
||||
@@ -60,7 +60,7 @@ class FilterableTable {
|
||||
}
|
||||
|
||||
localize(copy: typeof messages.en): void {
|
||||
this.#input.placeholder = 'Search...'
|
||||
this.#input.placeholder = copy.placeholder
|
||||
this.#input.setAttribute('aria-label', copy.filter)
|
||||
this.#clear.setAttribute('aria-label', copy.clear)
|
||||
this.#clear.title = copy.clear
|
||||
@@ -80,10 +80,14 @@ class FilterableTable {
|
||||
this.changed()
|
||||
}
|
||||
|
||||
positionHeader(top: number): void {
|
||||
measureHeaderOffset(top: number): number {
|
||||
return Math.min(
|
||||
Math.max(top - this.table.getBoundingClientRect().top, 0), Math.max(this.table.clientHeight - this.head.offsetHeight, 0),
|
||||
)
|
||||
}
|
||||
|
||||
positionHeader(offset: number): void {
|
||||
// Horizontal overflow makes the table the sticky scroll container; translate its own head to retain column alignment.
|
||||
const bounds = this.table.getBoundingClientRect()
|
||||
const offset = Math.min(Math.max(top - bounds.top, 0), Math.max(bounds.height - this.head.offsetHeight, 0))
|
||||
this.head.style.setProperty('--dsh-table-header-offset', `${offset}px`)
|
||||
}
|
||||
|
||||
@@ -145,12 +149,14 @@ export class DocumentTables {
|
||||
}
|
||||
|
||||
#schedule(): void {
|
||||
if (this.#frame !== undefined) return
|
||||
if (this.#tables.size === 0 || this.#frame !== undefined) return
|
||||
this.#frame = window.requestAnimationFrame(() => {
|
||||
this.#frame = undefined
|
||||
if (this.#tables.size === 0) return
|
||||
const navigation = this.doc.querySelectorAll('.VPNav, .VPLocalNav')
|
||||
const top = Math.max(0, ...Array.from(navigation, element => element.getBoundingClientRect().bottom))
|
||||
for (const table of this.#tables.values()) table.positionHeader(top)
|
||||
const positions = Array.from(this.#tables.values(), table => ({ table, offset: table.measureHeaderOffset(top) }))
|
||||
for (const { table, offset } of positions) table.positionHeader(offset)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,4 +22,4 @@ The default-theme extension shares one fullscreen modal and pan/zoom controller
|
||||
|
||||
Native code groups keep their radio controls in separate forms so copied search excerpts cannot clear the page's selected platform. Search excerpts and MPA builds display every command block because tab switching is unavailable; the [code-group adapter](.vitepress/code-groups.ts) and its [focused tests](tests/code-groups.spec.ts) own this behavior.
|
||||
|
||||
Long tables with at least ten data rows receive independent text filters and page-scroll headers. Filtering matches visible cell text, ignores case and surrounding query whitespace, and preserves original rows and links; clearing restores their order and visibility. Smaller tables stay unchanged. Keep filtering in the [theme adapter](.vitepress/theme/table-filter.ts), preserve native horizontal scrolling, and leave raw Markdown complete. The [focused tests](tests/table-filter.spec.ts) cover controls and lifecycle; verify header alignment in a browser.
|
||||
Long tables with at least ten data rows receive independent text filters and page-scroll headers. Filtering matches visible cell text, ignores case and surrounding query whitespace, and preserves original rows and links; clearing restores their order and visibility. Smaller tables stay unchanged. Keep filtering in the [theme adapter](.vitepress/theme/table-filter.ts), preserve native horizontal scrolling, and leave raw Markdown complete. Initial hash navigation accounts for controls inserted during hydration. The [focused tests](tests/table-filter.spec.ts) cover controls and lifecycle; verify header alignment and cold-open anchor links in a production browser build.
|
||||
@@ -1 +1 @@
|
||||
<div class="dsh-table-controls"><div class="dsh-table-toolbar"><input type="search" placeholder="Search..." aria-label="Filter table"><button type="button" aria-label="Clear" title="Clear"><span class="vpi-plus" aria-hidden="true"></span></button></div><p role="status">No matching rows.</p></div>
|
||||
<div class="dsh-table-controls"><div class="dsh-table-toolbar"><input type="search" placeholder="Search..." aria-label="Filter table"><button type="button" aria-label="Clear" title="Clear"><span aria-hidden="true">×</span></button></div><p role="status">No matching rows.</p></div>
|
||||
@@ -1 +1 @@
|
||||
<div class="dsh-table-controls"><div class="dsh-table-toolbar"><input type="search" placeholder="Search..." aria-label="筛选表格"><button type="button" aria-label="清空" title="清空"><span class="vpi-plus" aria-hidden="true"></span></button></div><p role="status">没有匹配的行。</p></div>
|
||||
<div class="dsh-table-controls"><div class="dsh-table-toolbar"><input type="search" placeholder="Search..." aria-label="筛选表格"><button type="button" aria-label="清空" title="清空"><span aria-hidden="true">×</span></button></div><p role="status">没有匹配的行。</p></div>
|
||||
@@ -7,7 +7,7 @@ import { fireEvent, getByRole, queryAllByRole, queryByRole, waitFor } from '@tes
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Theme from '../.vitepress/theme/index.ts'
|
||||
|
||||
vi.mock('vitepress', () => ({ useData: () => data, useRoute: () => route, onContentUpdated() {} }))
|
||||
vi.mock('vitepress', () => ({ useData: () => data, useRoute: () => route, onContentUpdated() {}, getScrollOffset: () => 64 }))
|
||||
vi.mock('vitepress/theme', () => ({ default: { Layout: {
|
||||
setup: (_props: unknown, { slots }: { slots: Slots }) => () => h('main', null, slots['doc-before']?.()),
|
||||
} } }))
|
||||
@@ -104,6 +104,37 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('page Markdown actions', () => {
|
||||
it('positions the initial hash after layout enhancements and cancels it on unmount', async () => {
|
||||
const anchor = document.createElement('h2')
|
||||
anchor.id = 'after-table'
|
||||
anchor.style.paddingTop = '16px'
|
||||
document.body.append(anchor)
|
||||
const url = location.href
|
||||
const state: unknown = history.state
|
||||
history.replaceState(null, '', '#after-table')
|
||||
const scroll = vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
|
||||
const frame = vi.spyOn(window, 'requestAnimationFrame').mockReturnValue(42)
|
||||
const cancel = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
||||
try {
|
||||
vi.spyOn(anchor, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 300, 100, 20))
|
||||
await mount()
|
||||
expect(scroll).not.toHaveBeenCalled()
|
||||
const callback = frame.mock.calls[0]?.[0]
|
||||
assert(callback)
|
||||
callback(0)
|
||||
expect(scroll).toHaveBeenCalledWith(0, 252)
|
||||
app?.unmount()
|
||||
await mount()
|
||||
app?.unmount()
|
||||
app = undefined
|
||||
expect(cancel).toHaveBeenCalledWith(42)
|
||||
} finally {
|
||||
anchor.remove()
|
||||
history.replaceState(state, '', url)
|
||||
vi.restoreAllMocks()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['en-US', 'zh-CN'])('preserves the accessible controls and status in %s', async (lang) => {
|
||||
data.lang.value = lang
|
||||
data.frontmatter.value = { rawMarkdownPath: `${lang === 'en-US' ? 'en/' : ''}guide/quickstart.md` }
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('documentation table filtering', () => {
|
||||
expect(getByRole(fixture.section, 'status').textContent).toBe('No matching rows.')
|
||||
expect(fixture.table.textContent).toBe(content)
|
||||
const clear = getByRole<HTMLButtonElement>(fixture.section, 'button', { name: 'Clear' })
|
||||
expect(clear.textContent).toBe('')
|
||||
expect(clear.textContent).toBe('×')
|
||||
clear.focus()
|
||||
clear.click()
|
||||
expect(visible(fixture.table)).toEqual(fixture.rows)
|
||||
@@ -181,7 +181,16 @@ describe('documentation table filtering', () => {
|
||||
expect(visible(previous.table)).toEqual(previous.rows)
|
||||
})
|
||||
|
||||
it('keeps the header below navigation and within the table after scroll, resize, and filtering', () => {
|
||||
it('skips layout work on pages without long tables', () => {
|
||||
renderTable(9)
|
||||
install()
|
||||
fireEvent.scroll(window)
|
||||
fireEvent.resize(window)
|
||||
observers[0]?.notify()
|
||||
expect(frames.size).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps the header below navigation and above the horizontal scrollbar after scroll, resize, and filtering', () => {
|
||||
const { table, section } = renderTable()
|
||||
const head = table.tHead
|
||||
assert(head)
|
||||
@@ -190,7 +199,8 @@ describe('documentation table filtering', () => {
|
||||
document.body.prepend(nav)
|
||||
const layout = { top: 200, height: 500, nav: 64 }
|
||||
vi.spyOn(nav, 'getBoundingClientRect').mockImplementation(() => new DOMRect(0, 0, 800, layout.nav))
|
||||
vi.spyOn(table, 'getBoundingClientRect').mockImplementation(() => new DOMRect(0, layout.top, 600, layout.height))
|
||||
vi.spyOn(table, 'getBoundingClientRect').mockImplementation(() => new DOMRect(0, layout.top, 600, layout.height + 15))
|
||||
vi.spyOn(table, 'clientHeight', 'get').mockImplementation(() => layout.height)
|
||||
vi.spyOn(head, 'offsetHeight', 'get').mockReturnValue(40)
|
||||
install()
|
||||
paint()
|
||||
|
||||
Reference in new issue
Block a user