提交 7f1603ac authored 作者: 张伊明's avatar 张伊明

feat 优化智能翻译

上级 3c0cd0ea
流水线 #563 已失败 于阶段
in 34 秒
...@@ -11,10 +11,8 @@ ...@@ -11,10 +11,8 @@
<div v-if="isCompareLoading" class="page-header-status"> <div v-if="isCompareLoading" class="page-header-status">
<span class="status-loading-dot" /> <span class="status-loading-dot" />
<span class="page-header-status-title">文档翻译中</span> <span class="page-header-status-title">文档解析中</span>
<span class="page-header-status-text"> <span class="page-header-status-text">已完成...{{ translationProgress }}%,已用时{{ elapsedSeconds }}</span>
已完成...{{ translationProgress }}%,已用时{{ elapsedSeconds }}
</span>
</div> </div>
<div v-else class="page-header-action-group" @click.stop> <div v-else class="page-header-action-group" @click.stop>
...@@ -39,74 +37,59 @@ ...@@ -39,74 +37,59 @@
<template v-else> <template v-else>
<div class="compare-content"> <div class="compare-content">
<div class="article-nav-tree"> <aside class="article-nav-tree">
<div class="tree-title">导航</div> <div class="tree-title">导航</div>
<el-tree <el-tree
ref="articleTreeRef"
:data="articleTree" :data="articleTree"
node-key="id" node-key="id"
:props="treeProps" :props="treeProps"
:current-node-key="currentNodeKey" :current-node-key="currentNodeKey"
highlight-current highlight-current
default-expand-all
@node-click="onClickTreeNode" @node-click="onClickTreeNode"
/> />
</div> </aside>
<div class="paragraph-compare-wrapper"> <div ref="compareScrollRef" class="sync-compare-scroll" @scroll="onSyncScroll">
<div class="paragraph-compare-panel"> <section
<div class="panel-title">段落对照</div> v-for="page in pageBlocks"
<div class="paragraph-list"> :key="page.pageId"
<div :id="page.pageId"
v-for="section in sectionBlocks" class="page-block"
:key="section.id" :class="{ 'page-block--active': currentPageId === page.pageId }"
class="section-block" >
:data-section-id="section.id" <div class="page-block__header">{{ page.pageTitle }}</div>
<div class="page-block__body">
<article
v-for="item in page.items"
:key="`${page.pageId}-${item.id}`"
class="page-item"
:data-section-id="item.id"
> >
<div class="section-title">{{ section.title }}</div> <div class="page-item__text page-item__text--en" :class="[getBlockVariantClass(item.type), shouldCenterBlock(item) ? 'page-item__text--center' : '']">
<div <div v-if="item.type === 'table' && item.tableHtml" class="page-item__html" v-html="item.tableHtml"></div>
v-for="paragraph in section.paragraphs" <img
:key="paragraph.id" v-else-if="item.type === 'image' && item.previewImgPath"
class="paragraph-compare-row" class="page-item__image"
:class="{ 'paragraph-compare-row--active': isSelectedParagraph(paragraph.id) }" :src="item.previewImgPath"
@click.stop="toggleParagraphSelection(paragraph)" alt=""
> />
<div class="paragraph-cell source-cell"> <div v-else class="page-item__content" v-html="renderBlockHtml(item.text, item.styleSpans)"></div>
<div class="cell-label">原文</div>
<div class="cell-content">{{ paragraph.sourceText }}</div>
</div>
<div class="paragraph-cell translated-cell">
<div class="cell-label">译文</div>
<div class="cell-content">{{ paragraph.translatedText }}</div>
</div>
</div> </div>
</div> <div class="page-item__text page-item__text--zh" :class="[getBlockVariantClass(item.type), shouldCenterBlock(item) ? 'page-item__text--center' : '']">
<div v-if="item.type === 'table' && item.translatedTableHtml" class="page-item__html" v-html="item.translatedTableHtml"></div>
<div v-if="sectionBlocks.length === 0" class="empty-tip">当前章节暂无段落占位数据</div> <img
</div> v-else-if="item.type === 'image' && item.previewImgPath"
</div> class="page-item__image"
:src="item.previewImgPath"
<transition name="fade"> alt=""
<aside v-if="showParagraphActionPanel" class="paragraph-action-panel" @click.stop> />
<div class="paragraph-action-panel-header"> <div v-else class="page-item__content" v-html="renderBlockHtml(item.translatedText || item.text || '(待接入译文)', item.styleSpans)"></div>
<button type="button" class="paragraph-action-btn" @click="onParagraphAction('insert')">插入</button>
<button type="button" class="paragraph-action-btn" @click="onParagraphAction('delete')">删除</button>
</div>
<div class="paragraph-style-section">
<div class="paragraph-style-title">样式</div>
<div class="paragraph-style-list">
<button
v-for="style in paragraphStyles"
:key="style.key"
type="button"
class="paragraph-style-btn"
:class="{ 'paragraph-style-btn--active': isStyleSelected(style.key) }"
@click="onStyleSelect(style.key)"
>
{{ style.label }}
</button>
</div> </div>
</div> </article>
</aside> </div>
</transition> </section>
</div> </div>
</div> </div>
</template> </template>
...@@ -121,7 +104,7 @@ import { ElMessage } from 'element-plus' ...@@ -121,7 +104,7 @@ import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import IntelligenceLeftTabBar from '@/components/intelligenceLeftTabBar/index.vue' import IntelligenceLeftTabBar from '@/components/intelligenceLeftTabBar/index.vue'
import comparePlaceholderUrl from './icons/container-1885-placeholder.png' import comparePlaceholderUrl from './icons/container-1885-placeholder.png'
import { fetchDocumentComparePageData } from './services/documentCompareService' import mergedAns from './merged_ans.json'
const route = useRoute() const route = useRoute()
...@@ -134,117 +117,243 @@ const headerActions = [ ...@@ -134,117 +117,243 @@ const headerActions = [
{ key: 'save', label: '保存', variant: 'primary' } { key: 'save', label: '保存', variant: 'primary' }
] ]
const paragraphStyles = [ const normalizedPages = ref([])
{ key: 'body', label: '正文' }, const normalizedBlocks = ref([])
{ key: 'chart-title', label: '图表标题' }, const articleTree = ref([])
{ key: 'figure-caption', label: '图标注释' }, const articleTreeRef = ref(null)
{ key: 'cover-title', label: '封面标题' }, const compareScrollRef = ref(null)
{ key: 'cover-subtitle', label: '封面副标题' }, const currentNodeKey = ref('')
{ key: 'catalog', label: '目录' }, const currentPageId = ref('')
{ key: 'h1', label: '一级标题' },
{ key: 'h2', label: '二级标题' },
{ key: 'h3', label: '三级标题' },
{ key: 'h4', label: '四级标题' },
{ key: 'author', label: '作者' },
{ key: 'time', label: '时间' },
{ key: 'publisher', label: '发布机构' }
]
const isCompareLoading = ref(true) const isCompareLoading = ref(true)
const elapsedSeconds = ref(0) const elapsedSeconds = ref(0)
const timerId = ref(null)
const translationProgress = ref(0) const translationProgress = ref(0)
const articleTree = ref([]) const timerId = ref(null)
const paragraphRows = ref([])
const currentNodeKey = ref('')
const selectedParagraphIds = ref([])
const paragraphStyleMap = ref({})
const treeProps = { label: 'title', children: 'children' } const treeProps = { label: 'title', children: 'children' }
const translatedTextLookup = new Map([
['page0_node0', 'I. 基本信息'],
['page0_node1', '美国能源部(DOE)'],
['page0_node2', '科学办公室(SC)、关键矿产与能源创新办公室(CMEI)、环境管理办公室(EM)、电力办公室(OE)、核能办公室(NE)以及碳氢化合物与地热能源办公室(HGEO)'],
['page0_node3', '执行摘要'],
['page0_node4', 'DOE 相关办公室现公开征集跨学科团队申请,围绕 Genesis Mission 国家科学与技术挑战,推动利用新型人工智能模型和框架加速科学发现与研发流程。'],
['page0_node5', 'DOE 正在征集 FY26 第一阶段小团队与第二阶段大团队申请,涵盖先进制造、生物技术、关键材料、核裂变、核聚变、量子信息科学、半导体与微电子、发现科学和能源等领域。'],
['page0_node6', '本次 RFA 将继续开放,允许 FY26 第一阶段获奖者申请更大规模的第二阶段资助。'],
['page0_node7', '在对应截止日期之后,仍可提交第一阶段和第二阶段申请,但 DOE 保留拒绝此类申请且不予审查的权利。'],
['page0_node8', '1. Genesis Mission 联盟成员'],
['page0_node9', 'Genesis Mission 联盟成员可提交申请,资金将通过 DOE 的其他交易授权(OTA)提供。'],
['page1_node0', '就 OTA 而言,本 RFA 的部分行政规定可能不适用于申请人或其分包商。'],
['page1_node1', '请注意,是否属于 Genesis Mission 联盟成员并非申请本 RFA 或参与入选项目的必要条件。'],
['page1_node2', '所有申请人注意事项:'],
['page1_node3', '获得本 RFA 下的奖励并不赋予,也不要求申请人加入 Genesis Mission 联盟。'],
['page1_node4', '申请人可在申请中提议非本土实体作为分包方。'],
['page1_node5', '资助详情'],
['page1_node6', '预计总可用资金、奖项数量、单项资助金额与项目周期见表。'],
['page1_node7', '关键信息'],
['page1_node8', 'RFA 名称、编号、公告类型、援助清单、法定授权与适用法规。'],
['page2_node0', '表格信息摘要'],
['page2_node1', '关键日期'],
['page2_node2', '关键日期见本 RFA 封面页。'],
['page2_node3', '提交组队要求'],
['page2_node4', '本 RFA 要求提交内容为多机构团队。'],
['page2_node5', '机构联系信息'],
['page2_node6', 'Grants.gov 客服与项目联系人信息。'],
['page2_node7', '说明性网络研讨会 / 办公时间'],
['page2_node8', 'DOE 将于 2026 年 3 月 26 日星期四下午 3 点(东部时间)举办说明性网络研讨会。'],
['page2_node9', '建议'],
['page2_node10', '建议尽早完成各系统注册并尽早提交申请。'],
['page2_node11', '3']
])
const displayFileName = computed(() => { const displayFileName = computed(() => {
const fileName = route.query.fileName const fileName = route.query.fileName
return typeof fileName === 'string' && fileName ? fileName : '未命名文档(占位)' return typeof fileName === 'string' && fileName ? fileName : '未命名文档(占位)'
}) })
const sectionBlocks = computed(() => { const compareLoadingText = computed(() => `文档解析中,预计用时${ESTIMATED_SECONDS}秒,已用时${elapsedSeconds.value}秒...`)
const tree = Array.isArray(articleTree.value) ? articleTree.value : []
return tree
.filter((item) => item.id)
.map((item) => ({
id: item.id,
title: item.title,
paragraphs: paragraphRows.value.filter((row) => row.sectionId === item.id)
}))
.filter((item) => item.paragraphs.length > 0)
})
const showParagraphActionPanel = computed(() => selectedParagraphIds.value.length > 0) const pageBlocks = computed(() => {
const compareLoadingText = computed( return normalizedPages.value.map((page) => ({
() => `文档解析中,预计用时${ESTIMATED_SECONDS}秒,已用时${elapsedSeconds.value}秒...` pageId: `page-${page.pageIdx}`,
) pageTitle: `第 ${page.pageIdx + 1} 页`,
items: normalizedBlocks.value.filter((block) => block.pageIdx === page.pageIdx)
}))
})
function getFirstNode(nodes) { function shouldCenterBlock(block) {
if (!Array.isArray(nodes) || nodes.length === 0) return null if (!block) return false
const [firstNode] = nodes if (block.type !== 'heading') return false
if (firstNode.children?.length) return getFirstNode(firstNode.children) const text = String(block.text || '').trim()
return firstNode if (!text) return false
const strongHeading = block.headingLevel != null
const chapterLike = /^(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)[\.|、\s]/i.test(text)
const alphaHeading = /^[A-Z][a-zA-Z\s/&-]{3,}$/.test(text) && !/^[A-Z]{1,3}\b/.test(text)
const chineseHeading = /^[一二三四五六七八九十]+[、.]/.test(text)
const notNumericList = !/^\d+[\.|、]/.test(text)
return strongHeading || (notNumericList && (chapterLike || alphaHeading || chineseHeading))
} }
async function onClickTreeNode(nodeData) { function mapSemanticTypeToNodeType(semanticType, contentPayload = {}) {
currentNodeKey.value = nodeData.id if (semanticType === 'heading' || contentPayload.heading_level != null) return 'heading'
const targetSectionId = nodeData.sectionId || nodeData.id if (semanticType === 'paragraph') return 'paragraph'
await nextTick() if (semanticType === 'table' || contentPayload.type === 'table') return 'table'
const targetEl = document.querySelector(`[data-section-id='${targetSectionId}']`) if (semanticType === 'page_number') return 'page_number'
targetEl?.scrollIntoView({ behavior: 'smooth', block: 'start' }) if (semanticType === 'image' || contentPayload.preview_img_path) return 'image'
return 'unknown'
} }
function toggleParagraphSelection(paragraph) { function getBlockTypeLabel(type) {
const idx = selectedParagraphIds.value.indexOf(paragraph.id) const labelMap = { heading: '标题', paragraph: '正文', table: '表格', image: '图片', page_number: '页码', unknown: '未知' }
if (idx >= 0) { return labelMap[type] || '未知'
selectedParagraphIds.value = selectedParagraphIds.value.filter((id) => id !== paragraph.id)
return
}
selectedParagraphIds.value = [...selectedParagraphIds.value, paragraph.id]
} }
function isSelectedParagraph(paragraphId) { function getBlockVariantClass(type) {
return selectedParagraphIds.value.includes(paragraphId) return `document-block--${type || 'unknown'}`
} }
function isStyleSelected(styleKey) { function escapeHtml(text = '') {
return selectedParagraphIds.value.some((id) => paragraphStyleMap.value[id] === styleKey) return String(text)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
} }
function onStyleSelect(styleKey) { function renderBlockHtml(text = '', styleSpans = []) {
selectedParagraphIds.value.forEach((id) => { const safeText = escapeHtml(text)
paragraphStyleMap.value[id] = styleKey if (!Array.isArray(styleSpans) || styleSpans.length === 0) return safeText.replaceAll('\n', '<br />')
})
ElMessage.success(`已选择样式:${paragraphStyles.find((item) => item.key === styleKey)?.label || styleKey}`) const sorted = [...styleSpans].sort((a, b) => a.start - b.start)
console.log('样式选择占位', { let html = ''
selectedParagraphIds: [...selectedParagraphIds.value], let cursor = 0
styleKey
sorted.forEach((span) => {
const start = Math.max(0, span.start || 0)
const end = Math.max(start, span.end || start)
if (start > cursor) html += escapeHtml(text.slice(cursor, start))
const spanText = escapeHtml(text.slice(start, end))
const style = span?.style || {}
const inlineStyle = [
style.bold ? 'font-weight:700' : '',
style.italic ? 'font-style:italic' : '',
style.underline ? 'text-decoration:underline' : '',
style.color_hex ? `color:${style.color_hex}` : ''
].filter(Boolean).join(';')
html += `<span style="${inlineStyle}">${spanText}</span>`
cursor = end
}) })
if (cursor < text.length) html += escapeHtml(text.slice(cursor))
return html.replaceAll('\n', '<br />')
} }
function onParagraphAction(actionKey) { function buildNormalizedBlocks(rawData) {
ElMessage.info(`段落操作占位:${actionKey}`) const pages = Array.isArray(rawData?.data?.pages) ? rawData.data.pages : []
console.log('段落操作占位', { const nodes = Array.isArray(rawData?.data?.merged_nodes) ? rawData.data.merged_nodes : []
actionKey,
selectedParagraphIds: [...selectedParagraphIds.value] normalizedPages.value = pages.map((page) => ({
pageIdx: page.page_idx,
width: page.width,
height: page.height,
rotation: page.rotation,
nodeIds: Array.isArray(page.node_ids) ? page.node_ids : []
}))
normalizedBlocks.value = nodes.map((node, index) => {
const contentPayload = node?.content_payload || {}
const type = mapSemanticTypeToNodeType(node.semantic_type, contentPayload)
return {
id: node.node_id || `node_${index}`,
nodeId: node.node_id || '',
pageIdx: node.page_idx ?? null,
type,
semanticType: node.semantic_type || 'unknown',
readingOrder: node?.layout?.reading_order ?? index,
title: contentPayload.text || node.node_id || `未命名块${index + 1}`,
text: contentPayload.text || '',
translatedText: translatedTextLookup.get(node.node_id) || '',
html: contentPayload.html || '',
headingLevel: contentPayload.heading_level ?? null,
tableHtml: contentPayload.table_body_html || '',
translatedTableHtml: contentPayload.table_body_html || '',
previewImgPath: contentPayload.preview_img_path || '',
layout: {
bbox: node?.layout?.bbox || [],
bboxNorm: node?.layout?.bbox_norm || [],
angle: node?.layout?.angle ?? 0,
link: node?.layout?.link ?? null,
zIndex: node?.layout?.z_index ?? null
},
styleSpans: Array.isArray(node.style_spans) ? node.style_spans : []
}
}) })
} }
function buildNavigationTree(rawData) {
const nodes = Array.isArray(rawData?.data?.merged_nodes) ? rawData.data.merged_nodes : []
articleTree.value = nodes
.map((node, index) => {
const contentPayload = node?.content_payload || {}
const nodeType = mapSemanticTypeToNodeType(node.semantic_type, contentPayload)
const text = contentPayload.text || node.node_id || '未命名块'
if (nodeType !== 'heading') return null
if (!shouldCenterBlock({ ...node, text })) return null
return {
id: node.node_id || `heading_${index}`,
title: text,
pageIdx: node.page_idx ?? 0,
nodeType,
sectionId: node.node_id || `heading_${index}`,
children: []
}
})
.filter(Boolean)
}
function findBlockElementById(blockId) {
return document.querySelector(`[data-section-id='${blockId}']`)
}
async function onClickTreeNode(nodeData) {
currentNodeKey.value = nodeData.id
const targetPage = pageBlocks.value.find((page) => page.items.some((item) => item.id === nodeData.id))
currentPageId.value = targetPage?.pageId || currentPageId.value
await nextTick()
findBlockElementById(nodeData.id)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
function onSyncScroll() {
const scroller = compareScrollRef.value
if (!scroller) return
const pageEls = Array.from(scroller.querySelectorAll('.page-block'))
const viewportTop = scroller.getBoundingClientRect().top
let activePage = pageBlocks.value[0]?.pageId || ''
for (const pageEl of pageEls) {
const rect = pageEl.getBoundingClientRect()
if (rect.top - viewportTop <= 96) activePage = pageEl.id
}
currentPageId.value = activePage
const activePageBlocks = pageBlocks.value.find((page) => page.pageId === activePage)?.items || []
const activeHeading = articleTree.value.find((item) => activePageBlocks.some((block) => block.id === item.id))
if (activeHeading) currentNodeKey.value = activeHeading.id
}
function onPageClick() {
currentNodeKey.value = currentPageId.value || currentNodeKey.value
}
function onHeaderAction(actionKey) { function onHeaderAction(actionKey) {
ElMessage.info(`顶部功能按钮占位:${actionKey}`) ElMessage.info(`顶部功能按钮占位:${actionKey}`)
console.log('顶部功能按钮占位', { actionKey })
} }
function onPageClick() { function getFirstNavigableNode(nodes) {
selectedParagraphIds.value = [] if (!Array.isArray(nodes)) return null
return nodes.find((node) => node?.id) || null
} }
onMounted(async () => { onMounted(async () => {
const fileName = typeof route.query.fileName === 'string' ? route.query.fileName : ''
const requestStartTime = Date.now() const requestStartTime = Date.now()
const updateElapsedSeconds = () => { const updateElapsedSeconds = () => {
elapsedSeconds.value = Math.floor((Date.now() - requestStartTime) / 1000) elapsedSeconds.value = Math.floor((Date.now() - requestStartTime) / 1000)
...@@ -254,12 +363,14 @@ onMounted(async () => { ...@@ -254,12 +363,14 @@ onMounted(async () => {
timerId.value = window.setInterval(updateElapsedSeconds, 1000) timerId.value = window.setInterval(updateElapsedSeconds, 1000)
try { try {
const data = await fetchDocumentComparePageData({ fileName }) buildNormalizedBlocks(mergedAns)
articleTree.value = data.articleTree buildNavigationTree(mergedAns)
paragraphRows.value = data.paragraphRows translationProgress.value = Number(mergedAns?.data?.pages?.length ? 100 : 0)
translationProgress.value = Number(data.translationProgress) || 0 const firstNode = getFirstNavigableNode(articleTree.value)
const firstNode = getFirstNode(data.articleTree) if (firstNode) {
if (firstNode) currentNodeKey.value = firstNode.id currentNodeKey.value = firstNode.id
currentPageId.value = `page-${firstNode.pageIdx ?? 0}`
}
} finally { } finally {
isCompareLoading.value = false isCompareLoading.value = false
} }
...@@ -276,7 +387,9 @@ onUnmounted(() => { ...@@ -276,7 +387,9 @@ onUnmounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.document-compare-page { .document-compare-page {
display: flex; display: flex;
height: calc(100vh - 64px);
min-height: calc(100vh - 64px); min-height: calc(100vh - 64px);
overflow: hidden;
background: #fff; background: #fff;
} }
...@@ -284,8 +397,8 @@ onUnmounted(() => { ...@@ -284,8 +397,8 @@ onUnmounted(() => {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: calc(100vh - 64px); min-height: 0;
overflow: auto; overflow: hidden;
position: relative; position: relative;
} }
...@@ -307,12 +420,12 @@ onUnmounted(() => { ...@@ -307,12 +420,12 @@ onUnmounted(() => {
.compare-body { .compare-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 8px; padding: 8px;
background: var(--color-primary-2); background: var(--color-primary-2);
box-sizing: border-box; box-sizing: border-box;
position: relative;
} }
.page-header-left { display: flex; align-items: center; gap: 8px; min-width: 0; } .page-header-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
...@@ -331,30 +444,28 @@ onUnmounted(() => { ...@@ -331,30 +444,28 @@ onUnmounted(() => {
.compare-loading-placeholder { flex: 1; min-height: 560px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 16px; box-sizing: border-box; background: var(--bg-white-100); border-radius: 8px; } .compare-loading-placeholder { flex: 1; min-height: 560px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 16px; box-sizing: border-box; background: var(--bg-white-100); border-radius: 8px; }
.compare-loading-placeholder-img { max-width: 100%; max-height: min(720px, calc(100vh - 160px)); width: auto; height: auto; object-fit: contain; display: block; } .compare-loading-placeholder-img { max-width: 100%; max-height: min(720px, calc(100vh - 160px)); width: auto; height: auto; object-fit: contain; display: block; }
.compare-loading-placeholder-text { font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 16px; line-height: 30px; color: #344054; } .compare-loading-placeholder-text { font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 16px; line-height: 30px; color: #344054; }
.compare-content { flex: 1; min-height: 560px; display: flex; gap: 12px; position: relative; } .compare-content { flex: 1; min-height: 0; display: flex; gap: 12px; overflow: hidden; }
.article-nav-tree, .paragraph-compare-panel { border: 1px solid #e8ecf2; border-radius: 8px; background: #fff; } .article-nav-tree { width: 280px; flex-shrink: 0; padding: 16px 19px; display: flex; flex-direction: column; min-height: 0; overflow: hidden; border: 1px solid #e8ecf2; border-radius: 8px; background: #fff; }
.article-nav-tree { width: 280px; flex-shrink: 0; padding: 16px 19px; display: flex; flex-direction: column; }
.tree-title { font-family: 'Source Han Sans CN', sans-serif; font-weight: 700; font-size: 16px; line-height: 24px; letter-spacing: 1px; color: var(--text-primary-80-color); margin-bottom: 17px; } .tree-title { font-family: 'Source Han Sans CN', sans-serif; font-weight: 700; font-size: 16px; line-height: 24px; letter-spacing: 1px; color: var(--text-primary-80-color); margin-bottom: 17px; }
:deep(.article-nav-tree .el-tree-node__label) { font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 14px; line-height: 22px; color: var(--text-primary-80-color); } :deep(.article-nav-tree .el-tree-node__label) { font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 14px; line-height: 22px; color: var(--text-primary-80-color); }
.paragraph-compare-wrapper { flex: 1; display: flex; align-items: stretch; gap: 12px; min-width: 0; } .sync-compare-scroll { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; overflow-x: hidden; border: 1px solid #e8ecf2; border-radius: 8px; background: #fff; padding: 16px; box-sizing: border-box; }
.paragraph-compare-panel { flex: 1; min-width: 0; padding: 12px; display: flex; flex-direction: column; } .page-block { display: flex; flex-direction: column; gap: 12px; padding: 12px 0 20px; border-bottom: 1px solid #d9e2ec; }
.panel-title { color: #344054; font-size: 14px; font-weight: 600; margin-bottom: 12px; } .page-block--active { background: #f8fafc; }
.paragraph-list { display: flex; flex-direction: column; gap: 12px; overflow: auto; } .page-block__header { font-family: 'Source Han Sans CN', sans-serif; font-size: 15px; font-weight: 700; line-height: 24px; color: #101828; }
.section-block { display: flex; flex-direction: column; gap: 12px; } .page-block__body { display: flex; flex-direction: column; gap: 12px; }
.section-title { font-family: 'Source Han Sans CN', sans-serif; font-size: 14px; font-weight: 600; line-height: 22px; color: var(--text-primary-80-color, #344054); } .page-item { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: stretch; }
.paragraph-compare-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; cursor: pointer; } .page-item__text { min-height: 72px; padding: 0; background: transparent; border: none; }
.paragraph-compare-row--active .paragraph-cell { outline: 2px solid var(--color-primary-35); } .page-item__text--en { border-right: 1px solid #eef2f7; padding-right: 12px; }
.paragraph-cell { border: 1px solid #e4e7ec; border-radius: 6px; padding: 10px; min-height: 120px; } .page-item__text--zh { padding-left: 12px; }
.cell-label { font-size: 12px; color: #667085; margin-bottom: 8px; } .page-item__text--center .page-item__content { text-align: center; }
.cell-content { font-size: 14px; line-height: 22px; color: #344054; white-space: pre-wrap; } .page-item__content { font-size: 14px; line-height: 1.8; color: #344054; white-space: pre-wrap; word-break: break-word; }
.empty-tip { color: #98a2b3; font-size: 14px; } .page-item__html :deep(table) { width: 100%; border-collapse: collapse; font-size: 13px; }
.paragraph-action-panel { width: 160px; flex: 0 0 160px; align-self: stretch; background: #fff; border: 1px solid var(--bg-black-10); border-radius: 10px; box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12); padding: 12px; display: flex; flex-direction: column; z-index: 20; } .page-item__html :deep(td), .page-item__html :deep(th) { border: 1px solid #d0d5dd; padding: 8px; vertical-align: top; }
.paragraph-action-panel-header { display: flex; gap: 8px; margin-bottom: 12px; } .page-item__image { max-width: 100%; display: block; border-radius: 6px; }
.paragraph-action-btn { flex: 1; border: 1px solid var(--bg-black-10); background: #fff; border-radius: 6px; padding: 6px 12px; font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; cursor: pointer; } .page-item--heading .page-item__content { font-size: 18px; font-weight: 700; color: #101828; }
.paragraph-style-title { font-family: 'Source Han Sans CN', sans-serif; font-size: 14px; line-height: 22px; color: #344054; margin-bottom: 8px; } .page-item--paragraph .page-item__content { font-size: 14px; }
.paragraph-style-list { display: flex; flex-wrap: wrap; gap: 8px; } .page-item--table .page-item__content { font-size: 13px; }
.paragraph-style-btn { flex: 0 1 auto; width: auto; padding: 6px 19px; border: 1px solid var(--bg-black-10); border-radius: 6px; background: #fff; font-family: 'Source Han Sans CN', sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; cursor: pointer; color: var(--text-primary-80-color, #344054); } .page-item--image .page-item__content { font-size: 13px; color: #667085; }
.paragraph-style-btn--active { border-color: var(--color-primary-100); background: var(--color-primary-2); color: var(--color-primary-100); } .page-item--page_number .page-item__content { font-size: 12px; color: #98a2b3; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; } .page-item--unknown .page-item__content { color: #667085; font-style: italic; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style> </style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论