提交 d020eac5 authored 作者: 安云鹏's avatar 安云鹏

Merge branch 'ayp-dev' into pre

差异被折叠。
<template>
<div class="intelligenceLeftTabBar">
<div class="navBox" :class="{navBoxShow:isNavMenuShow}">
<div class="navList" v-for="(item,index) in navList " :key="index" :class="{on:navPath==item.path}" @click="onNavListClick(item.path)">
<div class="icon" :style="{background:`url(${item.img})no-repeat`,backgroundSize:'24px 24px',backgroundPosition:'17px 17px'}"></div>
<span class="text-tip-1" style="white-space: nowrap; ">{{ item.name }}</span>
</div>
</div>
<img class="show" src="@/assets/icons/muenShow.png" :style="isNavMenuShow?'transform: scaleX(1)':''" alt="" @click="()=>{isNavMenuShow=!isNavMenuShow}">
</div>
</template>
<script setup>
import muen1 from '@/assets/icons/tool-item-icon1.png'
import muen2 from '@/assets/icons/tool-item-icon2.png'
import muen3 from '@/assets/icons/tool-item-icon3.png'
import muen4 from '@/assets/icons/tool-item-icon4.png'
import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const isNavMenuShow=ref(false)
const navList=ref([
{
img:muen1,
path:'/writtingAsstaint',
name:'智能写报'
},
{
img:muen2,
path:'/writtingAsstaint1',
name:'智能翻译'
},
{
img:muen3,
path:'/writtingAsstaint2',
name:'智能查询'
},
{
img:muen4,
path:'/writtingAsstaint3',
name:'智能对话'
},
])
const navPath=ref()
const route=useRoute()
if(route.path){
navPath.value=route.path
}
const onNavListClick=(path)=>{
if(path=='/writtingAsstaint'){
navPath.value=path
}else{
ElMessage.error('正在开发中')
}
}
</script>
<style lang="scss" scoped>
.intelligenceLeftTabBar{
padding: 5px 0;
border-right: 1px solid rgb(234, 236, 238);
position: relative;
.navBox{
height: 100%;
width: 65px;
transition: all 0.3s;
padding: 0 3px;
.navList{
display: flex;
align-items: center;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
height: 60px;
.icon{
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 10px;
margin-right: 15px;
}
}
.on{
background-color: var(--color-primary-10);
color: var(--color-primary-100);
font-weight: Bold;
}
}
.navBoxShow{
width: 200px;
transition: all 0.3s;
}
.show{
position: absolute;
width: 24px;
height: 24px;
right: 21px;
bottom: 21px;
cursor: pointer;
transform: scaleX(-1)
}
}
</style>
\ No newline at end of file
......@@ -295,13 +295,16 @@ export function useMarkdownStream() {
// 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value)
let content = rawContent.value || ''
// 将 ==n== 转换为按钮样式的 HTML
// 使用正向预读和反向预读确保只匹配被 == 包裹的数字
content = content.replace(/==(\d+)==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
})
// content = content.replace(/==(\d+)、==/g, (match, p1) => {
// return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
// })
console.log(content,11223)
content = content.replace(/==\s*(\d+)、.*?==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${match.replace(/==/g, '') }">${p1}</button>`;
});
return md.render(content)
})
......
......@@ -7,7 +7,7 @@
<router-view />
</div>
</div>
<div class="right-btn" @click="handleClickToolBox">
<div class="right-btn" @click="handleClickToolBox" v-if="route.path!='/writtingAsstaint'">
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" />
......@@ -22,7 +22,7 @@
</div>
</div>
<div class="tool-box">
<div class="tool-box" v-if="route.path!='/writtingAsstaint'">
<!-- <div class="tool-item">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
......@@ -816,4 +816,7 @@ body {
cursor: not-allowed;
pointer-events: none;
}
</style>
<template>
<div class="headerBox">
<div class="tabBox" v-if="store.bottomProgressNum>0">
<div class="fileName">
<img src="@/assets/icons/pdf-icon.png" alt=" ">
<span class="text-tip-1-bold">{{ store.uploadFileList[0]?.name||'文件错误' }}</span>
</div>
<div class="tab">
<div class="tabList text-tip-1-bold" v-for="(item,index) in store.tabList" :key="index" :class="{'on':store.headerTabType==item.type}"
:style="!item.active?'color:#bfbfbf;cursor: no-drop;':''"
@click="onTabListClick(item.type,item.active)">{{ item.name }}</div>
</div>
<div class="switch" v-if="store.headerTabType=='translate'">
<el-switch v-model="store.isShowOriginal"/>
<div class="iconBOx">
<img src="@/assets/icons/translate-icon.png" alt="">
<span class="text-tip-1">显示原文</span>
</div>
<el-button @click="store.handleIsSsearchFor"><img style="width: 16px;" src="@/assets/icons/aiBox/search.png" alt=""> 查找</el-button>
</div>
<div v-else style="margin-right: 22px;">
<el-button @click="onExport">导出</el-button>
</div>
</div>
<div class="logo" v-else>
<img src="@/assets/icons/tool-item-icon1.png" alt="">
<span class="text-title-3-bold">智能写库</span>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,computed,watch} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const emit = defineEmits(["onExport"]);
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const onTabListClick= (type,active)=>{
if(!active) return
store.handleHeaderTab(type)
}
const onExport=()=>{
if(store.headerTabType=='mind'){
emit('onExport')
}else if(store.headerTabType=='message'){
store.exportContent
}
}
</script>
<style lang="scss" scoped>
.headerBox{
background-color: #fff;
border-bottom: 1px solid rgb(234, 236, 238);
.logo{
height: 60px;
display: flex;
align-items: center;
img{
width: 30px;
height: 30px;
margin-left: 28px;
margin-right: 20px;
}
}
.tabBox{
height: 60px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.fileName{
display: flex;
align-items: center;
img{
width: 24px;
height: 24px;
margin-left: 28px;
margin-right: 16px;
}
}
.tab{
width: 232px;
display: flex;
justify-content: space-between;
position: absolute;
left: 50%;
transform: translate(-50%);
.tabList{
cursor: pointer;
height: 59px;
line-height: 66px;
}
.on{
border-bottom: 4px solid var(--color-primary-100);
color:var(--color-primary-100) ;
transition: all 0.1s;
}
}
.btns{
margin-right: 23px;
}
}
.switch{
display: flex;
align-items: center;
margin-right: 23px;
.iconBOx{
display: flex;
align-items: center;
margin-right: 12px;
img{
width: 16px;
height: 16px;
margin:0 5px;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="left-box-wrapper">
<div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div>
<!-- <div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div> -->
<div class="left-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box-input">
......@@ -11,15 +11,15 @@
<div class="header">报文主题</div>
<div class="title-box">
<div class="title">主题名称</div>
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px"
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px ;background: #f7f8f9;"
class="title-input" placeholder="输入主题名称,如:大而美法案" v-model="store.writtingTitle" />
</div>
<div class="description-box">
<!-- <div class="description-box">
<div class="title">主题描述</div>
<el-input :disabled="store.isDisableTemplate" class="description-input" type="textarea"
style="width: 476px" :rows="8" placeholder="输入报文主题描述,如:从科技领域方面分析大而美法案通过后对中国可能产生的影响"
v-model="store.descText" />
</div>
</div> -->
</div>
<!-- 报文模板 -->
......@@ -88,24 +88,24 @@
</div>
<!-- 提交区域 -->
<div class="submit-area">
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成按钮 -->
<div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<!-- <div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<div class="submit-icon">
<img src="../assets/images/ai.png" alt="" />
</div>
<div class="submit-text">生成报文</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box" v-else>
<!-- <div class="process-footer-box" v-else>
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -114,11 +114,11 @@
<div class="text">停止</div>
</div>
</div>
</div>
</div> end -->
</div>
<!-- 步骤侧边栏(拆分出来) -->
<div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<!-- <div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<div class="left-box-input">
<div class="process-box">
<div class="process-main-box">
......@@ -137,17 +137,17 @@
</div>
</div>
</div>
</div>
<div class="submit-area">
</div> -->
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box">
<!-- <div class="process-footer-box">
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -155,25 +155,23 @@
<div class="icon"></div>
<div class="text">停止</div>
</div>
</div>
</div>
</div>
<!-- 条款翻译侧边栏 -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="store.isShowClauseTranslation">
</div> -->
<!-- </div>
</div> -->
<!-- 条款翻译侧边栏 srot -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps&&store.headerTabType=='message'">
<div class="translation-main-box">
<div class="translation-actions" v-if="!store.isGenerating">
<!-- <div class="translation-actions" v-if="!store.isGenerating">
<div class="back-input-btn" @click="store.backToInputAndClear">返回输入栏</div>
</div>
</div> -->
<!-- 政令标题卡片 -->
<div class="metadata-card" v-if="store.pdfMetadata">
<!-- <div class="metadata-card" v-if="store.pdfMetadata">
<div class="card-header">
<div class="chinese-name">{{ store.pdfMetadata.name }}</div>
<div class="type-tag">{{ store.pdfMetadata.signing_date }}</div>
</div>
<div class="english-name">{{ store.pdfMetadata.order_title }}</div>
</div>
</div> -->
<div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right">
......@@ -200,7 +198,7 @@
</div>
</div>
<!-- 步骤侧边栏显隐按钮 -->
<div class="toggle-steps-btn" @click="store.isShowSteps = !store.isShowSteps">
<div class="toggle-steps-btn" @click=" isShowSteps ">
<div class="arrow" :class="{ 'is-active': store.isShowSteps }"></div>
</div>
</div>
......@@ -225,6 +223,10 @@ const emit = defineEmits(["generate"]);
const triggerGenerate = () => {
emit("generate");
};
const isShowSteps=()=>{
store.isShowSteps = !store.isShowSteps
store.highlightClauseId=''
}
// 数字转中文序号
const getChineseNumber = (num) => {
......@@ -245,18 +247,30 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
watch(
() => store.highlightClauseId,
async (newId) => {
if (!newId || !translationContentRef.value) return;
console.log('789')
await nextTick();
if (!newId || !translationContentRef.value) return;
const container = translationContentRef.value;
const item = container.querySelector(`.translation-item[data-clause-number="${newId}"]`);
const result = newId.replace(/^\d+、/, '');
const item = container.querySelector(`.translation-item[data-clause-number="${newId.match(/^(\d+)/)[1]}"]`);
const itemHtml=item.querySelector(`.translated-text`);
if (!item) return;
// 你已经拿到的 外层大标签
const element =itemHtml
// 你要找的文字
const targetText = result
const location=findTextInElement(element, targetText);
const containerRect = container.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
const delta = itemRect.top - containerRect.top;
const targetTop = container.scrollTop + delta;
// 让高亮条款的“标题区域”贴到容器顶部
container.scrollTo({
top: Math.max(0, targetTop),
......@@ -264,6 +278,74 @@ watch(
});
}
);
// 👇 核心:在 element 内部找文字位置
// =========================================
const findTextInElement=(element, targetText )=> {
// 遍历标签内的所有内容
const nodes = element.childNodes;
let rect =''
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 只找纯文字
if (node.nodeType === 3) {
const originalText = node.textContent;
const cleanText=(str)=> {
return str
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/,/g, ',')
.replace(/。/g, '.')
.replace(/:/g, ':')
.replace(/;/g, ';');
}
// 清理后的文字(无标点)
const nodeClean = cleanText(originalText);
const targetClean = cleanText(targetText);
// 用干净文字对比
if (nodeClean.includes(targetClean)) {
// 找到真实位置(用原始文本定位,不影响)
const index = originalText.indexOf(
originalText.includes(targetText) ? targetText : originalText
);
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + targetText.length);
// 拿到位置
rect = range.getBoundingClientRect();
const marks = element.querySelectorAll('mark');
marks.forEach(mark => {
// 把 mark 里的文字放回原位,删除标签
const parent = mark.parentNode;
while (mark.firstChild) {
parent.insertBefore(mark.firstChild, mark);
}
parent.removeChild(mark);
// 合并相邻文本节点(恢复页面原貌)
parent.normalize();
});
const mark = document.createElement("mark");
mark.style.backgroundColor = '#055FC2';
mark.style.color = "#fff"; // 文字颜色
range.surroundContents(mark);
break;
}
}
}
return rect
}
// 监听 store.processLog 变化,更新步骤内容并滚动
watch(
......@@ -308,7 +390,7 @@ defineExpose({
width: 521px;
height: 100%;
padding-top: 22px;
padding-bottom: 29px;
padding-bottom: 10px;
box-sizing: border-box;
border-right: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
......@@ -1026,4 +1108,4 @@ defineExpose({
}
}
}
</style>
\ No newline at end of file
</style>
......@@ -41,7 +41,9 @@ const handleGlobalClick = (e) => {
const clauseId = btn.getAttribute('data-clause');
if (clauseId) {
store.highlightClauseId = clauseId;
store.isShowSteps = true;
// 翻译栏一直显示,所以这里只需要确保它在视图内
console.log(store.highlightClauseId )
}
}
};
......@@ -88,6 +90,7 @@ watch(
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
......@@ -103,7 +106,7 @@ watch(
}
.content-box {
width: 1069px;
width: 100%;
height: 100%;
overflow-y: auto;
padding: 20px 80px;
......@@ -111,8 +114,8 @@ watch(
font-size: 16px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
background: rgba(255, 255, 255, 1);
margin: 17px auto 0 auto;
......
<template>
<div style="width: 100%; display: flex;justify-content: center;align-items: stretch;">
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" :report-content="store.reportContent" />
<div v-if="!store.reportContent" class="main-placeholder">
<img src="../assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div>智能体写报任务执行中...</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingMainBox from "./WrittingMainBox.vue";
const store = useWrittingAsstaintStore();
const mainBoxRef = ref(null); // 右侧子组件ref
</script>
<style lang="scss" scoped>
.main-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: #f7f8f9;
img {
max-width: 100%;
max-height: 80%;
object-fit: contain;
display: block;
}
.placeholder-text {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
</style>
\ No newline at end of file
<template>
<div class="mind-map-container">
<div ref="containerRef" class="mind-map"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import G6 from '@antv/g6'
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
// pushfenzhi
let mindMapData = {
id: 'root',
label: '名称',
children: [
]
}
let uniqueId = 1;
function convertMindMap(rawData) {
uniqueId = 1; // 每次重置
const tree = buildTreeRecursive(rawData.node, rawData.links);
return tree[0]; // 返回单个根节点
}
const raw = store.resultWriteData.政令深度分析.条款分析.科技条款内容[0].领域举措.data;
const treeData = convertMindMap(raw);
// 全局唯一自增计数器(最稳、最短、永不重复)
// 递归构建树 + 自动生成全局唯一数字ID
function buildTreeRecursive(nodes, links, parentId = null) {
const tree = [];
// 找到当前父节点的所有子节点
const childrenNodes = nodes.filter(node => {
if (parentId === null) {
return node.depth === 0;
}
return links.some(
link => link.source === parentId && link.target === node.id
);
});
// 遍历子节点,递归生成
for (const node of childrenNodes) {
const currentNode = {
id: `${uniqueId++}_`, // 🔥 纯数字自增,绝对唯一!
label: node.name,
children: buildTreeRecursive(nodes, links, node.id)
};
tree.push(currentNode);
}
return tree;
}
treeData.id= 'root',
console.log(treeData)
mindMapData=treeData
const containerRef = ref(null)
let graph = null
// 文字换行(不溢出)
function splitTextToLines(text, maxWidth, fontSize) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = `${fontSize}px sans-serif`
const lines = []
let currentLine = ''
let width = 0
for (const char of text) {
const w = ctx.measureText(char).width
if (width + w > maxWidth) {
lines.push(currentLine)
currentLine = char
width = w
} else {
currentLine += char
width += w
}
}
if (currentLine) lines.push(currentLine)
const lineHeight = fontSize + 8
const totalHeight = lineHeight * lines.length + 24
return { lines, lineHeight, totalHeight }
}
onMounted(() => {
if (graph) graph.destroy()
const el = containerRef.value
if (!el) return
// 注册节点
G6.registerNode('custom-node', {
draw(cfg, group) {
const isRoot = cfg.id === 'root'
const isDept = cfg.id.startsWith('dept')
const MAX_WIDTH = isRoot ? 280 : isDept ? 240 : 400
const fontSize = isRoot ? 16 : isDept ? 15 : 14
const fontWeight = isRoot || isDept ? 'bold' : 'normal'
const color = 'rgba(5, 95, 194, 1)'
const padding = 16
const { lines, lineHeight, totalHeight } = splitTextToLines(cfg.label, MAX_WIDTH - padding * 2, fontSize)
const nodeW = MAX_WIDTH
const nodeH = totalHeight
// 节点背景
group.addShape('rect', {
attrs: {
x: -nodeW / 2,
y: -nodeH / 2,
width: nodeW,
height: nodeH,
fill: '#f8fcff',
stroke: color,
lineWidth: 1,
radius: 6
}
})
// 文字居中
lines.forEach((line, i) => {
group.addShape('text', {
attrs: {
text: line,
x: 0,
y: (i - (lines.length - 1) / 2) * lineHeight,
textAlign: 'center',
textBaseline: 'middle',
fontSize, fontWeight, fill: color
}
})
})
return group
},
getAnchorPoints: () => [[0, 0.5], [1, 0.5]]
}, 'single-node')
graph = new G6.TreeGraph({
container: el,
width: el.offsetWidth,
height: el.offsetHeight,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'collapse-expand']
},
defaultNode: { type: 'custom-node' },
defaultEdge: {
type: 'cubic-horizontal',
style: { stroke: 'rgba(5, 95, 194, 0.4)', lineWidth: 1 }
},
layout: {
type: 'compactBox',
direction: 'LR',
getWidth: () => 400,
getHGap: () => 80,
getVGap: () => 50,
}
})
graph.data(mindMapData)
graph.render()
// 自适应显示
setTimeout(() => {
graph.fitCenter()
graph.zoomTo(0.65)
}, 200)
})
onUnmounted(() => graph?.destroy())
function exportGraph() {
if (!graph) return
graph.downloadFullImage('思维导图.png')
}
defineExpose({
exportGraph
});
</script>
<style scoped>
.mind-map-container {
width: 100%;
height: 100vh;
background: #f5f7fa;
}
.mind-map {
width: 92%;
height: 100%;
margin: 0 auto;
}
</style>
\ No newline at end of file
<template>
<div class="translation-content" ref="translationContentRef">
<!-- :class="{ active: store.highlightClauseId === item.payload?.clause_number }"
:data-clause-number="item.payload?.clause_number" -->
<!-- 查找 -->
<div class="searchFor" v-if="store.isSsearchFor">
<el-input v-model="keyword" style="width: 260px;" placeholder="查找原文内容" />
<div class="searchTextNum">
<span v-if="total==0">0</span>
<span v-else> {{ current + 1 }}</span>/{{ total }}
</div>
<div class="prev" @click="prev"><el-icon><ArrowUp /></el-icon></div>
<div class="next" @click="next"><el-icon><ArrowDown /></el-icon></div>
<div class="close" @click="closeClick"><el-icon><CloseBold /></el-icon></div>
</div>
<div class="content-box" ref="contentBox">
<div class="translation-item" v-for="(item, index) in renderList" :key="index">
<div class="item-body">
<div class="original-text" v-if="store.isShowOriginal">
<template v-for="(t, i) in item.fragments" :key="i">
<span :class="{ high: t.hit, current: t.hit && currentGlobalIndex === t.globalIndex}">
{{ t.text }}
</span>
</template>
</div>
<div class="translated-text">
<span class="clause-title">第{{ getChineseNumber(item.payload?.clause_number) }}节</span>{{ item.payload?.clause_content_zh }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,watch,computed} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import { useStream } from "@/hooks/useStream";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const translationContentRef = ref(null);
// 数字转中文序号
const getChineseNumber = (num) => {
const zh = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
const n = parseInt(num);
if (n <= 10) return zh[n];
if (n < 20) return '十' + zh[n % 10];
if (n < 100) {
return zh[Math.floor(n / 10)] + '十' + (n % 10 === 0 ? '' : zh[n % 10]);
}
return num;
};
const closeClick=()=>{
store.handleIsSsearchFor()
current.value=0
total.value=0
keyword.value=''
}
const keyword = ref('')
const contentBox = ref(null)
const current = ref(0)
const total = ref(0)
// 渲染列表(不修改原数据,自动更新)
const renderList = computed(() => {
const key = keyword.value.trim()
return (store.clauseTranslationMessages || []).map(item => {
const section = item.payload?.clause_section || ''
const content = item.payload?.clause_content || ''
const fullText = section + ' ' + content
if (!key) {
return {
...item,
fragments: [{ text: fullText, hit: false, globalIndex: -1 }]
}
}
const parts = fullText.split(new RegExp(`(${key})`, 'g'))
const fragments = []
parts.forEach(t => {
fragments.push({
text: t,
hit: t === key,
globalIndex: -1
})
})
return { ...item, fragments }
})
})
// 全局匹配列表(只计算一次,修复蓝色全部选中BUG)
const globalMatchList = computed(() => {
const arr = []
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) arr.push(f)
})
})
return arr
})
// 当前高亮的全局索引
const currentGlobalIndex = computed(() => {
if (!globalMatchList.value.length) return -1
return globalMatchList.value[current.value]?.globalIndex ?? -2
})
// 总数
watch(globalMatchList, (val) => {
total.value = val.length
current.value = 0
}, { immediate: true })
// 给每个命中项分配唯一 index
watch([renderList, globalMatchList], () => {
let idx = 0
const map = new Map()
globalMatchList.value.forEach(item => {
map.set(item, idx++)
})
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) f.globalIndex = map.get(f)
})
})
})
function doSearch() {
current.value = 0
scrollTo(0)
}
function prev() {
if (!total.value) return
current.value = (current.value - 1 + total.value) % total.value
console.log(current.value )
scrollTo(current.value)
}
function next() {
if (!total.value) return
current.value = (current.value + 1) % total.value
scrollTo(current.value)
}
function scrollTo(idx) {
setTimeout(() => {
const all = contentBox.value?.querySelectorAll('.high')
if (all?.[idx]) {
contentBox.value.scrollTo({
top: all[idx].offsetTop - 100,
behavior: 'smooth'
})
}
}, 0)
}
</script>
<style lang="scss" scoped>
.content-box{
overflow-y: auto;
margin-top: 30px;
height: calc(100vh - 250px);
.translation-item{
margin-bottom: 24px;
.item-body{
display: flex;
justify-content: space-between;
width: 85%;
margin: 0 auto;
.original-text{
min-width: 48%;
line-height: 30px;
margin-right: 62px;
flex: 2;
}
.translated-text{
width: 100%;
line-height: 30px;
}
}
}
}
.searchFor{
display: flex;
align-items: center;
width: 430px;
height: 60px;
padding: 12px 0;
border-radius: 10px;
background-color: #fff;
border: 1px solid rgb(234, 236, 238);
position: fixed;
top: 120px;
right: 20px;
:deep(.el-input__wrapper){
background-color: #fff;
}
.searchTextNum{
width: 70px;
height: 100%;
border-right: 1px solid rgb(234, 236, 238);
line-height: 40px;
margin-right: 16px;
text-align: center;
}
.prev{
margin-right: 12px;
}
.next{
margin-right: 12px;
}
// position: absolute;
}
.high {
background: #ffeb3b;
}
.current {
background: #409eff !important;
color: #fff !important;
}
</style>
\ No newline at end of file
<template>
<div class="writting-wrapper">
<!-- 头部区域 -->
<div class="writting-header">
<!-- <div class="writting-header">
<div class="tab-box">
<div class="tab" :class="{ tabActive: item.active }" v-for="(item, index) in store.tabList"
:key="index">
......@@ -10,12 +10,6 @@
</div>
<div class="edit-box"></div>
<div class="btn-box">
<!-- <div class="btn" @click="store.exportContent">
<div class="icon">
<img src="./assets/images/export-icon.png" alt="" />
</div>
<div class="text">导出</div>
</div> -->
<div class="btn" @click="store.toggleEditMode">
<div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
......@@ -30,22 +24,38 @@
<div class="text text1">保存</div>
</div>
</div>
</div>
</div> -->
<IntelligenceLeftTabBar></IntelligenceLeftTabBar>
<!-- 主体区域:子组件 -->
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<writtingleftBox ref="leftBoxRef" @generate="handleGenerate" />
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" />
<!-- 无数据时显示占位图 -->
<div v-show="!store.reportContent" class="main-placeholder">
<img src="./assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div v-if="store.isGenerating">智能体写报任务执行中...</div>
<div v-else>上传文件后点击“生成报文”开始写报...</div>
<div style="width: 100%;">
<WrittingHeader @onExport="onExport"></WrittingHeader>
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<!-- <writtingleftBox ref="leftBoxRef" @generate="handleGenerate" /> -->
<WrittingLeftBox ref="leftBoxRef" />
<!-- 翻译 -->
<WrittingTranslate v-if="store.isShowClauseTranslation&&store.headerTabType=='translate'"></WrittingTranslate>
<!-- 思维导图 " -->
<WrittingMind v-else-if="store.isShowClauseTranslation&&store.headerTabType=='mind' " ref="mindRef"></WrittingMind>
<!-- 写报 -->
<WrittingMessage v-else-if="store.isShowClauseTranslation&&store.headerTabType=='message'"></WrittingMessage>
<!-- 无数据时显示占位图 -->
<div v-else class="main-placeholder">
<img src="./assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div v-if="store.isGenerating">智能体写报任务执行中...</div>
<div v-else>上传文件后点击“生成报文”开始写报...</div>
</div>
</div>
<!-- 右侧子组件:绑定ref -->
<!-- <writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" /> -->
</div>
<WrittingBottom @generate="handleGenerate" @write="handleWrite"></WrittingBottom>
</div>
</div>
</template>
......@@ -55,14 +65,27 @@ import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingleftBox from "./components/WrittingLeftBox.vue";
import writtingMainBox from "./components/WrittingMainBox.vue";
import WrittingLeftBox from "./components/WrittingLeftBox.vue";
import WrittingHeader from "./components/WrittingHeader.vue"; //头
import WrittingBottom from "./components/WrittingBottom.vue"; //底部
import WrittingTranslate from "./components/WrittingTranslate.vue"; //翻译
import WrittingMind from "./components/WrittingMind.vue"; //思维导图
import WrittingMessage from "./components/WrittingMessage.vue"; //写报
// 获取路由实例(组件内读取)
const route = useRoute();
// 获取Pinia Store实例
const leftBoxRef = ref(null); // 左侧子组件ref
const mainBoxRef = ref(null); // 右侧子组件ref
const mindRef=ref(null) //思维导图ref
const onExport = () => {
mindRef.value.exportGraph()
}
const store = useWrittingAsstaintStore();
// 2. 核心:触发生成流程
......@@ -83,6 +106,19 @@ const handleGenerate = async () => {
console.error("生成报文失败:", error);
}
};
const handleWrite=async ()=>{
try {
// // 等待DOM更新(确保子组件DOM已挂载)
store.tabList[2].active=true //写报生成之后放开写报按钮
store.headerTabType='message'
await nextTick();
await store.generateWrite()
} catch (error) {
ElMessage.error(error.message);
console.error("生成写报失败:", error);
}
}
// 生命周期
onMounted(async () => {
......@@ -100,7 +136,7 @@ onUnmounted(() => {
.writting-wrapper {
width: 100%;
height: 100%;
display: flex;
.writting-header {
height: 60px;
box-sizing: border-box;
......@@ -191,7 +227,7 @@ onUnmounted(() => {
.writting-main {
display: flex;
height: calc(100% - 60px);
height: calc(100% - 126px);
position: relative;
.main-placeholder {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论