feat: 完善模型用量趋势与全局手账卡片并补齐阶段验收
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import DiagramInteractions from '@/components/common/DiagramInteractions.vue'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Link } from '@element-plus/icons-vue'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
@@ -68,7 +69,7 @@ function renderDiagram(source: string, apply: (value: HTMLElement) => void) {
|
||||
if (entry.apply === apply) diagramPreviews.delete(id)
|
||||
}
|
||||
const element = createMermaidPreview(source, themeStore.isDark, apply)
|
||||
diagramPreviews.set(element.id, { source, apply })
|
||||
diagramPreviews.set(element.dataset.previewId!, { source, apply })
|
||||
return element
|
||||
}
|
||||
watch(() => themeStore.currentThemeId, () => {
|
||||
@@ -264,7 +265,7 @@ defineExpose({ getEditor: () => crepe?.editor })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="visual-editor">
|
||||
<DiagramInteractions class="visual-editor">
|
||||
<div class="markdown-toolbar" role="toolbar" :aria-label="t('Markdown 格式工具栏', 'Markdown formatting toolbar')">
|
||||
<label class="toolbar-select heading-select" :title="t('设置标题级别', 'Set heading level')">
|
||||
<span class="format-glyph heading-glyph">H</span>
|
||||
@@ -313,7 +314,7 @@ defineExpose({ getEditor: () => crepe?.editor })
|
||||
</section>
|
||||
<div ref="editorRoot" />
|
||||
</div>
|
||||
</div>
|
||||
</DiagramInteractions>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { getMarkdown } from '@milkdown/kit/utils'
|
||||
import type { Editor } from '@milkdown/kit/core'
|
||||
import VisualMarkdownEditor from './VisualMarkdownEditor.vue'
|
||||
|
||||
vi.mock('@/services/mermaidService', () => ({ renderMermaid: vi.fn(async () => ({ svg: '<svg viewBox="0 0 400 200"><text>Diagram</text></svg>', warnings: [], width: 400, height: 200 })) }))
|
||||
let wrapper: VueWrapper
|
||||
afterEach(() => { wrapper?.unmount(); document.body.innerHTML = ''; vi.unstubAllGlobals() })
|
||||
it('keeps diagram buttons usable after real Milkdown preview copying without changing Markdown', async () => {
|
||||
localStorage.clear()
|
||||
setActivePinia(createPinia())
|
||||
vi.stubGlobal('IntersectionObserver', class {
|
||||
constructor(private callback: IntersectionObserverCallback) {}
|
||||
observe(target: Element) { queueMicrotask(() => this.callback([{ isIntersecting: true, target } as IntersectionObserverEntry], this as unknown as IntersectionObserver)) }
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
})
|
||||
wrapper = mount(VisualMarkdownEditor, { props: { initialContent: '```mermaid\ngraph TD; A-->B\n```' }, attachTo: document.body })
|
||||
await vi.waitFor(() => expect(wrapper.find('[data-diagram-action="in"]').exists()).toBe(true), { timeout: 3000 })
|
||||
const editor = (wrapper.vm as unknown as { getEditor(): Editor }).getEditor()
|
||||
const before = editor.action(getMarkdown())
|
||||
await wrapper.get('[data-diagram-action="in"]').trigger('click')
|
||||
expect((wrapper.get('.editor-mermaid-preview svg').element as SVGSVGElement).style.width).toBe('480px')
|
||||
expect(editor.action(getMarkdown())).toBe(before)
|
||||
})
|
||||
@@ -23,7 +23,7 @@ it('renders SVG with the requested theme and keeps async revisions isolated', as
|
||||
expect(oldPublish).not.toHaveBeenCalled()
|
||||
expect(latestPublish).toHaveBeenCalledWith(latest)
|
||||
expect(latestPublish.mock.calls[0]![0]).not.toBe(latest)
|
||||
document.getElementById(latest.id)?.remove()
|
||||
document.body.replaceChildren()
|
||||
expect(renderMermaid).toHaveBeenLastCalledWith('graph TD; A-->C', { theme: 'dark' })
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ it('shows syntax errors as text without executing markup', async () => {
|
||||
vi.mocked(renderMermaid).mockResolvedValueOnce({ svg: '', warnings: ['<img src=x onerror=alert(1)>'], width: 0, height: 0 })
|
||||
const preview = createMermaidPreview('invalid', false, vi.fn())
|
||||
await flushPromises()
|
||||
expect(preview.classList.contains('has-error')).toBe(true)
|
||||
expect(preview.querySelector('.has-error')).not.toBeNull()
|
||||
expect(preview.querySelector('img')).toBeNull()
|
||||
expect(preview.textContent).toContain('点击编辑')
|
||||
})
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { renderMermaid } from '@/services/mermaidService'
|
||||
import { t } from '@/i18n'
|
||||
import { appendDiagramControls } from '@/utils/diagramControls'
|
||||
|
||||
let previewId = 0
|
||||
export function createMermaidPreview(source: string, dark: boolean, applyPreview: (value: HTMLElement) => void): HTMLElement {
|
||||
// Each revision owns its element, so a slow render cannot replace newer content.
|
||||
// Milkdown sanitizes Element input to its inner HTML; retain the revision
|
||||
// marker and controls inside an otherwise disposable envelope.
|
||||
const envelope = document.createElement('div')
|
||||
const container = document.createElement('div')
|
||||
envelope.append(container)
|
||||
container.className = 'editor-mermaid-preview'
|
||||
container.id = `editor-mermaid-preview-${++previewId}`
|
||||
envelope.dataset.previewId = container.id
|
||||
container.setAttribute('aria-live', 'polite')
|
||||
container.textContent = t('正在渲染图表…', 'Rendering diagram…')
|
||||
const publish = async () => {
|
||||
@@ -18,7 +24,7 @@ export function createMermaidPreview(source: string, dark: boolean, applyPreview
|
||||
if (visible) {
|
||||
// PreviewPanel copies HTML instead of retaining the supplied element.
|
||||
// Update the current copy through Milkdown's reactive callback.
|
||||
applyPreview(container.cloneNode(true) as HTMLElement)
|
||||
applyPreview(envelope.cloneNode(true) as HTMLElement)
|
||||
}
|
||||
}
|
||||
void renderMermaid(source, { theme: dark ? 'dark' : 'light' }).then(result => {
|
||||
@@ -30,10 +36,11 @@ export function createMermaidPreview(source: string, dark: boolean, applyPreview
|
||||
}
|
||||
// Mermaid runs in strict mode; Milkdown sanitizes the preview before insertion.
|
||||
container.innerHTML = result.svg
|
||||
appendDiagramControls(container)
|
||||
void publish()
|
||||
}).catch(() => {
|
||||
container.textContent = t('图表渲染失败,请点击编辑检查源码。', 'Unable to render diagram. Choose Edit to inspect the source.')
|
||||
void publish()
|
||||
})
|
||||
return container
|
||||
return envelope
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user