AI审核改为 列表页+详情页 结构,列表带审核摘要
- 表 oa_ai_review 增加 summary 列(审核结论摘要,纯文本,列表展示用),
已应用到生产库;分析时由结果 Markdown 提炼前160字纯文本写入
- 列表查询清空大字段 result_md 减小响应体,详情接口仍返回完整结果
- 前端拆分:
· index.vue 重写为列表页:搜索(类型/关键字)+表格(类型/文件名/岗位/结论标签/
审核摘要/时间)+分页,「新增审核」改为弹窗上传(类型/岗位/文件),
审核完成后跳转详情;行可删除
· 新增 detail.vue 详情页:元信息(文件名+下载原件/岗位/模型/时间/审核人)
+ 结论标签 + 完整 Markdown 结果,返回列表按钮
· router 增加 /hint/aiReview/detail/:id 隐藏路由
- 原件已通过 OSS 留存,详情页可下载,下次可直接查看
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,9 @@ public class OaAiReview extends BaseEntity {
|
||||
/** 合同总体风险评级:高/中/低(简历为空) */
|
||||
private String riskLevel;
|
||||
|
||||
/** AI 审核结论摘要(列表展示,纯文本) */
|
||||
private String summary;
|
||||
|
||||
/** AI 审核结果(Markdown) */
|
||||
private String resultMd;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public class OaAiReviewVo implements Serializable {
|
||||
private String position;
|
||||
private Integer matchScore;
|
||||
private String riskLevel;
|
||||
private String summary;
|
||||
private String resultMd;
|
||||
private String model;
|
||||
private Integer tokens;
|
||||
|
||||
@@ -121,6 +121,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
entity.setFileUrl(fileUrl);
|
||||
entity.setPosition(position);
|
||||
entity.setResultMd(result);
|
||||
entity.setSummary(buildSummary(result));
|
||||
entity.setModel(miMoProps.getModel());
|
||||
if ("resume".equals(reviewType)) {
|
||||
entity.setMatchScore(parseInt(SCORE_PATTERN, result, 100));
|
||||
@@ -137,6 +138,16 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
return text.length() > MAX_TEXT_LEN ? text.substring(0, MAX_TEXT_LEN) : text;
|
||||
}
|
||||
|
||||
/** 从 Markdown 结果里提炼一段纯文本摘要,供列表展示 */
|
||||
private String buildSummary(String md) {
|
||||
if (StringUtils.isBlank(md)) return null;
|
||||
String text = md.replaceAll("(?m)^#+\\s*", "") // 标题符号
|
||||
.replaceAll("[*`>#\\-]", " ") // markdown 符号
|
||||
.replaceAll("\\s+", " ") // 折叠空白
|
||||
.trim();
|
||||
return text.length() > 160 ? text.substring(0, 160) : text;
|
||||
}
|
||||
|
||||
private Integer parseInt(Pattern p, String text, int max) {
|
||||
Matcher m = p.matcher(text == null ? "" : text);
|
||||
if (m.find()) {
|
||||
@@ -194,6 +205,10 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
}
|
||||
lqw.orderByDesc(OaAiReview::getCreateTime);
|
||||
Page<OaAiReviewVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
// 列表只需 summary,清空大字段 resultMd 减小响应体
|
||||
if (page.getRecords() != null) {
|
||||
page.getRecords().forEach(v -> v.setResultMd(null));
|
||||
}
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,12 @@ export const constantRoutes = [
|
||||
name: "editMeetingMinutes",
|
||||
meta: { title: "编辑会议纪要", activeMenu: "/hint/meeting" },
|
||||
},
|
||||
{
|
||||
path: "aiReview/detail/:id(\\d+)",
|
||||
component: () => import("@/views/oa/aiReview/detail"),
|
||||
name: "aiReviewDetail",
|
||||
meta: { title: "审核详情", activeMenu: "/hint/aiReview" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
110
ruoyi-ui/src/views/oa/aiReview/detail.vue
Normal file
110
ruoyi-ui/src/views/oa/aiReview/detail.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="app-container ai-review-detail" v-loading="loading">
|
||||
<el-card shadow="never" class="panel">
|
||||
<div slot="header" class="hd">
|
||||
<div class="hd-left">
|
||||
<el-button size="small" icon="el-icon-back" @click="goBack">返回列表</el-button>
|
||||
<span class="title"><i class="el-icon-document-checked" /> 审核详情</span>
|
||||
</div>
|
||||
<div v-if="info" class="hd-right">
|
||||
<el-tag size="small" :type="info.reviewType === 'contract' ? 'warning' : 'success'">
|
||||
{{ info.reviewType === 'contract' ? '合同审核' : '简历审核' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="info.reviewType === 'resume' && info.matchScore != null"
|
||||
size="small" type="success" effect="dark">匹配度 {{ info.matchScore }}</el-tag>
|
||||
<el-tag v-if="info.reviewType === 'contract' && info.riskLevel"
|
||||
size="small" :type="riskTagType(info.riskLevel)" effect="dark">{{ info.riskLevel }}风险</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-descriptions v-if="info" :column="3" size="small" border class="meta">
|
||||
<el-descriptions-item label="文件名">
|
||||
<span>{{ info.fileName }}</span>
|
||||
<el-button v-if="info.fileUrl" type="text" size="mini" icon="el-icon-download"
|
||||
style="margin-left:8px" @click="downloadFile">下载原件</el-button>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="info.reviewType === 'resume'" label="目标岗位">
|
||||
{{ info.position || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模型">{{ info.model || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">{{ info.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核人">{{ info.createBy || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="info" class="md-body" v-html="renderedMd" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAiReview } from '@/api/oa/aiReview'
|
||||
const marked = require('marked')
|
||||
|
||||
export default {
|
||||
name: 'OaAiReviewDetail',
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
info: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedMd () {
|
||||
if (!this.info || !this.info.resultMd) return ''
|
||||
try { return marked(this.info.resultMd) } catch (e) { return this.info.resultMd }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
marked.setOptions({ breaks: true })
|
||||
const id = this.$route.params.id
|
||||
if (id) this.load(id)
|
||||
else this.loading = false
|
||||
},
|
||||
methods: {
|
||||
load (id) {
|
||||
this.loading = true
|
||||
getAiReview(id).then(res => { this.info = res.data })
|
||||
.finally(() => { this.loading = false })
|
||||
},
|
||||
goBack () {
|
||||
this.$router.push('/hint/aiReview')
|
||||
},
|
||||
downloadFile () {
|
||||
if (this.info && this.info.fileUrl) window.open(this.info.fileUrl, '_blank')
|
||||
},
|
||||
riskTagType (r) {
|
||||
return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-review-detail { padding: 10px; }
|
||||
.panel { ::v-deep .el-card__header { padding: 10px 14px; } }
|
||||
.hd { display: flex; justify-content: space-between; align-items: center;
|
||||
.hd-left { display: flex; align-items: center; gap: 12px;
|
||||
.title { font-weight: 600; font-size: 14px; color: #303133; i { color: #409eff; margin-right: 4px; } }
|
||||
}
|
||||
.hd-right { display: flex; align-items: center; gap: 8px; }
|
||||
}
|
||||
.meta { margin-bottom: 16px; }
|
||||
|
||||
.md-body {
|
||||
font-size: 14px; color: #2c3e50; line-height: 1.8; padding: 4px 6px;
|
||||
::v-deep {
|
||||
h1, h2, h3 { color: #303133; margin: 18px 0 10px; font-weight: 600; }
|
||||
h1 { font-size: 19px; } h2 { font-size: 16px; border-left: 3px solid #409eff; padding-left: 8px; }
|
||||
h3 { font-size: 14px; }
|
||||
p { margin: 8px 0; }
|
||||
ul, ol { padding-left: 22px; margin: 8px 0; }
|
||||
li { margin: 4px 0; }
|
||||
strong { color: #c0392b; }
|
||||
code { background: #f4f7fa; padding: 1px 5px; border-radius: 3px; font-size: 13px; color: #e6a23c; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
|
||||
th, td { border: 1px solid #ebeef5; padding: 6px 10px; font-size: 13px; }
|
||||
th { background: #f5f7fa; }
|
||||
blockquote { border-left: 3px solid #dcdfe6; padding-left: 10px; color: #909399; margin: 8px 0; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,155 +1,189 @@
|
||||
<template>
|
||||
<div class="app-container ai-review">
|
||||
<el-row :gutter="12">
|
||||
<!-- 左:上传 + 历史 -->
|
||||
<el-col :span="8" :xs="24">
|
||||
<el-card shadow="never" class="panel">
|
||||
<div slot="header" class="hd">
|
||||
<i class="el-icon-cpu" /> AI 智能审核
|
||||
</div>
|
||||
<div class="app-container">
|
||||
<!-- 搜索 -->
|
||||
<el-form :model="query" ref="queryForm" size="small" :inline="true" label-width="68px">
|
||||
<el-form-item label="类型" prop="reviewType">
|
||||
<el-select v-model="query.reviewType" placeholder="全部" clearable style="width: 120px" @change="handleQuery">
|
||||
<el-option label="合同" value="contract" />
|
||||
<el-option label="简历" value="resume" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字" prop="keyword">
|
||||
<el-input v-model="query.keyword" placeholder="文件名 / 岗位" clearable style="width: 220px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-radio-group v-model="reviewType" size="small" class="type-switch" @change="onTypeChange">
|
||||
<!-- 工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-magic-stick" size="mini" @click="openUpload">新增审核</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list" @row-dblclick="goDetail">
|
||||
<el-table-column label="类型" align="center" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.reviewType === 'contract' ? 'warning' : 'success'">
|
||||
{{ scope.row.reviewType === 'contract' ? '合同' : '简历' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名" align="left" prop="fileName" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="岗位" align="center" prop="position" width="130" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.position">{{ scope.row.position }}</span>
|
||||
<span v-else style="color:#c0c4cc">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结论" align="center" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.reviewType === 'resume' && scope.row.matchScore != null"
|
||||
size="mini" type="success" effect="dark">匹配 {{ scope.row.matchScore }}</el-tag>
|
||||
<el-tag v-else-if="scope.row.reviewType === 'contract' && scope.row.riskLevel"
|
||||
size="mini" :type="riskTagType(scope.row.riskLevel)" effect="dark">
|
||||
{{ scope.row.riskLevel }}风险</el-tag>
|
||||
<span v-else style="color:#c0c4cc">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核摘要" align="left" prop="summary" min-width="280" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="summary-cell">{{ scope.row.summary || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核时间" align="center" prop="createTime" width="150" />
|
||||
<el-table-column label="操作" align="center" width="130" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="goDetail(scope.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="query.pageNum" :limit.sync="query.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 新增审核弹窗 -->
|
||||
<el-dialog title="新增 AI 审核" :visible.sync="uploadVisible" width="520px" :close-on-click-modal="false"
|
||||
@closed="resetUpload">
|
||||
<el-form label-width="80px" size="small">
|
||||
<el-form-item label="审核类型">
|
||||
<el-radio-group v-model="reviewType">
|
||||
<el-radio-button label="contract">合同审核</el-radio-button>
|
||||
<el-radio-button label="resume">简历审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="hint">
|
||||
<div class="dialog-hint">
|
||||
{{ reviewType === 'contract'
|
||||
? '从“我方”利益角度审查合同,找出不利条款并给出利好我方的修改/补充建议。'
|
||||
? '从“我方”利益角度审查合同,找出不利条款并给出利好我方的修改建议。'
|
||||
: '评估候选人,分析与目标岗位的匹配度、优势短板与面试建议。' }}
|
||||
</div>
|
||||
|
||||
<el-input v-if="reviewType === 'resume'" v-model="position" size="small" class="pos-input"
|
||||
placeholder="目标岗位(选填,如:机械设计工程师)" clearable />
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item v-if="reviewType === 'resume'" label="目标岗位">
|
||||
<el-input v-model="position" placeholder="选填,如:机械设计工程师" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传文件">
|
||||
<el-upload ref="uploader" drag action="#" :auto-upload="false" :limit="1"
|
||||
:show-file-list="true" :on-change="onFileChange" :on-remove="onFileRemove"
|
||||
:file-list="fileList" accept=".pdf,.doc,.docx" class="uploader">
|
||||
:on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList"
|
||||
accept=".pdf,.doc,.docx" class="uploader">
|
||||
<i class="el-icon-upload" />
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div slot="tip" class="el-upload__tip">支持 PDF / Word(.doc/.docx),单个文件 ≤ 20MB</div>
|
||||
<div slot="tip" class="el-upload__tip">支持 PDF / Word(.doc/.docx),≤ 20MB</div>
|
||||
</el-upload>
|
||||
|
||||
<el-button type="primary" icon="el-icon-magic-stick" :loading="analyzing"
|
||||
style="width:100%" @click="doAnalyze">
|
||||
{{ analyzing ? 'AI 审核中…' : '开始审核' }}
|
||||
</el-button>
|
||||
|
||||
<div class="hist-hd">
|
||||
<span><i class="el-icon-time" /> 历史记录</span>
|
||||
<el-input v-model="query.keyword" size="mini" placeholder="搜索文件名/岗位" clearable
|
||||
prefix-icon="el-icon-search" style="width:150px"
|
||||
@keyup.enter.native="searchHist" @clear="searchHist" />
|
||||
</div>
|
||||
<div v-loading="histLoading" class="hist-list">
|
||||
<div v-if="histList.length === 0 && !histLoading" class="hist-empty">暂无记录</div>
|
||||
<div v-for="r in histList" :key="r.id" class="hist-item"
|
||||
:class="{ sel: current && current.id === r.id }" @click="viewHistory(r.id)">
|
||||
<div class="hi-top">
|
||||
<el-tag size="mini" :type="r.reviewType === 'contract' ? 'warning' : 'success'" effect="plain">
|
||||
{{ r.reviewType === 'contract' ? '合同' : '简历' }}
|
||||
</el-tag>
|
||||
<span v-if="r.reviewType === 'resume' && r.matchScore != null" class="hi-badge score">
|
||||
{{ r.matchScore }}分
|
||||
</span>
|
||||
<span v-if="r.reviewType === 'contract' && r.riskLevel" class="hi-badge"
|
||||
:class="riskClass(r.riskLevel)">{{ r.riskLevel }}风险</span>
|
||||
<el-button type="text" icon="el-icon-delete" class="hi-del" @click.stop="delHistory(r)" />
|
||||
</div>
|
||||
<div class="hi-name">{{ r.fileName }}</div>
|
||||
<div class="hi-meta">{{ r.position || '—' }} · {{ r.createTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="query.pageNum"
|
||||
:limit.sync="query.pageSize" :page-sizes="[10,20,50]" small @pagination="loadHist" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右:结果 -->
|
||||
<el-col :span="16" :xs="24">
|
||||
<el-card shadow="never" class="panel result-panel">
|
||||
<div slot="header" class="hd result-hd">
|
||||
<span><i class="el-icon-document-checked" /> 审核结果</span>
|
||||
<div v-if="current" class="result-tags">
|
||||
<el-tag size="mini" :type="current.reviewType === 'contract' ? 'warning' : 'success'">
|
||||
{{ current.reviewType === 'contract' ? '合同' : '简历' }}
|
||||
</el-tag>
|
||||
<span class="r-file">{{ current.fileName }}</span>
|
||||
<el-tag v-if="current.reviewType === 'resume' && current.matchScore != null"
|
||||
size="mini" type="success" effect="dark">匹配度 {{ current.matchScore }}</el-tag>
|
||||
<el-tag v-if="current.reviewType === 'contract' && current.riskLevel"
|
||||
size="mini" :type="riskTagType(current.riskLevel)" effect="dark">
|
||||
{{ current.riskLevel }}风险</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="analyzing" :element-loading-text="loadingText" class="result-body">
|
||||
<div v-if="!current && !analyzing" class="result-empty">
|
||||
<i class="el-icon-cpu" />
|
||||
<div>上传合同或简历,点击“开始审核”获取 AI 分析结果</div>
|
||||
</div>
|
||||
<div v-else-if="current" class="md-body" v-html="renderedMd" />
|
||||
<div v-else class="result-empty"><div>{{ loadingText }}</div></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button size="small" @click="uploadVisible = false">取 消</el-button>
|
||||
<el-button size="small" type="primary" :loading="analyzing" @click="doAnalyze">
|
||||
{{ analyzing ? 'AI 审核中…' : '开始审核' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { analyzeAiReview, listAiReview, getAiReview, delAiReview } from '@/api/oa/aiReview'
|
||||
const marked = require('marked')
|
||||
import { analyzeAiReview, listAiReview, delAiReview } from '@/api/oa/aiReview'
|
||||
|
||||
export default {
|
||||
name: 'OaAiReview',
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
list: [],
|
||||
query: { pageNum: 1, pageSize: 10, keyword: '', reviewType: '' },
|
||||
|
||||
uploadVisible: false,
|
||||
reviewType: 'contract',
|
||||
position: '',
|
||||
fileList: [],
|
||||
rawFile: null,
|
||||
|
||||
analyzing: false,
|
||||
loadingText: 'AI 审核中,长文档可能需要 30~90 秒,请稍候…',
|
||||
current: null,
|
||||
|
||||
histList: [],
|
||||
total: 0,
|
||||
histLoading: false,
|
||||
query: { pageNum: 1, pageSize: 10, keyword: '', reviewType: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedMd () {
|
||||
if (!this.current || !this.current.resultMd) return ''
|
||||
try { return marked(this.current.resultMd) } catch (e) { return this.current.resultMd }
|
||||
analyzing: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
marked.setOptions({ breaks: true })
|
||||
this.loadHist()
|
||||
this.getList()
|
||||
},
|
||||
activated () {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
onTypeChange () {
|
||||
getList () {
|
||||
this.loading = true
|
||||
listAiReview(this.query).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
handleQuery () {
|
||||
this.query.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery () {
|
||||
this.resetForm('queryForm')
|
||||
this.query.reviewType = ''
|
||||
this.loadHist()
|
||||
this.handleQuery()
|
||||
},
|
||||
goDetail (row) {
|
||||
this.$router.push('/hint/aiReview/detail/' + row.id)
|
||||
},
|
||||
handleDelete (row) {
|
||||
this.$modal.confirm(`确认删除「${row.fileName}」的审核记录?`).then(() => {
|
||||
return delAiReview(row.id)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已删除')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
// ===== 新增审核 =====
|
||||
openUpload () {
|
||||
this.uploadVisible = true
|
||||
},
|
||||
resetUpload () {
|
||||
this.reviewType = 'contract'
|
||||
this.position = ''
|
||||
this.fileList = []
|
||||
this.rawFile = null
|
||||
this.analyzing = false
|
||||
if (this.$refs.uploader) this.$refs.uploader.clearFiles()
|
||||
},
|
||||
onFileChange (file, fileList) {
|
||||
// 仅保留最后一个文件
|
||||
if (fileList.length > 1) fileList.splice(0, fileList.length - 1)
|
||||
const isOk = /\.(pdf|doc|docx)$/i.test(file.name)
|
||||
if (!isOk) {
|
||||
if (!/\.(pdf|doc|docx)$/i.test(file.name)) {
|
||||
this.$modal.msgError('仅支持 PDF / Word(.doc/.docx)')
|
||||
this.fileList = []
|
||||
this.rawFile = null
|
||||
return
|
||||
this.fileList = []; this.rawFile = null; return
|
||||
}
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
this.$modal.msgError('文件不能超过 20MB')
|
||||
this.fileList = []
|
||||
this.rawFile = null
|
||||
return
|
||||
this.fileList = []; this.rawFile = null; return
|
||||
}
|
||||
this.rawFile = file.raw
|
||||
this.fileList = [file]
|
||||
@@ -165,12 +199,12 @@ export default {
|
||||
fd.append('reviewType', this.reviewType)
|
||||
if (this.reviewType === 'resume' && this.position) fd.append('position', this.position)
|
||||
this.analyzing = true
|
||||
this.current = null
|
||||
try {
|
||||
const res = await analyzeAiReview(fd)
|
||||
this.current = res.data
|
||||
this.$modal.msgSuccess('审核完成')
|
||||
this.loadHist()
|
||||
this.uploadVisible = false
|
||||
this.getList()
|
||||
if (res.data && res.data.id) this.goDetail(res.data)
|
||||
} catch (e) {
|
||||
// request.js 已弹错误提示
|
||||
} finally {
|
||||
@@ -178,34 +212,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
searchHist () {
|
||||
this.query.pageNum = 1
|
||||
this.loadHist()
|
||||
},
|
||||
loadHist () {
|
||||
this.histLoading = true
|
||||
this.query.reviewType = this.reviewType
|
||||
listAiReview(this.query).then(res => {
|
||||
this.histList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.histLoading = false })
|
||||
},
|
||||
viewHistory (id) {
|
||||
getAiReview(id).then(res => { this.current = res.data })
|
||||
},
|
||||
delHistory (r) {
|
||||
this.$modal.confirm(`确认删除「${r.fileName}」的审核记录?`).then(() => {
|
||||
return delAiReview(r.id)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已删除')
|
||||
if (this.current && this.current.id === r.id) this.current = null
|
||||
this.loadHist()
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
riskClass (r) {
|
||||
return r === '高' ? 'risk-high' : (r === '中' ? 'risk-mid' : 'risk-low')
|
||||
},
|
||||
riskTagType (r) {
|
||||
return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success')
|
||||
}
|
||||
@@ -214,71 +220,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-review { padding: 10px; }
|
||||
.panel { ::v-deep .el-card__header { padding: 10px 14px; } }
|
||||
.hd { font-weight: 600; font-size: 14px; color: #303133; i { color: #409eff; margin-right: 4px; } }
|
||||
|
||||
.type-switch { margin-bottom: 10px; width: 100%;
|
||||
::v-deep .el-radio-button { width: 50%; }
|
||||
::v-deep .el-radio-button__inner { width: 100%; }
|
||||
}
|
||||
.hint { font-size: 12px; color: #909399; line-height: 1.6; margin-bottom: 10px; }
|
||||
.pos-input { margin-bottom: 10px; }
|
||||
.uploader { margin-bottom: 10px;
|
||||
.summary-cell { color: #606266; font-size: 12px; }
|
||||
.dialog-hint { font-size: 12px; color: #909399; line-height: 1.6; margin-top: 4px; }
|
||||
.uploader {
|
||||
::v-deep .el-upload, ::v-deep .el-upload-dragger { width: 100%; }
|
||||
::v-deep .el-upload-dragger { height: 130px; }
|
||||
.el-icon-upload { font-size: 40px; color: #c0c4cc; margin: 18px 0 8px; line-height: 1; }
|
||||
}
|
||||
|
||||
.hist-hd { display: flex; justify-content: space-between; align-items: center;
|
||||
margin: 14px 0 8px; font-size: 13px; font-weight: 600; color: #303133;
|
||||
i { color: #409eff; margin-right: 4px; }
|
||||
}
|
||||
.hist-list { max-height: calc(100vh - 470px); overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
|
||||
.hist-empty { text-align: center; color: #c0c4cc; padding: 20px 0; font-size: 12px; }
|
||||
.hist-item { border: 1px solid #ebeef5; border-radius: 4px; padding: 6px 10px; cursor: pointer; transition: .15s;
|
||||
&:hover { border-color: #409eff; background: #fafdff; }
|
||||
&.sel { border-color: #409eff; background: #ecf5ff; }
|
||||
.hi-top { display: flex; align-items: center; gap: 6px; }
|
||||
.hi-badge { font-size: 11px; padding: 0 5px; border-radius: 8px; line-height: 16px;
|
||||
&.score { background: #f0f9eb; color: #67c23a; }
|
||||
&.risk-high { background: #fef0f0; color: #f56c6c; }
|
||||
&.risk-mid { background: #fdf6ec; color: #e6a23c; }
|
||||
&.risk-low { background: #f0f9eb; color: #67c23a; }
|
||||
}
|
||||
.hi-del { margin-left: auto; padding: 0; color: #c0c4cc; &:hover { color: #f56c6c; } }
|
||||
.hi-name { font-size: 13px; color: #303133; margin: 3px 0 1px; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
.hi-meta { font-size: 11px; color: #909399; }
|
||||
}
|
||||
|
||||
.result-panel { min-height: calc(100vh - 140px); }
|
||||
.result-hd { display: flex; justify-content: space-between; align-items: center;
|
||||
.result-tags { display: flex; align-items: center; gap: 8px;
|
||||
.r-file { font-size: 12px; color: #606266; max-width: 320px; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
}
|
||||
}
|
||||
.result-body { min-height: 400px; }
|
||||
.result-empty { text-align: center; color: #c0c4cc; padding: 100px 0;
|
||||
i { font-size: 56px; opacity: .4; display: block; margin-bottom: 14px; }
|
||||
}
|
||||
|
||||
.md-body {
|
||||
font-size: 14px; color: #2c3e50; line-height: 1.8; padding: 4px 6px;
|
||||
::v-deep {
|
||||
h1, h2, h3 { color: #303133; margin: 18px 0 10px; font-weight: 600; }
|
||||
h1 { font-size: 19px; } h2 { font-size: 16px; border-left: 3px solid #409eff; padding-left: 8px; }
|
||||
h3 { font-size: 14px; }
|
||||
p { margin: 8px 0; }
|
||||
ul, ol { padding-left: 22px; margin: 8px 0; }
|
||||
li { margin: 4px 0; }
|
||||
strong { color: #c0392b; }
|
||||
code { background: #f4f7fa; padding: 1px 5px; border-radius: 3px; font-size: 13px; color: #e6a23c; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
|
||||
th, td { border: 1px solid #ebeef5; padding: 6px 10px; font-size: 13px; }
|
||||
th { background: #f5f7fa; }
|
||||
blockquote { border-left: 3px solid #dcdfe6; padding-left: 10px; color: #909399; margin: 8px 0; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
`position` varchar(255) DEFAULT NULL COMMENT '简历目标岗位',
|
||||
`match_score` int(11) DEFAULT NULL COMMENT '简历匹配度评分 0-100',
|
||||
`risk_level` varchar(10) DEFAULT NULL COMMENT '合同风险评级 高/中/低',
|
||||
`summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)',
|
||||
`result_md` longtext COMMENT 'AI 审核结果(Markdown)',
|
||||
`model` varchar(50) DEFAULT NULL COMMENT '使用的模型',
|
||||
`tokens` int(11) DEFAULT NULL COMMENT '消耗 token',
|
||||
@@ -30,6 +31,9 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 审核记录(合同/简历)';
|
||||
|
||||
-- 若表已存在(旧版本),补加 summary 列(MySQL 不支持 ADD COLUMN IF NOT EXISTS,重复执行报错可忽略):
|
||||
-- ALTER TABLE `oa_ai_review` ADD COLUMN `summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)' AFTER `risk_level`;
|
||||
|
||||
-- ---------------- 菜单:信息 > AI审核 ----------------
|
||||
-- 父菜单 1774989374680858626 = 「信息」
|
||||
INSERT IGNORE INTO `sys_menu`
|
||||
|
||||
Reference in New Issue
Block a user