提交 7a10f6fc authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #226 已通过 于阶段
in 1 分 24 秒
差异被折叠。
......@@ -37,7 +37,7 @@
<div class="icon">
<img :src="item.icon" alt="" />
</div>
<div class="title">{{ item.title }}</div>
<div class="title" :class="{ 'active-title': item.active }">{{ item.title }}</div>
</div>
</div>
</div>
......@@ -157,57 +157,68 @@ const menuList = ref([
{
title: "科技法案",
icon: Menu2,
path: "/billHome"
path: "/billHome",
active: false
},
{
title: "科技政令",
icon: Menu3,
path: "/decree"
path: "/decree",
active: false
},
{
title: "美国科技智库",
icon: Menu4,
path: "/thinkTank"
path: "/thinkTank",
active: false
},
{
title: "出口管制",
icon: Menu5,
path: "/exportControl"
path: "/exportControl",
active: false
},
{
title: "科研合作限制",
icon: Menu6,
path: "/cooperationRestrictions"
path: "/cooperationRestrictions",
active: false
},
{
title: "投融资限制",
icon: Menu7,
path: "/finance"
path: "/finance",
active: false
},
{
title: "市场准入限制",
icon: Menu8,
path: "/marketAccessRestrictions"
path: "/marketAccessRestrictions",
active: false
},
{
title: "规则限制",
icon: Menu9,
path: "/ruleRestrictions"
path: "/ruleRestrictions",
active: false
},
{
title: "美国科技人物观点",
icon: Menu10,
path: "/technologyFigures"
path: "/technologyFigures",
active: false
},
{
title: "美国主要创新主体动向",
icon: Menu11,
path: "/innovationSubject"
path: "/innovationSubject",
active: false
},
{
title: "美国科研资助体系",
icon: Menu12,
path: "/scientificFunding"
path: "/scientificFunding",
active: false
}
]);
......@@ -235,6 +246,7 @@ const handleToModule = (item, index) => {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
if (index === 1) {
homeActiveTitleIndex.value = index
item.active = true
router.push({
path: item.path
})
......@@ -497,6 +509,11 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.active-title {
color: var(--color-main-active) !important;
font-size: 20px !important;
}
}
}
}
......@@ -528,6 +545,8 @@ onUnmounted(() => {
display: flex;
cursor: pointer;
&:hover {
.title {
color: var(--color-main-active);
......@@ -557,6 +576,11 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.active-title {
color: var(--color-main-active) !important;
font-size: 20px !important;
}
}
}
}
......
<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,12 +295,15 @@ 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)
})
......
const getQuarterRange = (quatarNum) => {
const quarters = {
1: ['2025-01-01', '2025-03-31'],
2: ['2025-04-01', '2025-06-30'],
3: ['2025-07-01', '2025-09-30'],
4: ['2025-10-01', '2025-12-31']
};
return quarters[quatarNum];
}
export default getQuarterRange
\ No newline at end of file
// 绘制echarts图表
import getMonthRange from './getMonthRange'
import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts'
import 'echarts-wordcloud';
import router from '@/router/index'
......@@ -77,6 +78,26 @@ const setChart = (option, chartId, allowClick, selectParam) => {
}
}
break
case '政令':
if (params.componentType === 'series' && params.seriesType === 'pie') {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
}
}
......
<svg viewBox="0 0 12 13" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.000000" fill="none" customFrame="#000000">
<path id="矢量 1651" d="M8.05031 2.15239e-07C8.33835 2.69049e-07 8.61901 0.0965686 8.85205 0.275865C9.08508 0.455161 9.25853 0.70798 9.34755 0.998087C9.43656 1.28819 9.43656 1.6007 9.34755 1.8908C9.25853 2.18091 9.08508 2.43373 8.85205 2.61303C8.61901 2.79232 8.33835 2.88889 8.05031 2.88889L3.9583 2.88889C3.69558 2.88873 3.43848 2.80821 3.21793 2.65703C2.99737 2.50585 2.82274 2.29043 2.71502 2.03667C2.35984 2.08328 2.0252 2.23849 1.752 2.48332C1.4788 2.72814 1.27889 3.05197 1.17673 3.41519C1.07456 3.77842 1.07456 4.1653 1.17673 4.52853C1.27889 4.89176 1.4788 5.21558 1.752 5.46041C2.0252 5.70523 2.35984 5.86044 2.71502 5.90706C2.82284 5.65343 2.99751 5.43816 3.21806 5.28711C3.43861 5.13606 3.69565 5.05566 3.9583 5.05556L8.05031 5.05556C8.31755 5.05563 8.57888 5.13883 8.80185 5.29483C9.02483 5.45084 9.19961 5.67276 9.3045 5.93306C9.88944 5.98317 10.4467 6.21802 10.9042 6.60722C11.3618 6.99642 11.6985 7.52206 11.8708 8.11613C12.0431 8.7102 12.0431 9.34536 11.8708 9.93943C11.6985 10.5335 11.3618 11.0591 10.9042 11.4483C10.4467 11.8375 9.88944 12.0724 9.3045 12.1225C9.19961 12.3828 9.02483 12.6047 8.80185 12.7607C8.57888 12.9167 8.31755 12.9999 8.05031 13L3.9583 13C3.71881 13.0001 3.48352 12.9334 3.27608 12.8067C3.06865 12.6799 2.89638 12.4976 2.77661 12.2779C2.65684 12.0583 2.59378 11.8092 2.59378 11.5556C2.59378 11.3019 2.65684 11.0528 2.77661 10.8332C2.89638 10.6136 3.06865 10.4312 3.27608 10.3044C3.48352 10.1777 3.71881 10.111 3.9583 10.1111L8.05031 10.1111C8.60409 10.1111 9.07944 10.4607 9.29291 10.9626C9.64884 10.9171 9.98445 10.7624 10.2585 10.5177C10.5326 10.273 10.7333 9.94881 10.8358 9.58501C10.9384 9.22121 10.9384 8.83362 10.8358 8.46982C10.7333 8.10603 10.5326 7.78186 10.2585 7.53714C9.98445 7.29241 9.64884 7.13778 9.29291 7.09222C9.18531 7.34593 9.0108 7.56132 8.79036 7.71251C8.56992 7.86369 8.31294 7.94423 8.05031 7.94444L3.9583 7.94444C3.69087 7.94445 3.42933 7.86121 3.20622 7.70506C2.9831 7.54892 2.80826 7.32676 2.70342 7.06622C2.11732 7.01784 1.55849 6.78394 1.09952 6.39491C0.640554 6.00587 0.302655 5.47968 0.129709 4.88468C-0.0432364 4.28967 -0.0432363 3.65334 0.129709 3.05833C0.302654 2.46332 0.640554 1.93713 1.09952 1.54809C1.55849 1.15906 2.11732 0.925163 2.70342 0.876778C2.80847 0.616508 2.9834 0.394654 3.2065 0.238777C3.4296 0.0829001 3.69102 -0.000130273 3.9583 2.15239e-07L8.05031 2.15239e-07Z" fill="rgb(95,101,108)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 12 13.3145" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.314453" fill="none" customFrame="#000000">
<path id="矢量 52" d="M2.47123 9.11862L8.566 3.02386C8.82631 2.76355 8.82631 2.3415 8.566 2.08119C8.30569 1.82088 7.88364 1.82088 7.62333 2.08119L1.52857 8.17595C1.40356 8.30096 1.33333 8.4705 1.33333 8.64729C1.33333 9.01542 1.63176 9.31385 1.9999 9.31385C2.17668 9.31385 2.34623 9.24363 2.47123 9.11862ZM2.82867 10.6472L1 10.6472C0.447715 10.6472 0 10.1995 0 9.64719L0 8.23273C0 7.96752 0.105357 7.71316 0.292894 7.52563L7.62333 0.195193C7.74833 0.0702133 7.91787 0 8.09467 0C8.27147 0 8.441 0.0702133 8.566 0.195193L10.452 2.08119C10.577 2.20621 10.6472 2.37575 10.6472 2.55253C10.6472 2.7293 10.577 2.89884 10.452 3.02386L2.82867 10.6472L2.82867 10.6472ZM0.666666 11.9805L11.3333 11.9805C11.7015 11.9805 12 12.279 12 12.6472C12 13.0154 11.7015 13.3139 11.3333 13.3139L0.666667 13.3139C0.298477 13.3139 0 13.0154 0 12.6472C0 12.279 0.298477 11.9805 0.666666 11.9805Z" fill="rgb(255,255,255)" fill-rule="evenodd" />
</svg>
......@@ -38,13 +38,9 @@
</div>
</template>
<template v-else>
<div
class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }"
v-for="item in tabs"
:key="item.path"
@click="emit('tab-click', item)"
>
<div class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:key="item.path" @click="emit('tab-click', item)">
<div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" />
......@@ -88,7 +84,13 @@
</div>
</template>
<template v-else>
<div class="btn3" @click="emit('open-analysis')">
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
</div>
<div class="text">{{ "进展预测" }}</div>
</div>
<div class="btn3" @click="emit('open-analysis', 'analysis')">
<div class="icon">
<img :src="btnIconAnalysis" alt="" />
</div>
......@@ -103,7 +105,8 @@
<script setup>
import { computed } from "vue";
import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png";
import btnIconAnalysis from "@/views/bill/billLayout/assets/icons/writting-icon.svg";
import btnIconForsee from "@/views/bill/billLayout/assets/icons/forsee-icon.svg";
const props = defineProps({
billInfo: {
......@@ -347,6 +350,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
justify-content: flex-end;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.btn2 {
cursor: pointer;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--bg-white-100);
border: 1px solid var(--bg-black-10);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn3 {
cursor: pointer;
width: 120px;
......@@ -358,16 +384,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
align-items: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(255, 255, 255, 1);
......@@ -387,4 +403,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
}
}
</style>
......@@ -2,16 +2,9 @@
<div class="layout-container">
<!-- 导航菜单 -->
<div class="layout-main">
<BillHeader
:billInfo="billInfoGlobal"
:defaultLogo="USALogo"
:tabs="mainHeaderBtnList"
:activeTitle="activeTitle"
:showTabs="showHeaderTabs"
:showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick"
/>
<BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
<div class="layout-main-center">
<router-view />
......@@ -115,12 +108,24 @@ const handleClickMainHeaderBtn = item => {
});
};
const handleAnalysisClick = () => {
const handleAnalysisClick = analysisType => {
const billId = route.query.billId;
if (!billId) return;
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
path: `/billLayout/ProgressForecast/${billId}`,
});
return;
}
// 分析报告 -> 写作助手
router.push({
path: "/writtingAsstaint",
query: {
topic: "法案",
fileId: route.query.billId
fileId: String(billId)
}
});
};
......@@ -149,11 +154,13 @@ watch(
// height: 1016px;
background: rgba(249, 250, 252, 1);
position: relative;
// margin: 0 auto;
.layout-main {
width: 100%;
height: calc(100vh - 72px);
height: 100vh;
overflow-y: auto;
.layout-main-center {
// height: calc(100% - 137px);
width: 1600px;
......
......@@ -151,7 +151,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'
import ChartContainer from '../../components/ChartContainer/index.vue'
import ChartHeader from '../../components/ChartHeader/index.vue'
import ActiveTag from '../../components/ActiveTag/index.vue'
......@@ -171,6 +171,10 @@ import getDateRange from '@/utils/getDateRange'
const route = useRoute();
const timer1 = ref(null)
const timer2 = ref(null)
const timer3 = ref(null)
// 图表/数据
const isShowChart = ref(false)
// 点击切换数据/图表
......@@ -180,7 +184,7 @@ const handleSwitchChartData = () => {
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
timer1.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
......@@ -267,7 +271,7 @@ const handleClickDemensionItem = (val) => {
})
val.active = true
curDemension.value = val.name
setTimeout(() => {
timer2.value = setTimeout(() => {
activeChart.value = val.chartTypeList[0]
curChartData.value = val.data
})
......@@ -702,8 +706,8 @@ const statusList = ref([
id: '参议院通过'
},
{
name: '双院通过',
id: '双院通过'
name: '分歧已解决',
id: '分歧已解决'
},
])
......@@ -778,8 +782,8 @@ const selectedCount = computed(() => selectedMap.value.size)
// 获取表格数据(示例)
const fetchTableData = async () => {
isSelectedAll.value = false
selectedMap.value.clear()
// isSelectedAll.value = false
// selectedMap.value.clear()
// 调用接口获取数据...
const params = {
page: currentPage.value,
......@@ -793,8 +797,8 @@ const fetchTableData = async () => {
originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value,
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0,
isInvolveCn: isInvolveCn ? 'Y' : 'N',
status: selectedStatus.value === '全部阶段' ? null : selectedStatus.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -836,7 +840,7 @@ const fetchTableData = async () => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
timer3.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
......@@ -877,7 +881,7 @@ const fetchAllData = async () => {
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0,
isInvolveCn: isInvolveCn ? 'Y' : 'N',
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -1116,14 +1120,21 @@ const handleExport = () => {
onMounted(async () => {
handleGetOrgList()
handleGetMemberList()
initParam()
// 初始化
await fetchTableData()
})
onBeforeUnmount(() => {
if (timer1.value) {
clearTimeout(timer1.value)
}
if (timer2.value) {
clearTimeout(timer2.value)
}
if (timer3.value) {
clearTimeout(timer3.value)
}
})
</script>
......@@ -1239,10 +1250,11 @@ onMounted(async () => {
.data-main-box {
width: 1568px;
height: 810px;
min-height: 810px;
border-radius: 10px;
background: var(--bg-white-100);
margin: 0 auto;
margin-bottom: 20px;
overflow: hidden;
.data-main-box-header {
......@@ -1274,8 +1286,7 @@ onMounted(async () => {
.data-main-box-main {
width: 1520px;
// height: 633px;
height: 680px;
min-height: 680px;
border-radius: 10px;
border: 1px solid var(--bg-black-5);
margin: 0 auto;
......
......@@ -17,7 +17,7 @@
<slot name="chart-box"></slot>
</div>
<div class="tip-box">
<TipTab />
<TipTab text="数据来源:美国国会官网" />
</div>
</div>
</div>
......@@ -176,8 +176,7 @@ const chartItemList = computed(() => {
.tip-box {
height: 54px;
box-sizing: border-box;
padding-top: 10px;
// background: orange;
padding: 15px 600px;
}
}
}
......
......@@ -2,7 +2,7 @@
<div class="select-wrapper" :class="{ 'select-wrapper-custom': selectValue === '自定义' }">
<div class="select-left text-tip-1">{{ selectTitle + ':' }}</div>
<div class="select-right" :class="{ 'select-right-custom': selectValue === '自定义' }">
<el-select v-model="selectValue" :placeholder="placeholderName" style="width: 240px">
<el-select v-model="selectValue" :placeholder="placeholderName" filterable style="width: 240px">
<!-- <el-option label="全部领域" value="全部领域" /> -->
<el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
......
......@@ -128,7 +128,7 @@
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455">
<el-table-column label="政令名称" width="720">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
}}</span>
......@@ -137,15 +137,23 @@
<el-table-column label="发布时间" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="提案人" width="480">
<el-table-column label="发布机构" width="180">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.sponsorPersonName
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
}}</span>
</template>
</el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" />
<el-table-column property="originDepart" label="提出委员会" width="180" />
<el-table-column property="status" label="所处阶段" width="120" />
<el-table-column label="涉及领域" width="350" class-name="date-column">
<template #default="scope">
<div class="tag-box">
<AreaTag v-for="tag, index in scope.row.domains" :key="index" :tagName="tag" />
</div>
</template>
</el-table-column>
<el-table-column label="政令类型" width="100">
<template #default="scope">{{ scope.row.typeStr }}</template>
</el-table-column>
</el-table>
</div>
</div>
......@@ -175,6 +183,8 @@ import { search } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
import { getDepartmentList } from "@/api/decree/home";
const route = useRoute();
// 图表/数据
......@@ -363,7 +373,7 @@ const activeTagList = computed(() => {
}
if (isInvolveTechnology.value) {
const involveStr = '政令相关'
const involveStr = '科技相关'
arr.push(
{
tag: '科技相关',
......@@ -593,18 +603,6 @@ const insList = ref([
name: '全部机构',
id: '全部机构'
},
{
name: '机构1',
id: '机构1'
},
{
name: '机构2',
id: '机构2'
},
{
name: '其他',
id: '其他'
},
])
const selectedIns = ref('全部机构')
const insPlaceHolder = ref('请选择发布机构')
......@@ -612,6 +610,30 @@ const handleSelectIns = value => {
selectedIns.value = value
}
const handleGetDeparmentList = async () => {
try {
// let { keyWord, pageNum, pageSize, day } = organizationInfo
const params = {
day: 365,
}
const res = await getDepartmentList(params);
console.log("机构列表", res);
if (res.code === 200) {
let arr = res.data.orgList.map(item => {
return {
name: item.orgName,
id: item.orgName
}
})
insList.value = [...insList.value, ...arr]
}
} catch (error) {
}
}
// 政令类型列表
const decreeTypeList = ref([
{
......@@ -642,6 +664,7 @@ const isInvolveTechnology = ref(false)
const handleClear = () => {
selectedArea.value = '全部领域'
selectedDate.value = ''
customTime.value = []
selectedIns.value = '全部机构'
selectedDecreeType.value = '全部类型'
isInvolveCn.value = false
......@@ -701,12 +724,12 @@ const fetchTableData = async () => {
keyword: '',
type: 2, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N',
proposedDateStart: customTime.value[0]?customTime.value[0]:null,
proposedDateEnd: customTime.value[1]?customTime.value[1]:null,
organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -731,7 +754,7 @@ const fetchTableData = async () => {
name: key,
value: Number(value)
}))
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsOriginChamber).map(([key, value]) => ({
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsorganizationName).map(([key, value]) => ({
name: key,
value: Number(value)
}))
......@@ -773,11 +796,11 @@ const fetchAllData = async () => {
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1
}
try {
const res = await search(params)
......@@ -933,6 +956,9 @@ const initParam = () => {
selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
}
selectedIns.value = route.query.orgnizationName? route.query.orgnizationName : '全部机构'
isInvolveCn.value = route.query.isInvolveCn ? true : false
isInvolveTechnology.value = route.query.isInvolveTechnology ? true : false
......@@ -946,7 +972,7 @@ const initParam = () => {
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('decreeRouteQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
if (savedQuery.selectedDate && Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
}
......@@ -959,16 +985,16 @@ const initParam = () => {
}
// 跳转法案详情
const handleClickToDetail = (curBill) => {
console.log('curBill', curBill);
// 跳转政令详情
const handleClickToDetail = (curDecree) => {
console.log('curDecree', curDecree);
window.sessionStorage.setItem("billId", curBill.id);
window.sessionStorage.setItem("curTabName", curBill.title);
window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("curTabName", curDecree.title);
const route = router.resolve({
path: "/billLayout",
path: "/decreeLayout",
query: {
billId: curBill.id
billId: curDecree.id
}
});
window.open(route.href, "_blank");
......@@ -989,13 +1015,16 @@ const handlePerClick = item => {
// 导出
const handleExport = () => {
if(!selectedCount.value) {
ElMessage.warning('请选择至少一项数据!')
return
}
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
......@@ -1005,7 +1034,7 @@ const handleExport = () => {
};
onMounted(async () => {
handleGetDeparmentList()
initParam()
// 初始化
......@@ -1127,10 +1156,11 @@ onMounted(async () => {
.data-main-box {
width: 1568px;
height: 810px;
min-height: 810px;
border-radius: 10px;
background: var(--bg-white-100);
margin: 0 auto;
margin-bottom: 20px;
overflow: hidden;
.data-main-box-header {
......@@ -1163,7 +1193,7 @@ onMounted(async () => {
.data-main-box-main {
width: 1520px;
// height: 633px;
height: 680px;
min-height: 680px;
border-radius: 10px;
border: 1px solid var(--bg-black-5);
margin: 0 auto;
......@@ -1257,6 +1287,14 @@ onMounted(async () => {
.date-column {
background-color: #ecf5ff;
.tag-box {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 340px;
}
}
.date-column .cell {
......@@ -1284,6 +1322,8 @@ onMounted(async () => {
}
}
:deep(.el-table__header-wrapper) {
// background-color: #f5f7fa;
height: 48px;
......
......@@ -72,7 +72,7 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import Icon1 from './assets/icons/sider-icon1.svg'
import Icon2 from './assets/icons/sider-icon2.svg'
import Icon3 from './assets/icons/sider-icon3.svg'
......@@ -398,6 +398,8 @@ const handleClickTab = (tab) => {
})
}
const timer = ref(null)
// 关闭当前标签页
const handleCloseCurTab = (tab, index) => {
......@@ -416,7 +418,7 @@ const handleCloseCurTab = (tab, index) => {
})
if (index === openedTabList.value.length - 1) {
tagsViewStore.delView(tab)
setTimeout(() => {
timer.value = setTimeout(() => {
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].active = true
activeTab = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]
router.push({
......@@ -593,6 +595,11 @@ onMounted(() => {
})
onBeforeUnmount(() => {
if(timer.value) {
clearTimeout(timer.value)
}
})
</script>
......
......@@ -814,7 +814,25 @@ const handleBox5 = async () => {
let chart1 = getBarChart(chart1Data.value.dataX, chart1Data.value.dataY);
chart1.yAxis.name = "数量";
chart1.yAxis.nameTextStyle = { align: 'right' }
setChart(chart1, "chart1");
let org = '全部机构'
if(box5Params.proposeName) {
org = keyOrganizationList.value.filter(item => {
return item.orgId === box5Params.proposeName
})[0].orgName
}
let domain = '全部领域'
if(box5Params.domainId) {
domain = areaList.value.filter(item => {
return item.id === box5Params.domainId
})[0].name
}
const selectParam = {
moduleType: '政令',
orgnizationName: org,
domains: domain,
selectDate: box5Params.year
}
setChart(chart1, "chart1", true, selectParam);
};
// 政令科技领域
......@@ -861,8 +879,19 @@ const handleGetDecreeArea = async () => {
};
const handleBox6 = async () => {
await handleGetDecreeArea();
let org = '全部机构'
if(box6Params.proposeName) {
org = keyOrganizationList.value.filter(item => {
return item.orgId === box6Params.proposeName
})[0].orgName
}
const selectParam = {
moduleType: '政令',
orgnizationName: org,
selectedDate: JSON.stringify([box6Params.year+'-01-01', box6Params.year+'-12-31'])
}
let chart2 = getPieChart(chart2Data.value);
setChart(chart2, "chart2");
setChart(chart2, "chart2", true, selectParam);
};
const handleBox6YearChange = () => {
......
......@@ -7,7 +7,7 @@
<router-view />
</div>
</div>
<div class="right-btn" @click="handleClickToolBox">
<div class="right-btn" v-if="isShowToolBox" @click="handleClickToolBox">
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" />
......@@ -22,20 +22,7 @@
</div>
</div>
<div class="tool-box">
<!-- <div class="tool-item">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon2.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon3.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon4.png" alt="" />
</div> -->
<div class="tool-box" v-if="isShowToolBox">
<el-tooltip content="智能写报" placement="left" :offset="10">
<div class="tool-item" @click="handleOpenPage('znxb')">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
......@@ -96,11 +83,13 @@ import { ElMessage } from "element-plus";
const router = useRouter();
const route = useRoute();
const isShowHeader = computed(() => {
const isShow = route.meta.isShowHeader
return isShow? true : false
const isShowToolBox = computed(() => {
const isDataLibrary = route.fullPath.includes("dataLibrary");
const isWrittingAsstaint = route.path === "/writtingAsstaint";
return !isDataLibrary && !isWrittingAsstaint;
})
const isShowHeader = computed(() => !!route.meta.isShowHeader);
const isShowAiBox = ref(false);
......@@ -816,4 +805,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,13 +247,25 @@ 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;
......@@ -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 = '#ffff00;';
mark.style.color = "#000"; // 文字颜色
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);
......
......@@ -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;
......@@ -138,9 +141,10 @@ watch(
cursor: pointer;
vertical-align: middle;
transition: background-color 0.2s;
border-radius: 50%;
background: #818181;
&:hover {
background-color: #044da5;
background-color: #818181;
}
&:active {
......
<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 style="width: 100%;">
<WrittingHeader @onExport="onExport"></WrittingHeader>
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<writtingleftBox ref="leftBoxRef" @generate="handleGenerate" />
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" />
<!-- <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-show="!store.reportContent" class="main-placeholder">
<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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论