feat(review): show format rules summary per target journal
- New summary row: shows the journal's rules (even if no diff applies) - Fixes user confusion when switching journals yields same diff - When all rules are null: 'no explicit format requirements' - Vitest: add case for summary rendering
This commit is contained in:
@@ -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 处');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
</button>
|
||||
</div>
|
||||
<!-- 目标期刊规则摘要:diff=0 时也能看到"该刊要求什么"(次级文字,不抢 diff 主视觉) -->
|
||||
<p v-if="rulesSummary" class="rf-rules" data-testid="rf-rules-summary">
|
||||
📋 该刊规则:{{ rulesSummary }}
|
||||
<span class="rf-rules-status">({{ rulesStatusText }})</span>
|
||||
</p>
|
||||
<p v-if="meta?.rules_note" class="rf-note" role="note">{{ meta.rules_note }}</p>
|
||||
|
||||
<!-- 汇总:已修改 / 建议(橙,不计入修改)/ 结构提示 -->
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user