|
|
@ -108,6 +108,7 @@ |
|
|
|
class="section-content markdown-content" |
|
|
|
v-html="renderContent(section, getItemValue(item, section.field))" |
|
|
|
@click="locateByText(getItemValue(item, section.field), item, section, categoryIndex, idx)" |
|
|
|
@contextmenu="handleContextMenu($event, getItemValue(item, section.field), item, section, categoryIndex, idx)" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 页面按钮 - 只在有pdfSource且存在多页时显示 --> |
|
|
@ -151,6 +152,7 @@ |
|
|
|
class="section-content markdown-content comparison-content" |
|
|
|
v-html="renderMarkdown(getComparisonContent(item, section))" |
|
|
|
@click="locateByText(getComparisonContent(item, section), item, section, categoryIndex, idx)" |
|
|
|
@contextmenu="handleContextMenu($event, getComparisonContent(item, section), item, section, categoryIndex, idx)" |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
@ -241,6 +243,7 @@ |
|
|
|
v-html="renderContent(section, getItemValue(item, section.field))" |
|
|
|
v-if="getItemValue(item, section.field)" |
|
|
|
@click="locateByText(getItemValue(item, section.field), item, section, index, idx)" |
|
|
|
@contextmenu="handleContextMenu($event, getItemValue(item, section.field), item, section, index, idx)" |
|
|
|
></div> |
|
|
|
|
|
|
|
<!-- 页面按钮 - 只在有pdfSource且存在多页时显示 --> |
|
|
@ -285,6 +288,7 @@ |
|
|
|
class="section-content markdown-content comparison-content" |
|
|
|
v-html="renderMarkdown(getComparisonContent(item, section))" |
|
|
|
@click="locateByText(getComparisonContent(item, section), item, section, index, idx)" |
|
|
|
@contextmenu="handleContextMenu($event, getComparisonContent(item, section), item, section, index, idx)" |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
@ -320,10 +324,39 @@ |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</Drawer> |
|
|
|
|
|
|
|
<!-- 右键菜单 --> |
|
|
|
<div |
|
|
|
v-if="contextMenu.visible" |
|
|
|
class="context-menu" |
|
|
|
:style="{ |
|
|
|
left: contextMenu.x + 'px', |
|
|
|
top: contextMenu.y + 'px', |
|
|
|
display: contextMenu.visible ? 'block' : 'none' |
|
|
|
}" |
|
|
|
@click.stop |
|
|
|
> |
|
|
|
<div |
|
|
|
class="context-menu-item" |
|
|
|
@click="locateSelectedText" |
|
|
|
v-if="contextMenu.canLocate && contextMenu.selectedText" |
|
|
|
> |
|
|
|
<span>📍 定位原文</span> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="context-menu-item disabled" |
|
|
|
v-else |
|
|
|
> |
|
|
|
<span>📍 定位原文{{ !contextMenu.selectedText ? '(未选中文本)' : '(不支持)' }}</span> |
|
|
|
</div> |
|
|
|
<div class="context-menu-item" @click="copySelectedText" v-if="contextMenu.selectedText"> |
|
|
|
<span>📋 复制选中文本</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts" setup> |
|
|
|
import { ref, computed, watch, nextTick, type PropType } from 'vue'; |
|
|
|
import { ref, computed, watch, nextTick, onUnmounted, type PropType } from 'vue'; |
|
|
|
import { Drawer, Button, Card, Switch, Tabs, TabPane } from 'ant-design-vue'; |
|
|
|
import { DownOutlined, UpOutlined, CopyOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue'; |
|
|
|
import { message } from 'ant-design-vue'; |
|
|
@ -440,6 +473,19 @@ |
|
|
|
const currentSelectItemIndex = ref<number | null>(null); |
|
|
|
const fieldPageButtons = ref<Record<string, number[]>>({}); |
|
|
|
|
|
|
|
// 右键菜单状态 |
|
|
|
const contextMenu = ref({ |
|
|
|
visible: false, |
|
|
|
x: 0, |
|
|
|
y: 0, |
|
|
|
selectedText: '', |
|
|
|
canLocate: false, |
|
|
|
currentItem: null as TaskResultItem | null, |
|
|
|
currentFieldConfig: null as FieldConfig | null, |
|
|
|
currentCategoryIndex: null as number | null, |
|
|
|
currentItemIndex: null as number | null |
|
|
|
}); |
|
|
|
|
|
|
|
const pdfContainerRef = ref<InstanceType<typeof ReviewPdfContainer> | null>(null); |
|
|
|
|
|
|
|
// PDF布局配置(支持tabs模式下动态切换) |
|
|
@ -509,14 +555,11 @@ |
|
|
|
const matches = [ |
|
|
|
category.name === tabConfig.label, |
|
|
|
category.name === tabConfig.key, |
|
|
|
category.name.includes(tabConfig.label), |
|
|
|
tabConfig.label.includes(category.name), |
|
|
|
|
|
|
|
// tabConfig.label.includes(category.name), |
|
|
|
// 额外的匹配:去掉括号和空格后的匹配 |
|
|
|
category.name.replace(/[()\(\)\s]/g, '') === tabConfig.label.replace(/[()\(\)\s]/g, ''), |
|
|
|
// 处理特殊情况:审查类型的匹配 |
|
|
|
(category.name.includes('审查') && tabConfig.label.includes('审查') && |
|
|
|
(category.name.includes(tabConfig.label.replace('审查', '')) || |
|
|
|
tabConfig.label.includes(category.name.replace('审查', '')))) |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
const isMatch = matches.some(Boolean); |
|
|
@ -1202,6 +1245,32 @@ |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 组件挂载时添加全局点击事件监听 |
|
|
|
const handleGlobalClick = (event: Event) => { |
|
|
|
// 如果点击的不是右键菜单,则隐藏菜单 |
|
|
|
const target = event.target as HTMLElement; |
|
|
|
if (!target.closest('.context-menu')) { |
|
|
|
hideContextMenu(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 添加和移除事件监听器 |
|
|
|
watch(() => contextMenu.value.visible, (visible) => { |
|
|
|
if (visible) { |
|
|
|
document.addEventListener('click', handleGlobalClick); |
|
|
|
document.addEventListener('contextmenu', handleGlobalClick); |
|
|
|
} else { |
|
|
|
document.removeEventListener('click', handleGlobalClick); |
|
|
|
document.removeEventListener('contextmenu', handleGlobalClick); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 组件卸载时清理事件监听器 |
|
|
|
onUnmounted(() => { |
|
|
|
document.removeEventListener('click', handleGlobalClick); |
|
|
|
document.removeEventListener('contextmenu', handleGlobalClick); |
|
|
|
}); |
|
|
|
|
|
|
|
watch([expandReadItems, expandAdoptedItems], () => { |
|
|
|
updateActiveItemKeys(); |
|
|
|
}); |
|
|
@ -1231,6 +1300,93 @@ |
|
|
|
const comparisonField = getComparisonField(fieldConfig); |
|
|
|
return item[comparisonField] || ''; |
|
|
|
}; |
|
|
|
|
|
|
|
// 定位选中的文本 |
|
|
|
const locateSelectedText = async () => { |
|
|
|
if (!contextMenu.value.selectedText || !contextMenu.value.canLocate) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const { selectedText, currentItem, currentFieldConfig, currentCategoryIndex, currentItemIndex } = contextMenu.value; |
|
|
|
|
|
|
|
// 隐藏右键菜单 |
|
|
|
contextMenu.value.visible = false; |
|
|
|
|
|
|
|
// 调用现有的定位方法 |
|
|
|
await locateByText( |
|
|
|
selectedText, |
|
|
|
currentItem || undefined, |
|
|
|
currentFieldConfig || undefined, |
|
|
|
currentCategoryIndex || undefined, |
|
|
|
currentItemIndex || undefined |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
// 处理右键菜单 |
|
|
|
const handleContextMenu = (event: MouseEvent, fieldValue: any, item: TaskResultItem, fieldConfig: FieldConfig, categoryIndex: number, itemIndex: number) => { |
|
|
|
console.log('右键菜单触发', { event, fieldValue, item, fieldConfig, categoryIndex, itemIndex }); |
|
|
|
event.preventDefault(); |
|
|
|
event.stopPropagation(); |
|
|
|
|
|
|
|
// 获取选中的文本 |
|
|
|
const selection = window.getSelection(); |
|
|
|
const selectedText = selection?.toString().trim(); |
|
|
|
|
|
|
|
console.log('选中的文本:', selectedText); |
|
|
|
|
|
|
|
// 即使没有选中文本,也显示菜单(但禁用状态) |
|
|
|
if (!selectedText) { |
|
|
|
console.log('没有选中文本,显示禁用菜单'); |
|
|
|
contextMenu.value = { |
|
|
|
visible: true, |
|
|
|
x: event.clientX, |
|
|
|
y: event.clientY, |
|
|
|
selectedText: '', |
|
|
|
canLocate: false, |
|
|
|
currentItem: item, |
|
|
|
currentFieldConfig: fieldConfig, |
|
|
|
currentCategoryIndex: categoryIndex, |
|
|
|
currentItemIndex: itemIndex |
|
|
|
}; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查字段是否支持PDF定位 |
|
|
|
const canLocate = supportsPdfLocation(fieldConfig); |
|
|
|
|
|
|
|
// 设置右键菜单状态 |
|
|
|
contextMenu.value = { |
|
|
|
visible: true, |
|
|
|
x: event.clientX, |
|
|
|
y: event.clientY, |
|
|
|
selectedText: selectedText, |
|
|
|
canLocate: canLocate, |
|
|
|
currentItem: item, |
|
|
|
currentFieldConfig: fieldConfig, |
|
|
|
currentCategoryIndex: categoryIndex, |
|
|
|
currentItemIndex: itemIndex |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
// 隐藏右键菜单(点击其他地方时) |
|
|
|
const hideContextMenu = () => { |
|
|
|
contextMenu.value.visible = false; |
|
|
|
}; |
|
|
|
|
|
|
|
// 复制选中的文本 |
|
|
|
const copySelectedText = () => { |
|
|
|
if (contextMenu.value.selectedText) { |
|
|
|
navigator.clipboard.writeText(contextMenu.value.selectedText) |
|
|
|
.then(() => { |
|
|
|
message.success('选中文本已复制到剪贴板'); |
|
|
|
}) |
|
|
|
.catch((err) => { |
|
|
|
console.error('复制失败:', err); |
|
|
|
message.error('复制失败'); |
|
|
|
}); |
|
|
|
} |
|
|
|
contextMenu.value.visible = false; |
|
|
|
}; |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="less" scoped> |
|
|
@ -1393,6 +1549,18 @@ |
|
|
|
overflow: auto; |
|
|
|
font-size: 18px; |
|
|
|
font-weight: 500; |
|
|
|
user-select: text; /* 允许文本选择 */ |
|
|
|
|
|
|
|
/* 选中文本的样式 */ |
|
|
|
::selection { |
|
|
|
background-color: #bae7ff; |
|
|
|
color: #1890ff; |
|
|
|
} |
|
|
|
|
|
|
|
::-moz-selection { |
|
|
|
background-color: #bae7ff; |
|
|
|
color: #1890ff; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.comparison-section { |
|
|
@ -1567,4 +1735,48 @@ |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
/* 右键菜单样式 */ |
|
|
|
.context-menu { |
|
|
|
position: fixed; |
|
|
|
background: #ffffff; |
|
|
|
border: 1px solid #d9d9d9; |
|
|
|
border-radius: 6px; |
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|
|
|
padding: 4px 0; |
|
|
|
min-width: 140px; |
|
|
|
z-index: 9999; |
|
|
|
user-select: none; |
|
|
|
} |
|
|
|
|
|
|
|
.context-menu-item { |
|
|
|
padding: 8px 16px; |
|
|
|
cursor: pointer; |
|
|
|
font-size: 14px; |
|
|
|
color: #262626; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
transition: all 0.2s; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
background-color: #f5f5f5; |
|
|
|
color: #1890ff; |
|
|
|
} |
|
|
|
|
|
|
|
&.disabled { |
|
|
|
color: #bfbfbf; |
|
|
|
cursor: not-allowed; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
background-color: transparent; |
|
|
|
color: #bfbfbf; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
span { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |