feat(data-process): 完善 Word 与 Excel 原文件预览
This commit is contained in:
622
frontend/src/views/data-process/create/OfficeSourceViewer.vue
Normal file
622
frontend/src/views/data-process/create/OfficeSourceViewer.vue
Normal file
@@ -0,0 +1,622 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import {
|
||||
getDataProcessOfficePreview,
|
||||
getDataProcessSourceRawUrl,
|
||||
type DataProcessDocxPreview,
|
||||
type DataProcessDocxTableRow,
|
||||
type DataProcessOfficePreview,
|
||||
type DataProcessXlsxPreview,
|
||||
type DataProcessXlsxPreviewRow,
|
||||
} from '@/api/modules/dataProcess'
|
||||
import type { PreviewItem } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
taskId: string | number | null
|
||||
sourceFileId: string | number | null
|
||||
fileName: string
|
||||
fileFormat?: string
|
||||
selectedItem: PreviewItem | null
|
||||
}>()
|
||||
|
||||
const XLSX_PAGE_SIZE = 100
|
||||
const scrollRef = ref<HTMLElement | null>(null)
|
||||
const preview = shallowRef<DataProcessOfficePreview | null>(null)
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const activeSheetIndex = ref(0)
|
||||
const pageOffset = ref(0)
|
||||
let loadSequence = 0
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
const normalizedFormat = computed(() => (
|
||||
props.fileFormat?.toLowerCase().replace(/^\./, '')
|
||||
|| props.fileName.split('.').pop()?.toLowerCase()
|
||||
|| ''
|
||||
))
|
||||
const isDocx = computed(() => normalizedFormat.value === 'docx')
|
||||
const docxPreview = computed((): DataProcessDocxPreview | null => (
|
||||
preview.value?.format === 'docx' ? preview.value : null
|
||||
))
|
||||
const xlsxPreview = computed((): DataProcessXlsxPreview | null => (
|
||||
preview.value?.format === 'xlsx' ? preview.value : null
|
||||
))
|
||||
const sourceUrl = computed(() => (
|
||||
props.taskId != null && props.sourceFileId != null
|
||||
? getDataProcessSourceRawUrl(props.taskId, props.sourceFileId)
|
||||
: ''
|
||||
))
|
||||
const visibleRowRange = computed(() => {
|
||||
const sheet = xlsxPreview.value?.active_sheet
|
||||
if (!sheet || !sheet.rows.length) return '当前工作表没有可预览记录'
|
||||
const start = sheet.offset + 1
|
||||
const end = sheet.offset + sheet.rows.length
|
||||
return `第 ${start}–${end} 条记录`
|
||||
})
|
||||
|
||||
function overlapsSelection(start: number, end: number) {
|
||||
const item = props.selectedItem
|
||||
if (!item || item.sourceStart == null || item.sourceEnd == null) return false
|
||||
return end > item.sourceStart && start < item.sourceEnd
|
||||
}
|
||||
|
||||
function tableRowHighlighted(row: DataProcessDocxTableRow) {
|
||||
return overlapsSelection(row.source_start, row.source_end)
|
||||
}
|
||||
|
||||
function stableValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(stableValue)
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, item]) => [key, stableValue(item)]),
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function recordKey(value: unknown) {
|
||||
try {
|
||||
return JSON.stringify(stableValue(value))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const selectedRecordKey = computed(() => {
|
||||
const content = props.selectedItem?.originalContent
|
||||
if (!content) return ''
|
||||
try {
|
||||
return recordKey(JSON.parse(content))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
function xlsxRowHighlighted(row: DataProcessXlsxPreviewRow) {
|
||||
return Boolean(selectedRecordKey.value && recordKey(row.record) === selectedRecordKey.value)
|
||||
}
|
||||
|
||||
function displayCell(value: unknown) {
|
||||
if (value == null || value === '') return '—'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
async function locateSelectedItem() {
|
||||
await nextTick()
|
||||
const selected = scrollRef.value?.querySelector<HTMLElement>(
|
||||
'.docx-block.is-highlighted, .docx-table-row.is-highlighted, .xlsx-row.is-highlighted',
|
||||
)
|
||||
selected?.scrollIntoView({
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPreview(options: { reset?: boolean } = {}) {
|
||||
const sequence = ++loadSequence
|
||||
if (options.reset) {
|
||||
activeSheetIndex.value = 0
|
||||
pageOffset.value = 0
|
||||
preview.value = null
|
||||
}
|
||||
errorMessage.value = ''
|
||||
if (props.taskId == null || props.sourceFileId == null) {
|
||||
errorMessage.value = '缺少原文件标识,无法加载预览'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getDataProcessOfficePreview(
|
||||
props.taskId,
|
||||
props.sourceFileId,
|
||||
isDocx.value
|
||||
? {}
|
||||
: {
|
||||
sheet_index: activeSheetIndex.value,
|
||||
offset: pageOffset.value,
|
||||
limit: XLSX_PAGE_SIZE,
|
||||
},
|
||||
)
|
||||
if (sequence !== loadSequence) return
|
||||
preview.value = result
|
||||
if (result.format === 'xlsx') activeSheetIndex.value = result.active_sheet.index
|
||||
await locateSelectedItem()
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Office 原文件预览加载失败'
|
||||
} finally {
|
||||
if (sequence === loadSequence) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function changeSheet(value: string | number) {
|
||||
activeSheetIndex.value = Number(value)
|
||||
pageOffset.value = 0
|
||||
void loadPreview()
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
pageOffset.value = Math.max(0, pageOffset.value - XLSX_PAGE_SIZE)
|
||||
void loadPreview()
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (!xlsxPreview.value?.active_sheet.has_more) return
|
||||
pageOffset.value += XLSX_PAGE_SIZE
|
||||
void loadPreview()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.taskId, props.sourceFileId, normalizedFormat.value],
|
||||
() => void loadPreview({ reset: true }),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.selectedItem?.id,
|
||||
() => void locateSelectedItem(),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="office-source-viewer"
|
||||
:aria-label="`${normalizedFormat.toUpperCase()} 预览:${fileName}`"
|
||||
>
|
||||
<div class="office-toolbar">
|
||||
<template v-if="xlsxPreview">
|
||||
<div class="sheet-selector">
|
||||
<span>工作表</span>
|
||||
<el-select
|
||||
:model-value="activeSheetIndex"
|
||||
size="small"
|
||||
aria-label="选择 Excel 工作表"
|
||||
@update:model-value="changeSheet"
|
||||
>
|
||||
<el-option
|
||||
v-for="sheet in xlsxPreview.sheets"
|
||||
:key="sheet.index"
|
||||
:label="sheet.name"
|
||||
:value="sheet.index"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<span>{{ visibleRowRange }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>Word 网页版式预览</span>
|
||||
<span v-if="docxPreview?.truncated">文档较长,仅展示前 2,000 个内容块</span>
|
||||
</template>
|
||||
<span v-if="loading && preview" class="toolbar-loading" role="status">
|
||||
<i class="fa fa-spinner fa-spin" /> 正在更新预览
|
||||
</span>
|
||||
<a
|
||||
v-if="sourceUrl"
|
||||
class="source-file-link"
|
||||
:href="sourceUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="fa fa-external-link" /> 打开原文件
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !preview" class="office-state" role="status">
|
||||
<i class="fa fa-spinner fa-spin" />
|
||||
<strong>正在加载原文件预览</strong>
|
||||
<span>{{ isDocx ? '正在还原 Word 文档结构' : '正在读取 Excel 工作表' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="office-state is-error" role="alert">
|
||||
<i class="fa fa-exclamation-circle" />
|
||||
<strong>{{ isDocx ? 'Word 预览失败' : 'Excel 预览失败' }}</strong>
|
||||
<span>{{ errorMessage }}</span>
|
||||
<div>
|
||||
<el-button type="primary" size="small" @click="loadPreview()">重试</el-button>
|
||||
<el-button v-if="sourceUrl" tag="a" :href="sourceUrl" target="_blank" size="small">
|
||||
打开原文件
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="docxPreview" ref="scrollRef" class="docx-scroll">
|
||||
<article class="docx-page">
|
||||
<template v-for="(block, index) in docxPreview.blocks" :key="index">
|
||||
<component
|
||||
:is="block.heading_level ? `h${block.heading_level}` : 'p'"
|
||||
v-if="block.type === 'paragraph'"
|
||||
class="docx-block"
|
||||
:class="{
|
||||
'is-highlighted': overlapsSelection(block.source_start, block.source_end),
|
||||
'is-list': block.is_list,
|
||||
}"
|
||||
:style="{ textAlign: block.alignment }"
|
||||
:data-source-start="block.source_start"
|
||||
>
|
||||
{{ block.text }}
|
||||
</component>
|
||||
<div v-else class="docx-table-wrap">
|
||||
<table class="docx-table">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, rowIndex) in block.rows"
|
||||
:key="rowIndex"
|
||||
class="docx-table-row"
|
||||
:class="{ 'is-highlighted': tableRowHighlighted(row) }"
|
||||
:data-source-start="row.source_start"
|
||||
>
|
||||
<td v-for="(cell, cellIndex) in row.cells" :key="cellIndex">{{ cell || ' ' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!docxPreview.blocks.length" class="office-empty">文档中没有可预览的正文</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<template v-else-if="xlsxPreview">
|
||||
<div ref="scrollRef" class="xlsx-scroll">
|
||||
<table v-if="xlsxPreview.active_sheet.columns.length" class="xlsx-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="row-number-cell">#</th>
|
||||
<th
|
||||
v-for="column in xlsxPreview.active_sheet.columns"
|
||||
:key="column"
|
||||
:title="column"
|
||||
>
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in xlsxPreview.active_sheet.rows"
|
||||
:key="row.row_number"
|
||||
class="xlsx-row"
|
||||
:class="{ 'is-highlighted': xlsxRowHighlighted(row) }"
|
||||
>
|
||||
<th class="row-number-cell">{{ row.row_number }}</th>
|
||||
<td
|
||||
v-for="(value, cellIndex) in row.values"
|
||||
:key="cellIndex"
|
||||
:title="displayCell(value)"
|
||||
>
|
||||
{{ displayCell(value) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="office-empty">当前工作表没有可预览记录</div>
|
||||
</div>
|
||||
<div class="xlsx-pagination">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="pageOffset === 0 || loading"
|
||||
aria-label="上一页工作表记录"
|
||||
@click="previousPage"
|
||||
>
|
||||
<i class="fa fa-angle-left" /> 上一页
|
||||
</el-button>
|
||||
<span>{{ visibleRowRange }}</span>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="!xlsxPreview.active_sheet.has_more || loading"
|
||||
aria-label="下一页工作表记录"
|
||||
@click="nextPage"
|
||||
>
|
||||
下一页 <i class="fa fa-angle-right" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.office-source-viewer {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.office-toolbar {
|
||||
display: flex;
|
||||
min-height: 42px;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 12px;
|
||||
color: #7d8798;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e8ee;
|
||||
font-size: 11px;
|
||||
|
||||
}
|
||||
|
||||
.sheet-selector {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
> span {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
width: min(220px, 28vw);
|
||||
}
|
||||
}
|
||||
|
||||
.source-file-link {
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
color: #5147df;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-loading {
|
||||
color: #5b50f2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.office-state {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
padding: 28px;
|
||||
color: #667085;
|
||||
text-align: center;
|
||||
|
||||
> i {
|
||||
color: #5b50f2;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
> strong {
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> span {
|
||||
max-width: 420px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&.is-error > i {
|
||||
color: #d92d20;
|
||||
}
|
||||
}
|
||||
|
||||
.docx-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 22px;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.docx-page {
|
||||
width: min(760px, 100%);
|
||||
min-height: calc(100% - 2px);
|
||||
padding: 54px clamp(30px, 7%, 68px);
|
||||
margin: 0 auto;
|
||||
color: #262b34;
|
||||
background: #fff;
|
||||
border: 1px solid #dfe3e9;
|
||||
box-shadow: 0 2px 10px rgb(15 23 42 / 8%);
|
||||
font-family: "Songti SC", SimSun, serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.docx-block {
|
||||
padding: 2px 6px;
|
||||
margin: 0 0 10px;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
transition: background-color 0.18s ease, box-shadow 0.18s ease;
|
||||
|
||||
&.is-list {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
&.is-highlighted {
|
||||
background: #fff0b8;
|
||||
box-shadow: inset 3px 0 #f0b429;
|
||||
}
|
||||
}
|
||||
|
||||
h1.docx-block { font-size: 22px; }
|
||||
h2.docx-block { font-size: 19px; }
|
||||
h3.docx-block { font-size: 17px; }
|
||||
h4.docx-block,
|
||||
h5.docx-block,
|
||||
h6.docx-block { font-size: 15px; }
|
||||
|
||||
.docx-table-wrap {
|
||||
max-width: 100%;
|
||||
margin: 12px 0 18px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.docx-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
|
||||
td {
|
||||
padding: 7px 9px;
|
||||
border: 1px solid #9da5b2;
|
||||
vertical-align: top;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.docx-table-row.is-highlighted td {
|
||||
background: #fff0b8;
|
||||
}
|
||||
|
||||
.xlsx-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.xlsx-grid {
|
||||
min-width: 100%;
|
||||
color: #344054;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
table-layout: auto;
|
||||
font-size: 11px;
|
||||
|
||||
th,
|
||||
td {
|
||||
min-width: 120px;
|
||||
max-width: 320px;
|
||||
height: 36px;
|
||||
padding: 7px 10px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-right: 1px solid #e4e7ec;
|
||||
border-bottom: 1px solid #e4e7ec;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
color: #475467;
|
||||
background: #f2f4f7;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
tbody tr:hover td,
|
||||
tbody tr:hover th {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.row-number-cell {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
min-width: 54px !important;
|
||||
width: 54px;
|
||||
color: #98a2b3;
|
||||
text-align: center !important;
|
||||
background: #f8fafc;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
thead .row-number-cell {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.xlsx-row.is-highlighted {
|
||||
td,
|
||||
th {
|
||||
background: #fff0b8;
|
||||
box-shadow: inset 0 2px #f0b429, inset 0 -2px #f0b429;
|
||||
}
|
||||
}
|
||||
|
||||
.xlsx-pagination {
|
||||
display: flex;
|
||||
min-height: 46px;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 7px 12px;
|
||||
color: #7d8798;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e8ee;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.office-empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.office-source-viewer {
|
||||
height: 360px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.office-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.docx-scroll {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.docx-page {
|
||||
padding: 34px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.docx-scroll,
|
||||
.xlsx-scroll {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.docx-block {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import OfficeSourceViewer from './OfficeSourceViewer.vue'
|
||||
import PdfSourceViewer from './PdfSourceViewer.vue'
|
||||
import { sourceLines } from './previewModel'
|
||||
import type { PreviewItem, ProcessType } from './types'
|
||||
@@ -10,6 +11,7 @@ const props = defineProps<{
|
||||
selectedId: string | null
|
||||
processType: ProcessType
|
||||
fileName: string
|
||||
fileFormat?: string
|
||||
taskId: string | number | null
|
||||
sourceFileId: string | number | null
|
||||
files: { id: string; name: string; count: number; modifiedCount: number }[]
|
||||
@@ -34,7 +36,13 @@ const editorDraft = ref('')
|
||||
const lines = computed(() => sourceLines(props.sourceText))
|
||||
const selectedItem = computed(() => props.items.find((item) => item.id === props.selectedId) ?? props.items[0])
|
||||
const editingItem = computed(() => props.items.find((item) => item.id === editingItemId.value))
|
||||
const isPdfSource = computed(() => /\.pdf$/i.test(props.fileName))
|
||||
const normalizedFileFormat = computed(() => (
|
||||
props.fileFormat?.toLowerCase().replace(/^\./, '')
|
||||
|| props.fileName.split('.').pop()?.toLowerCase()
|
||||
|| ''
|
||||
))
|
||||
const isPdfSource = computed(() => normalizedFileFormat.value === 'pdf')
|
||||
const isOfficeSource = computed(() => ['docx', 'xlsx'].includes(normalizedFileFormat.value))
|
||||
|
||||
const filteredItems = computed(() => props.items.filter((item, index) => {
|
||||
const matchesSearch = !search.value.trim()
|
||||
@@ -106,7 +114,7 @@ watch(selectedItem, async (item) => {
|
||||
currentPage.value = Math.floor(visibleIndex / PREVIEW_PAGE_SIZE) + 1
|
||||
}
|
||||
|
||||
if (isPdfSource.value || item.sourceStart == null) return
|
||||
if (isPdfSource.value || isOfficeSource.value || item.sourceStart == null) return
|
||||
await nextTick()
|
||||
const target = sourceViewerRef.value?.querySelector<HTMLElement>(`[data-source-start="${item.sourceStart}"]`)
|
||||
?? sourceViewerRef.value?.querySelector<HTMLElement>('.source-line.is-highlighted')
|
||||
@@ -176,6 +184,14 @@ function lineRange(item: PreviewItem) {
|
||||
:file-name="fileName"
|
||||
:selected-item="selectedItem ?? null"
|
||||
/>
|
||||
<OfficeSourceViewer
|
||||
v-else-if="isOfficeSource"
|
||||
:task-id="taskId"
|
||||
:source-file-id="sourceFileId"
|
||||
:file-name="fileName"
|
||||
:file-format="normalizedFileFormat"
|
||||
:selected-item="selectedItem ?? null"
|
||||
/>
|
||||
<div v-else ref="sourceViewerRef" class="source-viewer" tabindex="0" aria-label="源文件内容">
|
||||
<div
|
||||
v-for="line in lines"
|
||||
|
||||
Reference in New Issue
Block a user