Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
d9959d1e
提交
d9959d1e
authored
2月 09, 2026
作者:
张伊明
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
优化写报界面样式,pinia抽取公共逻辑封装函数,优化状态管理
上级
383ef870
显示空白字符变更
内嵌
并排
正在显示
4 个修改的文件
包含
189 行增加
和
52 行删除
+189
-52
writtingAsstaintStore.js
src/stores/writtingAsstaintStore.js
+101
-7
WrittingLeftBox.vue
src/views/writtingAsstaint/components/WrittingLeftBox.vue
+77
-40
index.vue
src/views/writtingAsstaint/index.vue
+5
-5
vite.config.js
vite.config.js
+6
-0
没有找到文件。
src/stores/writtingAsstaintStore.js
浏览文件 @
d9959d1e
...
...
@@ -8,7 +8,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
isShowProcess
:
false
,
// 初始隐藏执行步骤面板
isShowSteps
:
false
,
// 步骤侧边栏显隐控制
isShowClauseTranslation
:
false
,
// 翻译侧边栏显隐控制
isEditMode
:
tru
e
,
isEditMode
:
fals
e
,
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
(
/
[^\u
4e00-
\u
9fa5
]
/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
{
...
...
src/views/writtingAsstaint/components/WrittingLeftBox.vue
浏览文件 @
d9959d1e
<
template
>
<div
class=
"left-box-wrapper"
>
<div
class=
"back"
@
click=
"store.resetGenerateState"
v-if=
"store.isGenerating"
>
<
返回
</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
:
2
0px
;
padding-top
:
4
0px
;
}
.left-box
{
display
:
flex
;
flex-direction
:
column
;
width
:
52
0
px
;
width
:
52
1
px
;
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
:
1
0px
auto
;
margin
:
2
0px
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
);
...
...
src/views/writtingAsstaint/index.vue
浏览文件 @
d9959d1e
...
...
@@ -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
(()
=>
{
...
...
vite.config.js
浏览文件 @
d9959d1e
...
...
@@ -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
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论