diff --git a/tests/frontend/unit/edu/reformat.rules-summary.test.js b/tests/frontend/unit/edu/reformat.rules-summary.test.js new file mode 100644 index 0000000..cad1ea0 --- /dev/null +++ b/tests/frontend/unit/edu/reformat.rules-summary.test.js @@ -0,0 +1,123 @@ +// tests/frontend/unit/edu/reformat.rules-summary.test.js +// 目标期刊规则摘要行(data-testid="rf-rules-summary"): +// 1) 有规则字段 + 有 diff → 摘要 + "已修改 N 处" +// 2) 规则全 null + diff=0 → "无强制约束" + "样本已满足该刊要求,无需修改" +// 背景:GB/BMC PB/Rice 规则相同、Frontiers 几乎无规则,用户切换期刊看不到 diff 差异, +// 即使 diff=0 也要展示"该刊要求什么",消除"没生效"错觉。 +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createPinia, setActivePinia } from 'pinia'; +import ReformatPanel from '../../../../ui/src/components/edu/panels/ReformatPanel.vue'; + +vi.mock('../../../../ui/src/api/review.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getJournals: vi.fn().mockResolvedValue({ ok: true, items: [] }), + getDirectory: vi.fn(), + reformatTex: vi.fn(), + compileTex: vi.fn(), + }; +}); + +import { useEduStore } from '../../../../ui/src/stores/edu.js'; + +function mountWithResult(eduSeed) { + const pinia = createPinia(); + setActivePinia(pinia); + const edu = useEduStore(); + Object.assign(edu, eduSeed); + const wrapper = mount(ReformatPanel, { global: { plugins: [pinia] } }); + return { wrapper, edu }; +} + +function resultWith(rules, changes) { + return { + ok: true, + job_id: 'abc123def456', + modified_tex: 'x', + changes, + warnings: [], + meta: { + filename: 'paper.tex', + journal_name: 'Molecular Plant', + rules_source: 'official', + rules_note: 'journals.json 期刊专属规则', + rules, + }, + }; +} + +describe('ReformatPanel 目标期刊规则摘要', () => { + beforeEach(() => { + global.fetch = vi.fn().mockRejectedValue(new Error('no network')); + if (!URL.createObjectURL) URL.createObjectURL = vi.fn(() => 'blob:mock'); + if (!URL.revokeObjectURL) URL.revokeObjectURL = vi.fn(); + }); + + it('1) 有规则字段 + 有 diff → 摘要列出字段且标注"已修改 N 处"', async () => { + // Molecular Plant 真实规则形态:12pt / 1.5 / author_year(font/margin null 不显示) + const { wrapper } = mountWithResult({ + reformatStatus: 'ready', + reformatResult: resultWith( + { + min_font_pt: 12, min_margin_mm: null, min_line_spacing: 1.5, + refs_style: 'author_year', max_pages: null, min_figures: null, max_fonts: null, + }, + [ + { line: 1, original: 'a', modified: 'b', reason: 'r', rule_key: 'font_pt', action: 'modified' }, + { line: 3, original: 'a', modified: 'b', reason: 'r', rule_key: 'line_spacing', action: 'modified' }, + { line: 4, original: 'a', modified: 'b', reason: 'r', rule_key: 'bib_style', action: 'modified' }, + ], + ), + }); + await wrapper.vm.$nextTick(); + const el = wrapper.find('[data-testid="rf-rules-summary"]'); + expect(el.exists()).toBe(true); + const text = el.text(); + expect(text).toContain('1.5 倍行距'); + expect(text).toContain('作者-年份引用'); + expect(text).toContain('字号 ≥12pt'); + expect(text).not.toContain('null'); + expect(text).not.toContain('边距'); // min_margin_mm=null → 不显示 + expect(text).toContain('已修改 3 处'); + }); + + it('2) 规则全 null + diff=0 → "无强制约束" + "样本已满足该刊要求,无需修改"', async () => { + const { wrapper } = mountWithResult({ + reformatStatus: 'ready', + reformatResult: resultWith( + { + min_font_pt: null, min_margin_mm: null, min_line_spacing: null, + refs_style: null, max_pages: null, min_figures: null, max_fonts: null, + }, + [], + ), + }); + await wrapper.vm.$nextTick(); + const el = wrapper.find('[data-testid="rf-rules-summary"]'); + expect(el.exists()).toBe(true); + const text = el.text(); + expect(text).toContain('该刊官网未规定明确格式要求,无强制约束'); + expect(text).toContain('样本已满足该刊要求,无需修改'); + expect(text).not.toContain('已修改'); + }); + + it('3) 双倍行距 + Vancouver(GB/BMC/Rice 形态)→ 正确映射中文标签', async () => { + const { wrapper } = mountWithResult({ + reformatStatus: 'ready', + reformatResult: resultWith( + { + min_font_pt: null, min_margin_mm: null, min_line_spacing: 2.0, + refs_style: 'vancouver', max_pages: null, min_figures: null, max_fonts: null, + }, + [{ line: 3, original: 'a', modified: 'b', reason: 'r', rule_key: 'line_spacing', action: 'modified' }], + ), + }); + await wrapper.vm.$nextTick(); + const text = wrapper.find('[data-testid="rf-rules-summary"]').text(); + expect(text).toContain('双倍行距'); + expect(text).toContain('Vancouver 引用'); + expect(text).toContain('已修改 1 处'); + }); +}); diff --git a/ui/src/components/edu/panels/ReformatPanel.vue b/ui/src/components/edu/panels/ReformatPanel.vue index 2b418fe..e3d0d2c 100644 --- a/ui/src/components/edu/panels/ReformatPanel.vue +++ b/ui/src/components/edu/panels/ReformatPanel.vue @@ -118,6 +118,31 @@ const structWarnings = computed(() => result.value?.warnings || []); const modifiedCount = computed(() => changes.value.filter((c) => c.action === 'modified').length); const suggestCount = computed(() => changes.value.filter((c) => c.action === 'warning').length); const sourceLabel = computed(() => SOURCE_LABEL[meta.value?.rules_source] || meta.value?.rules_source || '—'); +// 目标期刊规则摘要:只显示有值字段(null 不显示);全 null → 明确说明无强制约束。 +// 目的:diff=0 时(如 Frontiers / GB↔BMC 互切)也能看到"该刊要求什么",消除"没生效"错觉。 +const _REFS_LABEL = { vancouver: 'Vancouver 引用', apa: 'APA 引用', author_year: '作者-年份引用' }; +const rulesSummary = computed(() => { + const r = meta.value?.rules; + if (!r || typeof r !== 'object') return null; + const parts = []; + const ls = r.min_line_spacing; + if (typeof ls === 'number' && ls > 0) { + if (ls <= 1.05) parts.push('单倍行距'); + else if (Math.abs(ls - 1.5) < 0.01) parts.push('1.5 倍行距'); + else if (ls >= 1.95) parts.push('双倍行距'); + else parts.push(`行距 ≥${ls} 倍`); + } + if (r.refs_style && _REFS_LABEL[r.refs_style]) parts.push(_REFS_LABEL[r.refs_style]); + if (typeof r.min_font_pt === 'number') parts.push(`字号 ≥${r.min_font_pt}pt`); + if (typeof r.min_margin_mm === 'number') parts.push(`边距 ≥${r.min_margin_mm}mm`); + if (!parts.length) return '该刊官网未规定明确格式要求,无强制约束'; + return parts.join(' · '); +}); +// 与 diff 的关系:有修改 → "已修改 N 处";0 修改 → "样本已满足该刊要求,无需修改" +const rulesStatusText = computed(() => { + if (modifiedCount.value > 0) return `已修改 ${modifiedCount.value} 处`; + return '样本已满足该刊要求,无需修改'; +}); // ISO 时间戳 → "10-12 15:25"(内部字段不裸露;helper 规则见 UI 注释) const generatedShort = computed(() => { const s = meta.value?.generated_at; @@ -266,6 +291,11 @@ function download() { 复制任务 ID + +

+ 📋 该刊规则:{{ rulesSummary }} + ({{ rulesStatusText }}) +

{{ meta.rules_note }}

@@ -540,6 +570,15 @@ function download() { font-size: 12.5px; color: var(--pa-ink-muted); } +/* 规则摘要:次级文字(同 rf-meta 色阶),与汇总条的计数视觉区分 */ +.rf-rules { + margin: 4px 0 0; + font-size: 12.5px; + color: var(--pa-ink-muted); +} +.rf-rules-status { + color: var(--pa-ink-subtle); +} .rf-copy-id { margin-left: 8px; padding: 1px 8px;