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

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

上级 383ef870
...@@ -8,7 +8,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -8,7 +8,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
isShowProcess: false, // 初始隐藏执行步骤面板 isShowProcess: false, // 初始隐藏执行步骤面板
isShowSteps: false, // 步骤侧边栏显隐控制 isShowSteps: false, // 步骤侧边栏显隐控制
isShowClauseTranslation: false, // 翻译侧边栏显隐控制 isShowClauseTranslation: false, // 翻译侧边栏显隐控制
isEditMode: true, isEditMode: false,
writtingTitle: '', writtingTitle: '',
descText: '', descText: '',
reportContent: '', // 报文内容(核心) reportContent: '', // 报文内容(核心)
...@@ -80,19 +80,95 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -80,19 +80,95 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}, },
// ========== 路由参数处理 ========== // ========== 路由参数处理 ==========
setRouteParams(query) { async setRouteParams(query) {
this.routeQuery = { ...query }; this.routeQuery = { ...query };
this._isDisableTemplate = Object.keys(query).length > 0; this._isDisableTemplate = Object.keys(query).length > 0;
if (Object.keys(query).length > 0) { if (Object.keys(query).length > 0) {
const { topic, fileId } = query; const { topic, fileId } = query;
if (topic && fileId) { if (topic) {
this.curTempTitle = topic; this.curTempTitle = topic;
this.tempActiveIndex = this.tempList.findIndex(item => item.title === 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) { handleExceed(files, uploadRef) {
if (uploadRef) { if (uploadRef) {
...@@ -293,7 +369,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -293,7 +369,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏 this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束'); ElMessage.success('报文生成结束');
} else if ((msgData.event_type || '').toLowerCase().includes('error')) { } else if ((msgData.event_type || '').toLowerCase().includes('error')) {
ElMessage.error('生成失败:' + str); // 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败';
ElMessage.error('生成失败:' + errorMsg);
this.resetGenerateState(); this.resetGenerateState();
} else { } else {
// 老版 --index.vue 行为:步骤栏直接追加服务端发来的完整步骤内容,不加时间戳、不强行换行 // 老版 --index.vue 行为:步骤栏直接追加服务端发来的完整步骤内容,不加时间戳、不强行换行
...@@ -320,14 +398,30 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -320,14 +398,30 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== 业务入口 ========== // ========== 业务入口 ==========
async generateReport() { async generateReport() {
// 路由参数优先 // 路由参数优先
this.isGenerating = true this.isGenerating = true;
this.isShowProcess = true this.isShowProcess = true;
if (Object.keys(this.routeQuery).length !== 0) { 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 = { const params = {
query: this.writtingTitle, query: this.writtingTitle,
desc: this.descText, desc: this.descText,
topic: this.curTempTitle, topic: this.curTempTitle,
report_id: this.routeQuery.fileId report_id: fileId
}; };
await this.fetchReportData(params); await this.fetchReportData(params);
} else { } else {
......
<template> <template>
<div class="left-box-wrapper"> <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 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="left-box-input">
<!-- 表单输入区域 --> <!-- 表单输入区域 -->
<div class="sider" v-if="!store.isGenerating"> <div class="sider" v-if="!store.isGenerating">
...@@ -96,17 +96,28 @@ ...@@ -96,17 +96,28 @@
</div> </div>
<!-- 生成按钮 --> <!-- 生成按钮 -->
<div class="submit-btn" @click="triggerGenerate"> <div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<div class="submit-icon"> <div class="submit-icon">
<img src="../assets/images/ai.png" alt="" /> <img src="../assets/images/ai.png" alt="" />
</div> </div>
<div class="submit-text">生成报文</div> <div class="submit-text">生成报文</div>
</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> </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="left-box-input">
<div class="process-box"> <div class="process-box">
<div class="process-main-box"> <div class="process-main-box">
...@@ -148,35 +159,29 @@ ...@@ -148,35 +159,29 @@
</div> </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-main-box">
<div class="translation-header-new"> <div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div> <div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right"> <div class="header-right">
<el-switch <el-switch v-model="store.isShowOriginal" inline-prompt active-text="" inactive-text="" />
v-model="store.isShowOriginal"
inline-prompt
active-text=""
inactive-text=""
/>
<span class="switch-label">显示原文</span> <span class="switch-label">显示原文</span>
</div> </div>
</div> </div>
<div class="translation-content"> <div class="translation-content" ref="translationContentRef">
<div <div class="translation-item"
class="translation-item" :class="{ active: store.highlightClauseId === item.payload?.clause_number }"
:class="{ active: store.highlightClauseId === item.payload?.clause_number }" :data-clause-number="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="item-header">
<div class="index-badge">{{ item.payload?.clause_number }}</div> <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>
<div class="item-body"> <div class="item-body">
<div class="translated-text">{{ item.payload?.clause_content_zh }}</div> <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> </div>
</div> </div>
...@@ -190,7 +195,7 @@ ...@@ -190,7 +195,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from "vue"; import { nextTick, ref, watch } from "vue";
import { ElButton, ElIcon, ElInput, ElUpload, ElSwitch } from "element-plus"; import { ElButton, ElIcon, ElInput, ElUpload, ElSwitch } from "element-plus";
import { Upload } from "@element-plus/icons-vue"; import { Upload } from "@element-plus/icons-vue";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore"; import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
...@@ -201,6 +206,7 @@ const store = useWrittingAsstaintStore(); ...@@ -201,6 +206,7 @@ const store = useWrittingAsstaintStore();
// 组件内部引用 // 组件内部引用
const uploadRef = ref(null); const uploadRef = ref(null);
const processContainerRef = ref(null); const processContainerRef = ref(null);
const translationContentRef = ref(null);
const emit = defineEmits(["generate"]); const emit = defineEmits(["generate"]);
const triggerGenerate = () => { const triggerGenerate = () => {
...@@ -225,13 +231,24 @@ const { renderedProcess, updateProcess, clearContent } = useStream(); ...@@ -225,13 +231,24 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
// 监听条款高亮并滚动 // 监听条款高亮并滚动
watch( watch(
() => store.highlightClauseId, () => store.highlightClauseId,
(newId) => { async (newId) => {
if (newId) { if (!newId || !translationContentRef.value) return;
const item = document.querySelector(`.translation-item[data-clause-number="${newId}"]`); await nextTick();
if (item) {
item.scrollIntoView({ behavior: 'smooth', block: 'center' }); 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({ ...@@ -269,13 +286,13 @@ defineExpose({
position: relative; position: relative;
.left-box.process { .left-box.process {
padding-top: 20px; padding-top: 40px;
} }
.left-box { .left-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 520px; width: 521px;
height: 100%; height: 100%;
padding-top: 22px; padding-top: 22px;
padding-bottom: 29px; padding-bottom: 29px;
...@@ -285,26 +302,38 @@ defineExpose({ ...@@ -285,26 +302,38 @@ defineExpose({
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: relative; position: relative;
&.has-back-btn {
padding-top: 40px !important;
}
.left-box-input { .left-box-input {
overflow-y: scroll; flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-width: none; scrollbar-width: none;
padding-inline: 22px; padding-inline: 22px;
margin-top: 20px;
.process-box { .process-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
height: 100%; // 确保占满
.process-main-box { .process-main-box {
width: 476px; width: 476px;
flex: 1;
display: flex;
flex-direction: column;
.steps-box { .steps-box {
flex: 1;
display: flex;
flex-direction: column;
.steps-header { .steps-header {
flex-shrink: 0;
display: flex; display: flex;
.icon { .icon {
...@@ -331,17 +360,16 @@ defineExpose({ ...@@ -331,17 +360,16 @@ defineExpose({
.steps-content { .steps-content {
width: 100%; width: 100%;
min-height: 20px; min-height: 100px;
max-height: 480px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto;
} }
} }
.tool-box { .tool-box {
flex-shrink: 0;
width: 440px; width: 440px;
height: 160px; height: 160px;
margin: 10px auto; margin: 20px auto;
background: #f6f9fe; background: #f6f9fe;
border-radius: 10px; border-radius: 10px;
...@@ -638,11 +666,16 @@ defineExpose({ ...@@ -638,11 +666,16 @@ defineExpose({
.translation-main-box { .translation-main-box {
flex: 1;
display: flex;
flex-direction: column;
width: 520px; width: 520px;
gap: 20px; gap: 20px;
padding-inline: 22px; padding-inline: 22px;
overflow: hidden;
.translation-header-new { .translation-header-new {
flex-shrink: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
...@@ -670,11 +703,12 @@ defineExpose({ ...@@ -670,11 +703,12 @@ defineExpose({
} }
.translation-content { .translation-content {
flex: 1;
width: 100%; width: 100%;
max-height: 780px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding-right: 10px; padding-right: 10px;
position: relative;
.translation-item { .translation-item {
margin-bottom: 24px; margin-bottom: 24px;
...@@ -803,6 +837,7 @@ defineExpose({ ...@@ -803,6 +837,7 @@ defineExpose({
.submit-area { .submit-area {
flex-shrink: 0;
margin-top: auto; margin-top: auto;
width: 100%; width: 100%;
min-height: 74px; min-height: 74px;
...@@ -837,7 +872,9 @@ defineExpose({ ...@@ -837,7 +872,9 @@ defineExpose({
.process-footer-box { .process-footer-box {
height: 36px; height: 36px;
display: flex; display: flex;
position: absolute; align-items: center;
justify-content: flex-start;
width: 100%;
.footer-left { .footer-left {
width: 348px; width: 348px;
...@@ -855,7 +892,7 @@ defineExpose({ ...@@ -855,7 +892,7 @@ defineExpose({
.footer-right { .footer-right {
margin-left: 8px; margin-left: 8px;
display: flex; display: flex;
width: 120px; flex: 1; // 自动填充剩余空间
height: 36px; height: 36px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--color-main-active); border: 1px solid var(--color-main-active);
......
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
</div> </div>
<div class="edit-box"></div> <div class="edit-box"></div>
<div class="btn-box"> <div class="btn-box">
<div class="btn" @click="store.exportContent"> <!-- <div class="btn" @click="store.exportContent">
<div class="icon"> <div class="icon">
<img src="./assets/images/export-icon.png" alt="" /> <img src="./assets/images/export-icon.png" alt="" />
</div> </div>
<div class="text">导出</div> <div class="text">导出</div>
</div> </div> -->
<div class="btn" @click="store.toggleEditMode"> <div class="btn" @click="store.toggleEditMode">
<div class="icon"> <div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" /> <img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</div> </div>
<div class="text">{{ store.isEditMode ? "预览" : "编辑" }}</div> <div class="text">{{ store.isEditMode ? "预览" : "编辑" }}</div>
</div> </div>
<div class="btn btn1"> <div class="btn btn1" @click="store.exportContent">
<div class="icon"> <div class="icon">
<img src="./assets/images/save-icon.png" alt="" /> <img src="./assets/images/save-icon.png" alt="" />
</div> </div>
...@@ -85,9 +85,9 @@ const handleGenerate = async () => { ...@@ -85,9 +85,9 @@ const handleGenerate = async () => {
}; };
// 生命周期 // 生命周期
onMounted(() => { onMounted(async () => {
// 组件初始化时:读取路由参数,传递给Pinia // 组件初始化时:读取路由参数,传递给Pinia
store.setRouteParams(route.query); await store.setRouteParams(route.query);
}); });
onUnmounted(() => { onUnmounted(() => {
......
...@@ -44,7 +44,13 @@ export default defineConfig({ ...@@ -44,7 +44,13 @@ export default defineConfig({
port: 3000, port: 3000,
open: true, open: true,
proxy: { proxy: {
'/reportData': {
target: 'http://8.140.26.4:10022/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/reportData/, '')
},
'/api': { '/api': {
target: 'http://8.140.26.4:9085/', target: 'http://8.140.26.4:9085/',
changeOrigin: true, changeOrigin: true,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论