1
This commit is contained in:
Binary file not shown.
@@ -17,6 +17,7 @@ import (
|
||||
"rag-local/kb/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type contract struct{}
|
||||
@@ -116,6 +117,18 @@ func (c *contract) Detail(ctx context.Context, req *dto.GetContractDetailReq) (*
|
||||
return &dto.GetContractDetailRes{Task: task, Clauses: clauses, Marks: marks}, nil
|
||||
}
|
||||
|
||||
func (c *contract) Annotated(ctx context.Context, req *dto.AnnotatedContractReq) (*dto.AnnotatedContractRes, error) {
|
||||
htmlStr, err := service.AnnotationService.AnnotatedHTML(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 直接写响应体(html),中间件检测到已写入则不包装 JSON
|
||||
r := g.RequestFromCtx(ctx)
|
||||
r.Response.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
r.Response.Write(htmlStr)
|
||||
return &dto.AnnotatedContractRes{}, nil
|
||||
}
|
||||
|
||||
func (c *contract) Delete(ctx context.Context, req *dto.DeleteContractReq) (*dto.DeleteContractRes, error) {
|
||||
if err := service.AnnotationService.Delete(ctx, req.Id); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -41,6 +41,13 @@ type GetContractDetailRes struct {
|
||||
Marks map[int64][]*entity.ContractMark `json:"marks"`
|
||||
}
|
||||
|
||||
type AnnotatedContractReq struct {
|
||||
g.Meta `path:"/annotated" method:"get" tags:"合同标注" summary:"导出标注版合同(HTML,可打印/另存 PDF)"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
|
||||
type AnnotatedContractRes struct{}
|
||||
|
||||
type DeleteContractReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"合同标注" summary:"删除标注任务"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -378,6 +380,79 @@ func (s *annotationService) judgeClause(ctx context.Context, model *OpenAIChatMo
|
||||
return marks, nil
|
||||
}
|
||||
|
||||
// AnnotatedHTML 生成标注版合同 HTML:条款原文 + 内嵌法条标注,可打印/另存 PDF
|
||||
func (s *annotationService) AnnotatedHTML(ctx context.Context, taskId int64) (string, error) {
|
||||
task, err := dao.ContractTask.GetOne(ctx, taskId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if task == nil {
|
||||
return "", gerror.New("任务不存在")
|
||||
}
|
||||
clauses, err := dao.ContractClause.ListByTask(ctx, taskId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
statusText := map[int]string{0: "待处理", 1: "标注中", 2: "完成", 3: "失败"}[task.Status]
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>标注版-` +
|
||||
html.EscapeString(task.Filename) + `</title><style>
|
||||
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;max-width:820px;margin:24px auto;padding:0 16px;color:#333;line-height:1.8}
|
||||
h1{font-size:20px;text-align:center;margin-bottom:4px}
|
||||
.meta{text-align:center;color:#999;font-size:12px;margin-bottom:24px}
|
||||
.clause{border:1px solid #e4e7ed;border-radius:8px;padding:12px 16px;margin-bottom:14px}
|
||||
.clause-title{font-weight:600;font-size:15px;color:#303133}
|
||||
.clause-content{font-size:13px;color:#555;white-space:pre-wrap;margin:8px 0}
|
||||
.mark{border-left:3px solid #909399;background:#f4f4f5;padding:6px 10px;margin-bottom:6px;border-radius:0 4px 4px 0;font-size:12px;color:#606266}
|
||||
.mark.strong{border-left-color:#67c23a;background:#f0f9eb}
|
||||
.mark.good{border-left-color:#409eff;background:#ecf5ff}
|
||||
.mark-law{font-weight:600;color:#303133}
|
||||
.mark-score{float:right;color:#999}
|
||||
.mark-content{color:#606266;margin-top:2px}
|
||||
.mark-reason{color:#409eff;margin-top:2px}
|
||||
.toolbar{position:fixed;top:12px;right:16px;z-index:10}
|
||||
.toolbar button{padding:6px 14px;border:1px solid #409eff;background:#409eff;color:#fff;border-radius:6px;cursor:pointer;font-size:13px}
|
||||
@media print{.toolbar{display:none}}
|
||||
</style></head><body>
|
||||
<div class="toolbar"><button onclick="window.print()">打印 / 另存为 PDF</button></div>
|
||||
<h1>合同法律条款标注</h1>
|
||||
<div class="meta">文件名:` + html.EscapeString(task.Filename) + ` 导出时间:` + time.Now().Format("2006-01-02 15:04") +
|
||||
` 任务状态:` + statusText + `</div>`)
|
||||
for _, cl := range clauses {
|
||||
marks, err := dao.ContractMark.ListByClause(ctx, cl.Id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(`<div class="clause"><div class="clause-title">` + html.EscapeString(cl.Title) + `</div>` +
|
||||
`<div class="clause-content">` + html.EscapeString(cl.Content) + `</div>`)
|
||||
if len(marks) == 0 {
|
||||
sb.WriteString(`<div class="mark">无标注</div>`)
|
||||
}
|
||||
for _, m := range marks {
|
||||
cls := "mark weak"
|
||||
if m.Score >= 8 {
|
||||
cls = "mark strong"
|
||||
} else if m.Score >= 5 {
|
||||
cls = "mark good"
|
||||
}
|
||||
sb.WriteString(`<div class="` + cls + `"><span class="mark-law">《` + html.EscapeString(m.LawTitle) + `》` +
|
||||
html.EscapeString(m.LawItem) + `</span><span class="mark-score">` + formatScore(m.Score) + ` 分</span>` +
|
||||
`<div class="mark-content">` + html.EscapeString(m.Content) + `</div>` +
|
||||
`<div class="mark-reason">` + html.EscapeString(m.Reason) + `</div></div>`)
|
||||
}
|
||||
sb.WriteString(`</div>`)
|
||||
}
|
||||
sb.WriteString(`</body></html>`)
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func formatScore(score float64) string {
|
||||
if score == float64(int(score)) {
|
||||
return strconv.Itoa(int(score))
|
||||
}
|
||||
return strconv.FormatFloat(score, 'f', 1, 64)
|
||||
}
|
||||
|
||||
func (s *annotationService) fail(ctx context.Context, task *entity.ContractTask, msg string) {
|
||||
_ = dao.ContractTask.UpdateStatus(ctx, task.Id, consts.TaskStatusFailed, msg)
|
||||
g.Log().Errorf(ctx, "annotation task %d failed: %s", task.Id, msg)
|
||||
|
||||
@@ -15,3 +15,12 @@ export function getContractDetail(id) {
|
||||
export function deleteContract(id) {
|
||||
return request.post('/contract/delete', { id })
|
||||
}
|
||||
|
||||
// 导出标注版 HTML:走原生 fetch(拦截器会误判 blob 响应),token 手动带上
|
||||
export async function exportAnnotatedContract(id) {
|
||||
const resp = await fetch(`/contract/annotated?id=${id}`, {
|
||||
headers: { Authorization: 'Bearer ' + (localStorage.getItem('token') || '') }
|
||||
})
|
||||
if (!resp.ok) throw new Error('导出失败')
|
||||
return resp.blob()
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
|
||||
<!-- 详情抽屉:条款 + 标注 -->
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="80%" destroy-on-close>
|
||||
<div v-if="detail" class="detail-toolbar">
|
||||
<el-button type="primary" size="small" @click="exportAnnotated">导出标注版(HTML)</el-button>
|
||||
</div>
|
||||
<div v-if="detail" class="detail-body">
|
||||
<div class="clause-panel">
|
||||
<div class="panel-title">合同条款({{ detail.clauses.length }})</div>
|
||||
@@ -92,7 +95,7 @@ import { onMounted, onUnmounted, computed, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { listDatasets } from '../api/dataset.js'
|
||||
import { uploadContract, listContracts, getContractDetail, deleteContract } from '../api/contract.js'
|
||||
import { uploadContract, listContracts, getContractDetail, deleteContract, exportAnnotatedContract } from '../api/contract.js'
|
||||
|
||||
const datasets = ref([])
|
||||
const dsIds = ref([])
|
||||
@@ -183,6 +186,22 @@ async function openDetail(row) {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportAnnotated() {
|
||||
if (!detail.value) return
|
||||
try {
|
||||
const blob = await exportAnnotatedContract(detail.value.task.id)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = '标注版-' + detail.value.task.filename.replace(/\.[^.]+$/, '') + '.html'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('已导出标注版')
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTask(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('删除任务将同时删除条款与标注结果,确认?', '删除任务', { type: 'warning' })
|
||||
@@ -231,10 +250,15 @@ const detailTitle = computed(() => detail.value ? detail.value.task.filename : '
|
||||
font-size: 40px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.detail-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.detail-body {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
.clause-panel {
|
||||
width: 280px;
|
||||
|
||||
Reference in New Issue
Block a user