去掉作者端 Article Proofreading (原步骤2)

This commit is contained in:
2026-06-17 17:02:14 +08:00
parent ea5695f913
commit 5a1bbb0894
8 changed files with 261 additions and 198 deletions

View File

@@ -19,8 +19,8 @@ const service = axios.create({
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
baseURL: '/api', //本地
// baseURL: '/', //正式
// baseURL: '/api', //本地
baseURL: '/', //正式
});

View File

@@ -2,14 +2,14 @@
//记得切换
//正式
// const mediaUrl = '/public/';
// const baseUrl = '/';
const mediaUrl = '/public/';
const baseUrl = '/';
//正式环境
const mediaUrl = 'https://submission.tmrjournals.com/public/';
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
const baseUrl = '/api'
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
// const baseUrl = '/api'
//测试环境

View File

@@ -1145,7 +1145,7 @@ const en = {
typesettingType1: 'Vertical A4',
AnnotationList: 'Annotation List',
Annotations: 'Comments',
exportWord: 'Export Word',
exportWord: 'Generate Word file',
exportManuscriptEmpty: 'No content to export.',
exportManuscriptSuccess: 'Word downloaded.',
exportManuscriptFail: 'Failed to export Word.',
@@ -1174,8 +1174,10 @@ const en = {
citeRelevanceBatchTip: '{n} references per batch within each paragraph',
citeRelevanceRefBatch: 'Batch refs {nums}',
citeRelevanceBatchProgress: 'Paragraph batch {current}/{total}',
citeRelevanceGroupTotal: '{n} paragraphs to review',
citeRelevanceGroupTotal: '{n} batches to review',
citeRelevanceGroupNavLabel: 'Para {paragraph} · {refs}',
citeRelevanceTableNavLabel: 'Table {paragraph} · {refs}',
citeRelevancePreviewTitle: 'Preview',
citeRelevanceJumpTo: 'Jump to',
citeRelevanceCopyGroup: 'Copy paragraph',
citeRelevanceDownloadHtml: 'Download HTML',

View File

@@ -1131,7 +1131,7 @@ const zh = {
typesettingType1: '竖向 A4',
AnnotationList: '批注列表',
Annotations: '批注',
exportWord: '导出 Word',
exportWord: '生成 Word 文件',
exportManuscriptEmpty: '没有可导出的内容。',
exportManuscriptSuccess: 'Word 已下载。',
exportManuscriptFail: 'Word 导出失败。',
@@ -1160,8 +1160,10 @@ const zh = {
citeRelevanceBatchTip: '每段正文按 {n} 条参考文献一批提问',
citeRelevanceRefBatch: '本批文献 {nums}',
citeRelevanceBatchProgress: '本段第 {current}/{total} 批',
citeRelevanceGroupTotal: '共 {n} 待核查',
citeRelevanceGroupTotal: '共 {n} 待核查',
citeRelevanceGroupNavLabel: '段落 {paragraph} · {refs}',
citeRelevanceTableNavLabel: '表格 {paragraph} · {refs}',
citeRelevancePreviewTitle: '内容预览',
citeRelevanceJumpTo: '快速跳转',
citeRelevanceCopyGroup: '复制本段',
citeRelevanceDownloadHtml: '下载 HTML',

View File

@@ -136,16 +136,16 @@
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
</div>
</div>
<div class="content_box mt20 stepbox">
<!-- 文章引用 -->
<!-- <div class="content_box mt20 stepbox">
<div class="con">
<h4 class="con-title">{{ this.$t('PreAccept.step2') }}</h4>
<p style="color: #505050; font-size: 14px; padding: 20px; box-sizing: border-box">
<el-button @click="goGenerateCharts(thisArtcleId)" icon="el-icon-edit" type="text">Edit</el-button>
</p>
<!-- <div v-else style="padding: 20px; box-sizing: border-box"></div> -->
</div>
</div>
</div> -->
<div class="content_box mt20 stepbox">
<!-- 文章引用 -->
<div class="con">

View File

@@ -92,49 +92,6 @@
></i>
{{ $t('commonTable.exportWord') }}
</li>
<li
v-if="zyModeEnabled"
@click="handleDownloadReferencesHtml"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-document base-margin"
:style="{ '--m-r': '2px' }"
v-if="!referencesHtmlLoading"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.refHtmlDownload') }}
</li>
<li
v-if="zyModeEnabled"
@click="triggerReferencesHtmlToWord"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-upload2 base-margin"
:style="{ '--m-r': '2px' }"
v-if="!exportingReferencesHtmlWord"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.refHtmlToWord') }}
</li>
<input
ref="referencesHtmlFileInput"
type="file"
accept=".html,text/html"
style="display: none"
@change="handleReferencesHtmlToWord"
/>
<li
v-if="zyModeEnabled"
@click="handleOpenCitationRelevance"
@@ -1144,11 +1101,6 @@ import { debounce, throttle } from '@/common/js/debounce';
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
import LatexDataPanel from './LatexDataPanel.vue';
import { downloadManuscriptWord, fetchManuscriptReferenceList } from '@/utils/exportManuscriptWord';
import {
buildReferencesHtmlLabels,
downloadReferencesEditableHtml,
parseReferencesEditableHtml
} from '@/utils/manuscriptReferenceHtml';
import {
buildCitationReviewQueue,
buildCitationReviewHtmlLabels,
@@ -1280,8 +1232,6 @@ export default {
scrollPosition: 0,
wordList: [],
exportingManuscriptWord: false,
referencesHtmlLoading: false,
exportingReferencesHtmlWord: false,
citationRelevanceLoading: false,
manuscriptReferences: [],
proofreadingList: [],
@@ -1574,104 +1524,6 @@ export default {
this.exportingManuscriptWord = false;
}
},
async handleDownloadReferencesHtml() {
if (this.referencesHtmlLoading) {
return;
}
if (!this.$api) {
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
return;
}
this.referencesHtmlLoading = true;
try {
let references = this.manuscriptReferences;
if (!references || !references.length) {
references = await fetchManuscriptReferenceList(this.$api, this.articleId, this.pArticleId);
this.manuscriptReferences = references || [];
}
if (!references || !references.length) {
this.$message.warning(this.$t('commonTable.refHtmlEmpty'));
return;
}
const labels = buildReferencesHtmlLabels(this.$t.bind(this));
const fileName = 'references-editor' + (this.articleId ? '-' + this.articleId : '') + '.html';
downloadReferencesEditableHtml(references, labels, fileName);
this.$message.success(this.$t('commonTable.refHtmlDownloadSuccess'));
} catch (err) {
console.error(err);
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
} finally {
this.referencesHtmlLoading = false;
}
},
triggerReferencesHtmlToWord() {
if (this.exportingReferencesHtmlWord) {
return;
}
const input = this.$refs.referencesHtmlFileInput;
if (input) {
input.value = '';
input.click();
}
},
handleReferencesHtmlToWord(event) {
const input = event && event.target;
const file = input && input.files && input.files[0];
if (!file) {
return;
}
if (!this.wordList || !this.wordList.length) {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
if (input) {
input.value = '';
}
return;
}
this.exportingReferencesHtmlWord = true;
const reader = new FileReader();
reader.onload = async () => {
try {
const referenceHtmlItems = parseReferencesEditableHtml(String(reader.result || ''));
if (!referenceHtmlItems.length) {
this.$message.warning(this.$t('commonTable.refHtmlParseEmpty'));
return;
}
await downloadManuscriptWord(this.wordList, this.mediaUrl, 'manuscript', {
fetchReferences: false,
referenceHtmlItems: referenceHtmlItems,
apiClient: this.$api,
articleId: this.articleId,
pArticleId: this.pArticleId
});
this.$message.success(this.$t('commonTable.refHtmlToWordSuccess'));
} catch (err) {
console.error(err);
if (err && err.message === 'NO_CONTENT') {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
} else {
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
}
} finally {
this.exportingReferencesHtmlWord = false;
if (input) {
input.value = '';
}
}
};
reader.onerror = () => {
this.exportingReferencesHtmlWord = false;
if (input) {
input.value = '';
}
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
};
reader.readAsText(file, 'UTF-8');
},
async handleOpenCitationRelevance() {
if (this.citationRelevanceLoading) {
return;

View File

@@ -1,6 +1,9 @@
/** 正文引用标记,如 [1]、[1-7]、<blue>[4,6,8]</blue> */
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
/** 每批最多分析的参考文献条数 */
export const CITATION_REVIEW_BATCH_SIZE = 4;
function decodeHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
@@ -117,17 +120,162 @@ export function formatReferencePlainText(ref) {
return parts.filter(Boolean).join('. ').replace(/\.\s*\./g, '.').trim();
}
function isTextParagraphItem(item) {
function isTableItem(item) {
return !!(item && item.type == 2 && item.table);
}
function isCitableReviewItem(item) {
if (!item) {
return false;
}
if (item.type == 1 || item.type == 2) {
if (item.type == 1) {
return false;
}
if (isTableItem(item)) {
return true;
}
const html = String(item.content || item.text || '').trim();
return !!html;
}
function forEachTableRowCells(row, fn) {
if (!row || !Array.isArray(row)) {
return;
}
row.forEach(function (cell) {
if (cell && typeof cell === 'object' && cell.text != null) {
fn(cell);
}
});
}
function collectTablePartsHtml(table) {
const parts = [];
if (!table) {
return '';
}
if (table.title) {
parts.push(String(table.title));
}
(table.tableHeader || []).forEach(function (row) {
forEachTableRowCells(row, function (cell) {
parts.push(String(cell.text || ''));
});
});
(table.tableContent || []).forEach(function (row) {
forEachTableRowCells(row, function (cell) {
parts.push(String(cell.text || ''));
});
});
if (table.note) {
parts.push(String(table.note));
}
return parts.join('\n');
}
function buildTablePlainText(table) {
const lines = [];
if (!table) {
return '';
}
if (table.title) {
lines.push(htmlToPlainText(table.title));
}
(table.tableHeader || []).forEach(function (row) {
const cells = [];
forEachTableRowCells(row, function (cell) {
cells.push(htmlToPlainText(cell.text));
});
if (cells.length) {
lines.push(cells.join(' | '));
}
});
(table.tableContent || []).forEach(function (row) {
const cells = [];
forEachTableRowCells(row, function (cell) {
cells.push(htmlToPlainText(cell.text));
});
if (cells.length) {
lines.push(cells.join(' | '));
}
});
if (table.note) {
lines.push(htmlToPlainText(table.note));
}
return lines.filter(Boolean).join('\n');
}
function getReviewItemSourceHtml(item) {
if (isTableItem(item)) {
return collectTablePartsHtml(item.table);
}
return String(item.content || item.text || '');
}
function getReviewItemPlainText(item) {
if (isTableItem(item)) {
return buildTablePlainText(item.table);
}
return htmlToPlainText(getReviewItemSourceHtml(item));
}
function buildTableRowPreviewHtml(row, oddRowIds) {
if (!row || !Array.isArray(row)) {
return '';
}
const isOdd = row.rowId && (oddRowIds || []).indexOf(row.rowId) !== -1;
let cellsHtml = '';
row.forEach(function (cell) {
if (!cell || typeof cell !== 'object') {
return;
}
cellsHtml +=
'<td rowspan="' +
(cell.rowspan || 1) +
'" colspan="' +
(cell.colspan || 1) +
'">' +
(cell.text || '') +
'</td>';
});
return '<tr class="' + (isOdd ? 'oddColor' : '') + '">' + cellsHtml + '</tr>';
}
function buildReviewTablePreviewHtml(item) {
const table = item.table;
if (!table) {
return '';
}
let tableRows = '';
(table.tableHeader || []).forEach(function (row) {
tableRows += buildTableRowPreviewHtml(row, []);
});
(table.tableContent || []).forEach(function (row) {
tableRows += buildTableRowPreviewHtml(row, table.oddRowIds || []);
});
return (
'<div class="cite-preview-table">' +
(table.title ? '<div class="cite-preview-table-title">' + table.title + '</div>' : '') +
'<table border="1"><tbody>' +
tableRows +
'</tbody></table>' +
(table.note ? '<div class="cite-preview-table-note">' + table.note + '</div>' : '') +
'</div>'
);
}
function buildReviewContentPreviewHtml(item) {
if (isTableItem(item)) {
return buildReviewTablePreviewHtml(item);
}
const html = String(item.content || item.text || '');
if (!html) {
return '';
}
return '<div class="cite-preview-paragraph">' + html + '</div>';
}
function buildCitationEntry(citeNum, refList) {
const ref = refList[citeNum - 1] || null;
return {
@@ -144,19 +292,40 @@ export function formatCitationNumsLabel(citeNums) {
}).join(' ');
}
function chunkCitationNumbers(citeNums, batchSize) {
const size = batchSize || CITATION_REVIEW_BATCH_SIZE;
const chunks = [];
const nums = citeNums || [];
let i = 0;
for (i = 0; i < nums.length; i += size) {
chunks.push(nums.slice(i, i + size));
}
return chunks;
}
export function getCitationReviewGroupMeta(item, index, labels) {
const l = labels || {};
const groupNo = index + 1;
const paragraphNo = item.paragraphIndex + 1;
const citeNumsLabel = formatCitationNumsLabel(item.citeNums);
const citeNumsPlain = (item.citeNums || []).join('、');
const batchTotal = item.batchTotal || 1;
const batchIndex = item.batchIndex || 1;
let batchSuffix = '';
let title = '正文段落 ' + paragraphNo + ' · ' + citeNumsLabel;
if (l.groupLabel) {
title = l.groupLabel
if (batchTotal > 1 && l.batchProgress) {
batchSuffix = ' · ' + l.batchProgress.replace('{current}', batchIndex).replace('{total}', batchTotal);
}
let title = (item.sourceType === 'table' ? '表格 ' : '正文段落 ') + paragraphNo + ' · ' + citeNumsLabel + batchSuffix;
const navLabel = item.sourceType === 'table' ? l.tableGroupLabel : l.groupLabel;
if (navLabel) {
title = navLabel
.replace('{group}', groupNo)
.replace('{paragraph}', paragraphNo)
.replace('{refs}', citeNumsLabel);
.replace('{refs}', citeNumsLabel) + batchSuffix;
}
return {
@@ -170,39 +339,51 @@ export function getCitationReviewGroupMeta(item, index, labels) {
}
/**
* 构建核查队列:每个含引文的正文段落一组,该段全部参考文献一并询问;
* 同一条文献出现在不同段落时,会在各段落中分别询问。
* 构建核查队列:含引文的正文段落与表格均纳入;每处按最多 4 条参考文献拆批。
*/
export function buildCitationReviewQueue(wordList, references) {
export function buildCitationReviewQueue(wordList, references, batchSize) {
const items = [];
const refList = references || [];
const size = batchSize || CITATION_REVIEW_BATCH_SIZE;
(wordList || []).forEach(function (item, paragraphIndex) {
if (!isTextParagraphItem(item)) {
(wordList || []).forEach(function (item, contentIndex) {
if (!isCitableReviewItem(item)) {
return;
}
const html = item.content || item.text || '';
const citeNums = parseCitationNumbersFromHtml(html);
const sourceHtml = getReviewItemSourceHtml(item);
const citeNums = parseCitationNumbersFromHtml(sourceHtml);
if (!citeNums.length) {
return;
}
const paragraphText = htmlToPlainText(html);
const citations = citeNums.map(function (citeNum) {
return buildCitationEntry(citeNum, refList);
});
const sourceType = isTableItem(item) ? 'table' : 'paragraph';
const sourcePlainText = getReviewItemPlainText(item);
const previewHtml = buildReviewContentPreviewHtml(item);
const citeChunks = chunkCitationNumbers(citeNums, size);
const batchTotal = citeChunks.length;
items.push({
paragraphIndex: paragraphIndex,
amId: item.am_id,
paragraphHtml: html,
paragraphText: paragraphText,
citeNums: citeNums.slice(),
citations: citations,
hasMissingReference: citations.some(function (c) {
return c.referenceMissing;
})
citeChunks.forEach(function (chunkNums, batchIndex) {
const citations = chunkNums.map(function (citeNum) {
return buildCitationEntry(citeNum, refList);
});
items.push({
contentIndex: contentIndex,
paragraphIndex: contentIndex,
sourceType: sourceType,
amId: item.am_id,
paragraphHtml: sourceHtml,
paragraphText: sourcePlainText,
previewHtml: previewHtml,
citeNums: chunkNums.slice(),
citations: citations,
batchIndex: batchIndex + 1,
batchTotal: batchTotal,
hasMissingReference: citations.some(function (c) {
return c.referenceMissing;
})
});
});
});
@@ -216,6 +397,7 @@ export function buildWenaiRelevancePrompt(item) {
const paragraphText = item.paragraphText || '';
const citations = item.citations || [];
const isTable = item.sourceType === 'table';
const citeLabels = citations.map(function (c) {
return '[' + c.citeNum + ']';
});
@@ -227,12 +409,18 @@ export function buildWenaiRelevancePrompt(item) {
});
const labelList = citeLabels.join('、') || '';
const intro = isTable
? '请审读以下表格及其引用文献,结合表格内容、标题与注释,判断各文献与表格的相关程度。'
: '请审读以下正文段落及其引用文献,结合该段落的论述内容,判断各文献与正文的相关程度。';
const contentLabel = isTable ? '【表格】' : '【正文段落】';
const contentSection = isTable
? ''
: contentLabel + '\n' + paragraphText + '\n\n';
return (
'请审读以下正文段落及其引用文献,结合该段落的论述内容,判断各文献与正文的相关程度。\n\n' +
'【正文段落】\n' +
paragraphText +
intro +
'\n\n' +
contentSection +
'【参考文献】\n' +
refBlock.trim() +
'\n\n' +
@@ -241,8 +429,8 @@ export function buildWenaiRelevancePrompt(item) {
'请按文献序号 ' +
labelList +
' 依次说明,保留原文序号且顺序连续,不要重新编号或跳号。\n' +
'对每条文献分别给出关联判断:直接相关 / 强相关 / 弱相关 / 不相关,并用一句话简要说明理由。\n' +
'将全部内容合并写成一段连贯的批注文字,语气客观、专业、克制,可直接作为稿件文件批注使用。'
'对每条文献一条条列出分别给出关联判断:直接相关 / 强相关 / 弱相关 / 不相关,并用一句话简要说明理由。\n\n' +
'再结合上述给出的弱相关和不相关的文献写成一段简短的不相关理由批注文字,以编辑的语气建议,可直接作为稿件文件批注使用。'
);
}
@@ -270,6 +458,7 @@ export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
const copyAllText = l.copyAll || '复制全部';
const copySuccessText = l.copySuccess || '已复制';
const copyFailText = l.copyFail || '复制失败';
const previewTitle = l.previewTitle || '内容预览';
let navHtml = '';
let bodyHtml = '';
@@ -280,6 +469,13 @@ export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
const meta = getCitationReviewGroupMeta(item, i, labels);
const prompt = buildWenaiRelevancePrompt(item);
const promptId = 'cite-prompt-' + meta.groupNo;
const previewBlock = item.previewHtml
? '<div class="group-preview"><div class="group-preview-title">' +
escapeHtml(previewTitle) +
'</div>' +
item.previewHtml +
'</div>'
: '';
navHtml +=
'<a class="nav-link" href="#' +
@@ -302,6 +498,7 @@ export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
escapeHtml(copyGroupText) +
'</button>' +
'</div>' +
previewBlock +
'<pre class="group-prompt" id="' +
promptId +
'">' +
@@ -320,7 +517,7 @@ export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
'</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:960px;margin:0 auto;padding:20px}' +
'.page{max-width:1400px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px}' +
'.summary{color:#606266;font-size:14px;margin-bottom:12px}' +
@@ -337,6 +534,13 @@ export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
'.group-head h3{margin:0;font-size:15px;color:#303133}' +
'.copy-btn{border:1px solid #409eff;background:#ecf5ff;color:#409eff;border-radius:4px;padding:4px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
'.copy-btn:hover{background:#409eff;color:#fff}' +
'.group-preview{margin-bottom:12px;padding:12px;background:#fcfcfd;border:1px solid #ebeef5;border-radius:4px;overflow-x:auto}' +
'.group-preview-title{font-size:12px;color:#909399;margin-bottom:8px;font-weight:600}' +
'.cite-preview-paragraph{font-size:14px;line-height:1.6;color:#303133}' +
'.cite-preview-table-title,.cite-preview-table-note{text-align:center;margin:8px 0;font-size:14px;color:#303133}' +
'.cite-preview-table table{width:100%;border-collapse:collapse;font-size:13px;background:#fff}' +
'.cite-preview-table td{border:1px solid #dcdfe6;padding:6px 8px;text-align:center;vertical-align:middle;color:#303133}' +
'.cite-preview-table tr.oddColor td{background:#f5f7fa}' +
'.group-prompt{margin:0;padding:12px;background:#fafafa;border:1px solid #ebeef5;border-radius:4px;white-space:pre-wrap;word-break:break-word;font-size:13px;font-family:Consolas,"Courier New",monospace}' +
'#all-prompt{display:none}' +
'</style></head><body><div class="page">' +
@@ -401,6 +605,9 @@ export function buildCitationReviewHtmlLabels(translate) {
copyAll: t('commonTable.citeRelevanceCopyAllWenai'),
copySuccess: t('commonTable.citeRelevanceCopySuccess'),
copyFail: t('commonTable.citeRelevanceCopyFail'),
groupLabel: t('commonTable.citeRelevanceGroupNavLabel')
groupLabel: t('commonTable.citeRelevanceGroupNavLabel'),
tableGroupLabel: t('commonTable.citeRelevanceTableNavLabel'),
batchProgress: t('commonTable.citeRelevanceBatchProgress'),
previewTitle: t('commonTable.citeRelevancePreviewTitle')
};
}

View File

@@ -147,7 +147,7 @@ export function buildReferencesEditableHtml(references, labels) {
'<title>' + escapeHtml(pageTitle) + '</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:Charis SIL,Georgia,"Times New Roman",serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:920px;margin:0 auto;padding:20px}' +
'.page{max-width:1400px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px;text-align:center;font-style:italic;font-weight:bold;color:#008080}' +
'.intro{color:#606266;font-size:14px;margin-bottom:12px;white-space:pre-wrap}' +