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

优化写报界面样式,pinia抽取公共逻辑封装函数,优化状态管理

上级 383ef870
......@@ -8,7 +8,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
isShowProcess: false, // 初始隐藏执行步骤面板
isShowSteps: false, // 步骤侧边栏显隐控制
isShowClauseTranslation: false, // 翻译侧边栏显隐控制
isEditMode: true,
isEditMode: false,
writtingTitle: '',
descText: '',
reportContent: '', // 报文内容(核心)
......@@ -80,19 +80,95 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
},
// ========== 路由参数处理 ==========
setRouteParams(query) {
async setRouteParams(query) {
this.routeQuery = { ...query };
this._isDisableTemplate = Object.keys(query).length > 0;
if (Object.keys(query).length > 0) {
const { topic, fileId } = query;
if (topic && fileId) {
if (topic) {
this.curTempTitle = topic;
this.tempActiveIndex = this.tempList.findIndex(item => item.title === topic);
}
// 如果携带 fileId:外部跳转仅回填/锁定参数,不自动生成
if (fileId) {
this.curTempTitle = topic || '政令';
this.tempActiveIndex = this.tempList.findIndex(item => item.title === this.curTempTitle);
// 初始标题设为空,待点击生成后从接口获取并清洗
this.writtingTitle = '';
}
}
},
// ========== 内部工具函数 (抽取的公共逻辑) ==========
_cleanTitle(title) {
return (title || '').replace(/[^\u4e00-\u9fa5]/g, '');
},
async _fetchBusinessData({ url, payload, typeName }) {
this.isGenerating = true;
this.isShowProcess = true;
this.processLog = `${this.formattedTime}:正在获取${typeName}数据...\r\n`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === 'success' && data.result) {
this.processLog += `${this.formattedTime}:${typeName}数据获取成功,开始生成写报...\r\n`;
// 清洗标题
this.writtingTitle = this._cleanTitle(data.result['报告标题']);
// 触发 AI 生成
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: data.result
});
} else {
throw new Error(data.error_log?.join(', ') || `获取${typeName}数据失败`);
}
} catch (error) {
console.error(`获取${typeName}数据异常:`, error);
ElMessage.error(`获取${typeName}数据失败: ${error.message}`);
this.resetGenerateState();
}
},
// ========== 获取业务数据并触发生成 (业务封装) ==========
async fetchOrderReportData(orderId) {
await this._fetchBusinessData({
url: '/reportData/get-order-report-data',
payload: { order_id: Number(orderId) },
typeName: '政令'
});
},
async fetchBillReportData(billId) {
await this._fetchBusinessData({
url: '/reportData/get-bill-report-data',
payload: { bill_id: billId },
typeName: '法案'
});
},
async fetchListReportData(sancDate) {
await this._fetchBusinessData({
url: '/reportData/get-sanc-list-report-data',
payload: { sanc_date: sancDate },
typeName: '实体清单'
});
},
// ========== 文件上传相关 ==========
handleExceed(files, uploadRef) {
if (uploadRef) {
......@@ -293,7 +369,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
} else if ((msgData.event_type || '').toLowerCase().includes('error')) {
ElMessage.error('生成失败:' + str);
// 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败';
ElMessage.error('生成失败:' + errorMsg);
this.resetGenerateState();
} else {
// 老版 --index.vue 行为:步骤栏直接追加服务端发来的完整步骤内容,不加时间戳、不强行换行
......@@ -320,14 +398,30 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== 业务入口 ==========
async generateReport() {
// 路由参数优先
this.isGenerating = true
this.isShowProcess = true
this.isGenerating = true;
this.isShowProcess = true;
if (Object.keys(this.routeQuery).length !== 0) {
const { fileId, topic } = this.routeQuery;
// 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成
if (fileId) {
if (this.curTempTitle === '法案') {
await this.fetchBillReportData(fileId);
} else if (this.curTempTitle === '清单') {
await this.fetchListReportData(fileId);
} else {
await this.fetchOrderReportData(fileId);
}
return;
}
// 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口
const params = {
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
report_id: this.routeQuery.fileId
report_id: fileId
};
await this.fetchReportData(params);
} else {
......
<template>
<div class="left-box-wrapper">
<div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div>
<div class="left-box process" v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box" :class="{ 'has-back-btn': store.isGenerating }" v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box-input">
<!-- 表单输入区域 -->
<div class="sider" v-if="!store.isGenerating">
......@@ -96,17 +96,28 @@
</div>
<!-- 生成按钮 -->
<div class="submit-btn" @click="triggerGenerate">
<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 class="process-footer-box" v-else>
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
<div class="footer-right" @click="store.resetGenerateState">
<div class="icon"></div>
<div class="text">停止</div>
</div>
</div>
</div>
</div>
<!-- 步骤侧边栏(拆分出来) -->
<div class="left-box process" 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">
......@@ -148,35 +159,29 @@
</div>
<!-- 条款翻译侧边栏 -->
<div class="left-box translation-box" v-if="store.isShowClauseTranslation">
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowClauseTranslation">
<div class="translation-main-box">
<div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right">
<el-switch
v-model="store.isShowOriginal"
inline-prompt
active-text=""
inactive-text=""
/>
<el-switch v-model="store.isShowOriginal" inline-prompt active-text="" inactive-text="" />
<span class="switch-label">显示原文</span>
</div>
</div>
<div class="translation-content">
<div
class="translation-item"
<div class="translation-content" ref="translationContentRef">
<div class="translation-item"
:class="{ active: store.highlightClauseId === item.payload?.clause_number }"
:data-clause-number="item.payload?.clause_number"
v-for="(item, index) in store.clauseTranslationMessages"
:key="index"
>
v-for="(item, index) in store.clauseTranslationMessages" :key="index">
<div class="item-header">
<div class="index-badge">{{ item.payload?.clause_number }}</div>
<div class="clause-title">第{{ getChineseNumber(item.payload?.clause_number) }}节 .{{ item.payload?.clause_section }}</div>
<div class="clause-title">第{{ getChineseNumber(item.payload?.clause_number) }}节 .{{
item.payload?.clause_section }}</div>
</div>
<div class="item-body">
<div class="translated-text">{{ item.payload?.clause_content_zh }}</div>
<div class="original-text" v-if="store.isShowOriginal">{{ item.payload?.clause_content }}</div>
<div class="original-text" v-if="store.isShowOriginal">{{ item.payload?.clause_content }}
</div>
</div>
</div>
</div>
......@@ -190,7 +195,7 @@
</template>
<script setup>
import { ref, watch } from "vue";
import { nextTick, ref, watch } from "vue";
import { ElButton, ElIcon, ElInput, ElUpload, ElSwitch } from "element-plus";
import { Upload } from "@element-plus/icons-vue";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
......@@ -201,6 +206,7 @@ const store = useWrittingAsstaintStore();
// 组件内部引用
const uploadRef = ref(null);
const processContainerRef = ref(null);
const translationContentRef = ref(null);
const emit = defineEmits(["generate"]);
const triggerGenerate = () => {
......@@ -225,13 +231,24 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
// 监听条款高亮并滚动
watch(
() => store.highlightClauseId,
(newId) => {
if (newId) {
const item = document.querySelector(`.translation-item[data-clause-number="${newId}"]`);
if (item) {
item.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
async (newId) => {
if (!newId || !translationContentRef.value) return;
await nextTick();
const container = translationContentRef.value;
const item = container.querySelector(`.translation-item[data-clause-number="${newId}"]`);
if (!item) return;
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),
behavior: 'smooth'
});
}
);
......@@ -269,13 +286,13 @@ defineExpose({
position: relative;
.left-box.process {
padding-top: 20px;
padding-top: 40px;
}
.left-box {
display: flex;
flex-direction: column;
width: 520px;
width: 521px;
height: 100%;
padding-top: 22px;
padding-bottom: 29px;
......@@ -285,26 +302,38 @@ defineExpose({
background: rgba(255, 255, 255, 1);
position: relative;
&.has-back-btn {
padding-top: 40px !important;
}
.left-box-input {
overflow-y: scroll;
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
padding-inline: 22px;
margin-top: 20px;
.process-box {
display: flex;
flex-direction: column;
gap: 20px;
height: 100%; // 确保占满
.process-main-box {
width: 476px;
flex: 1;
display: flex;
flex-direction: column;
.steps-box {
flex: 1;
display: flex;
flex-direction: column;
.steps-header {
flex-shrink: 0;
display: flex;
.icon {
......@@ -331,17 +360,16 @@ defineExpose({
.steps-content {
width: 100%;
min-height: 20px;
max-height: 480px;
min-height: 100px;
overflow-x: hidden;
overflow-y: auto;
}
}
.tool-box {
flex-shrink: 0;
width: 440px;
height: 160px;
margin: 10px auto;
margin: 20px auto;
background: #f6f9fe;
border-radius: 10px;
......@@ -638,11 +666,16 @@ defineExpose({
.translation-main-box {
flex: 1;
display: flex;
flex-direction: column;
width: 520px;
gap: 20px;
padding-inline: 22px;
overflow: hidden;
.translation-header-new {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
......@@ -670,11 +703,12 @@ defineExpose({
}
.translation-content {
flex: 1;
width: 100%;
max-height: 780px;
overflow-x: hidden;
overflow-y: auto;
padding-right: 10px;
position: relative;
.translation-item {
margin-bottom: 24px;
......@@ -803,6 +837,7 @@ defineExpose({
.submit-area {
flex-shrink: 0;
margin-top: auto;
width: 100%;
min-height: 74px;
......@@ -837,7 +872,9 @@ defineExpose({
.process-footer-box {
height: 36px;
display: flex;
position: absolute;
align-items: center;
justify-content: flex-start;
width: 100%;
.footer-left {
width: 348px;
......@@ -855,7 +892,7 @@ defineExpose({
.footer-right {
margin-left: 8px;
display: flex;
width: 120px;
flex: 1; // 自动填充剩余空间
height: 36px;
box-sizing: border-box;
border: 1px solid var(--color-main-active);
......
......@@ -10,12 +10,12 @@
</div>
<div class="edit-box"></div>
<div class="btn-box">
<div class="btn" @click="store.exportContent">
<!-- <div class="btn" @click="store.exportContent">
<div class="icon">
<img src="./assets/images/export-icon.png" alt="" />
</div>
<div class="text">导出</div>
</div>
</div> -->
<div class="btn" @click="store.toggleEditMode">
<div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
......@@ -23,7 +23,7 @@
</div>
<div class="text">{{ store.isEditMode ? "预览" : "编辑" }}</div>
</div>
<div class="btn btn1">
<div class="btn btn1" @click="store.exportContent">
<div class="icon">
<img src="./assets/images/save-icon.png" alt="" />
</div>
......@@ -85,9 +85,9 @@ const handleGenerate = async () => {
};
// 生命周期
onMounted(() => {
onMounted(async () => {
// 组件初始化时:读取路由参数,传递给Pinia
store.setRouteParams(route.query);
await store.setRouteParams(route.query);
});
onUnmounted(() => {
......
......@@ -45,6 +45,12 @@ export default defineConfig({
open: true,
proxy: {
'/reportData': {
target: 'http://8.140.26.4:10022/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/reportData/, '')
},
'/api': {
target: 'http://8.140.26.4:9085/',
changeOrigin: true,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论