feat(review-ui): 优化会议项目页与邮件状态并提升AI对话可读性
parent
0e0124872c
commit
00f3b0f7ec
|
|
@ -152,6 +152,10 @@ export const sendMailInvitation = (id: number) =>
|
|||
export const retrySmsLog = (smsLogId: number) =>
|
||||
request.post({ url: '/project/review-meeting/retry-sms', params: { smsLogId } })
|
||||
|
||||
/** 手动重发失败邮件 */
|
||||
export const retryMailLog = (mailLogId: number) =>
|
||||
request.post({ url: '/project/review-meeting/retry-mail', params: { mailLogId } })
|
||||
|
||||
/** 获取短信发送状态列表 */
|
||||
export const getSmsLogList = (reviewMeetingId: number) =>
|
||||
request.get({ url: '/project/review-meeting/sms-log-list', params: { reviewMeetingId } })
|
||||
|
|
|
|||
|
|
@ -11,13 +11,33 @@
|
|||
<el-table-column label="重发次数" prop="retryCount" width="90" align="center" />
|
||||
<el-table-column label="最后发送时间" prop="sendTime" width="170" />
|
||||
<el-table-column label="失败原因" prop="errorMsg" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 2"
|
||||
v-hasPermi="['review:meeting:send-mail']"
|
||||
type="primary"
|
||||
link
|
||||
:loading="retryingIds.has(row.id)"
|
||||
@click="handleRetry(row)"
|
||||
>
|
||||
重新发送
|
||||
</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getMailLogList, type ReviewMeetingMailLogRespVO } from '@/api/review/meeting'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getMailLogList,
|
||||
retryMailLog,
|
||||
type ReviewMeetingMailLogRespVO
|
||||
} from '@/api/review/meeting'
|
||||
|
||||
defineOptions({ name: 'MailStatusDialog' })
|
||||
|
||||
|
|
@ -26,6 +46,7 @@ const loading = ref(false)
|
|||
const meetingName = ref('')
|
||||
const reviewMeetingId = ref<number>()
|
||||
const logList = ref<ReviewMeetingMailLogRespVO[]>([])
|
||||
const retryingIds = ref<Set<number>>(new Set())
|
||||
|
||||
const MAIL_STATUS_LABEL: Record<number, string> = { 0: '待发送', 1: '成功', 2: '失败', 3: '已忽略' }
|
||||
const MAIL_STATUS_TYPE: Record<number, string> = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' }
|
||||
|
|
@ -46,5 +67,21 @@ const loadData = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleRetry = async (row: ReviewMeetingMailLogRespVO) => {
|
||||
await ElMessageBox.confirm(
|
||||
`确认重新发送给 ${row.expertName || row.mail} 的邮件邀请函?`,
|
||||
'重发确认',
|
||||
{ type: 'warning', confirmButtonText: '确认重发' }
|
||||
)
|
||||
retryingIds.value.add(row.id)
|
||||
try {
|
||||
await retryMailLog(row.id)
|
||||
ElMessage.success('重发任务已触发')
|
||||
await loadData()
|
||||
} finally {
|
||||
retryingIds.value.delete(row.id)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,55 +4,37 @@
|
|||
<div class="page-title">项目列表</div>
|
||||
|
||||
<!-- 会议摘要信息 -->
|
||||
<div class="meeting-summary-bar">
|
||||
<span class="summary-label">所属会议:</span>
|
||||
<span class="summary-meeting-name">{{ meetingInfo.name || '-' }}</span>
|
||||
<span class="summary-sep">|</span>
|
||||
<span class="summary-label">会议时间:</span>
|
||||
<span class="summary-value">
|
||||
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
||||
~
|
||||
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
||||
</span>
|
||||
<span class="summary-sep">|</span>
|
||||
<span class="summary-label">会议地点:</span>
|
||||
<span class="summary-value">{{ meetingInfo.location || '-' }}</span>
|
||||
<span class="summary-sep">|</span>
|
||||
<span class="summary-label">状态:</span>
|
||||
<span :class="`status-text status-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="queryParams.projectTitle"
|
||||
placeholder="项目标题"
|
||||
clearable
|
||||
class="search-input"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
<el-input
|
||||
v-model="queryParams.reporter"
|
||||
placeholder="报告人"
|
||||
clearable
|
||||
class="search-input"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
<el-select v-model="queryParams.agendaCategory" placeholder="议程分类" clearable class="search-input">
|
||||
<el-option v-for="item in REVIEW_AGENDA_CATEGORY_OPTIONS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<button class="btn-reset" @click="resetQuery">重置</button>
|
||||
<button class="btn-search" @click="handleQuery">查询</button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="toolbar">
|
||||
<button class="btn-default" @click="openForm('create')">
|
||||
<span class="btn-icon">+</span> 新增评审项目
|
||||
</button>
|
||||
<button class="btn-default btn-danger" :disabled="selectedIds.length === 0" @click="handleDelete()">
|
||||
批量删除
|
||||
</button>
|
||||
<div class="meeting-info-card">
|
||||
<div class="meeting-info-head">
|
||||
<div class="meeting-title-wrap">
|
||||
<div class="meeting-caption">所属会议</div>
|
||||
<div class="meeting-name">{{ meetingInfo.name || '-' }}</div>
|
||||
<div class="meeting-time">
|
||||
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
||||
<span class="meeting-time-sep">~</span>
|
||||
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<span :class="`status-pill status-pill-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] || '-' }}</span>
|
||||
</div>
|
||||
<div class="meeting-meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">会议地点</span>
|
||||
<span class="meta-value">{{ meetingInfo.location || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">组织单位</span>
|
||||
<span class="meta-value">{{ meetingInfo.organizationUnit || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">会议主持人</span>
|
||||
<span class="meta-value">{{ meetingInfo.host || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">参会专家数</span>
|
||||
<span class="meta-value">{{ meetingInfo.expertCount ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
|
|
@ -64,9 +46,7 @@
|
|||
size="small"
|
||||
border
|
||||
class="review-table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="46" align="center" />
|
||||
<el-table-column label="拖拽" width="54" align="center">
|
||||
<template #default>
|
||||
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
||||
|
|
@ -195,49 +175,6 @@
|
|||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 编辑/新增项目弹窗 -->
|
||||
<el-dialog v-model="formVisible" :title="formType === 'create' ? '新增评审项目' : '编辑评审项目'" width="550px">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="序号" prop="seqNo">
|
||||
<el-input-number v-model="formData.seqNo" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<div style="display: flex; gap: 10px; width: 100%;">
|
||||
<el-time-picker v-model="formData.startTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间" style="flex: 1" />
|
||||
<span>-</span>
|
||||
<el-time-picker v-model="formData.endTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间" style="flex: 1" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="议程分类" prop="agendaCategory">
|
||||
<el-select v-model="formData.agendaCategory" placeholder="请选择议程分类" style="width: 100%">
|
||||
<el-option v-for="item in REVIEW_AGENDA_CATEGORY_OPTIONS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目标题" prop="projectTitle">
|
||||
<el-input v-model="formData.projectTitle" placeholder="请输入项目标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="汇报人" prop="reporter">
|
||||
<el-input v-model="formData.reporter" placeholder="请输入汇报人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报告单位" prop="reporterUnit">
|
||||
<el-input v-model="formData.reporterUnit" placeholder="请输入报告单位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评审日期" prop="reviewDate">
|
||||
<el-date-picker v-model="formData.reviewDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择评审日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评审结果" prop="reviewResult">
|
||||
<el-select v-model="formData.reviewResult" placeholder="请选择评审结果" clearable style="width: 100%">
|
||||
<el-option label="通过" value="PASS" />
|
||||
<el-option label="不通过" value="REJECT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="submitForm">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
|
|
@ -254,7 +191,6 @@ import {
|
|||
getReviewProjectPage,
|
||||
updateReviewProject,
|
||||
updateReviewProjectSeqBatch,
|
||||
createReviewProject,
|
||||
deleteReviewProject,
|
||||
type ReviewMeetingProjectRespVO
|
||||
} from '@/api/review/project'
|
||||
|
|
@ -295,10 +231,7 @@ const inlineSnapshotMap = ref<Record<number, InlineEditableFields>>({})
|
|||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
reviewMeetingId,
|
||||
projectTitle: undefined as string | undefined,
|
||||
agendaCategory: undefined as string | undefined,
|
||||
reporter: undefined as string | undefined
|
||||
reviewMeetingId
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
|
|
@ -397,11 +330,6 @@ const initSortable = () => {
|
|||
}
|
||||
|
||||
const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||
if (queryParams.projectTitle || queryParams.agendaCategory || queryParams.reporter) {
|
||||
ElMessage.warning('请先清空筛选条件后再拖拽排序')
|
||||
await getList()
|
||||
return
|
||||
}
|
||||
const moved = list.value.splice(oldIndex, 1)[0]
|
||||
if (!moved) return
|
||||
list.value.splice(newIndex, 0, moved)
|
||||
|
|
@ -446,68 +374,13 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
||||
const resetQuery = () => {
|
||||
queryParams.projectTitle = undefined
|
||||
queryParams.agendaCategory = undefined
|
||||
queryParams.reporter = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const selectedIds = ref<number[]>([])
|
||||
const handleSelectionChange = (val: ReviewMeetingProjectRespVO[]) => {
|
||||
selectedIds.value = val.map(v => v.id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id?: number) => {
|
||||
const ids = id ? [id] : selectedIds.value
|
||||
if (ids.length === 0) return
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${ids.length} 个项目吗?`, '警告', { type: 'warning' })
|
||||
await deleteReviewProject(ids)
|
||||
const handleDelete = async (id: number) => {
|
||||
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
|
||||
await deleteReviewProject([id])
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
}
|
||||
|
||||
const formVisible = ref(false)
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive<Partial<ReviewMeetingProjectRespVO>>({})
|
||||
const formRules = {
|
||||
agendaCategory: [{ required: true, message: '请选择议程分类', trigger: 'change' }],
|
||||
projectTitle: [{ required: true, message: '项目标题不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', row?: ReviewMeetingProjectRespVO) => {
|
||||
formType.value = type
|
||||
if (type === 'create') {
|
||||
Object.keys(formData).forEach(key => delete formData[key as keyof ReviewMeetingProjectRespVO])
|
||||
formData.reviewMeetingId = reviewMeetingId
|
||||
} else if (row) {
|
||||
Object.assign(formData, row)
|
||||
}
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await createReviewProject(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await updateReviewProject(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
formVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
||||
router.push({
|
||||
name: 'ReviewProjectDetail',
|
||||
|
|
@ -548,42 +421,93 @@ onBeforeUnmount(() => {
|
|||
border-bottom: 1px solid #e1e7f0;
|
||||
}
|
||||
|
||||
/* ── 会议摘要栏 ── */
|
||||
.meeting-summary-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
background-color: rgba(41, 90, 188, 0.05);
|
||||
border: 1px solid rgba(41, 90, 188, 0.12);
|
||||
border-radius: 6px;
|
||||
/* ── 会议信息卡 ── */
|
||||
.meeting-info-card {
|
||||
padding: 16px;
|
||||
background: linear-gradient(180deg, rgba(41, 90, 188, 0.1) 0%, rgba(41, 90, 188, 0.04) 100%);
|
||||
border: 1px solid rgba(41, 90, 188, 0.16);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.summary-label {
|
||||
color: #666;
|
||||
.meeting-info-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.summary-meeting-name {
|
||||
.meeting-title-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.meeting-caption {
|
||||
color: #4f6d9f;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.meeting-name {
|
||||
color: #295abc;
|
||||
font-size: 20px;
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
.meeting-time {
|
||||
margin: 0;
|
||||
color: #2f3f5e;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.summary-value {
|
||||
color: #333;
|
||||
.meeting-time-sep {
|
||||
margin: 0 6px;
|
||||
color: #7d91b4;
|
||||
}
|
||||
.summary-sep {
|
||||
color: #ddd;
|
||||
margin: 0 4px;
|
||||
.status-pill {
|
||||
align-self: flex-start;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-pill-0 {
|
||||
color: #b57616;
|
||||
background: rgba(236, 174, 75, 0.2);
|
||||
}
|
||||
.status-pill-1 {
|
||||
color: #267d1e;
|
||||
background: rgba(115, 192, 71, 0.2);
|
||||
}
|
||||
.status-pill-2,
|
||||
.status-pill-3 {
|
||||
color: #596b89;
|
||||
background: rgba(125, 145, 180, 0.16);
|
||||
}
|
||||
.meeting-meta-grid {
|
||||
display: grid;
|
||||
gap: 10px 14px;
|
||||
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
||||
}
|
||||
.meta-item {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(41, 90, 188, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.meta-label {
|
||||
display: block;
|
||||
color: #6a7f9f;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.meta-value {
|
||||
display: block;
|
||||
color: #20314f;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── 状态文字 ── */
|
||||
.status-text { font-size: 14px; font-weight: 500; }
|
||||
.status-0 { color: #ecae4b; }
|
||||
.status-1 { color: #73c047; }
|
||||
.status-2 { color: #999; }
|
||||
.status-3 { color: #999; }
|
||||
|
||||
.review-result-pass {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
|
|
@ -603,100 +527,6 @@ onBeforeUnmount(() => {
|
|||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ── 搜索栏 ── */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.search-input {
|
||||
width: 180px;
|
||||
}
|
||||
:deep(.search-input .el-input__wrapper) {
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
border-color: #dcdedf;
|
||||
}
|
||||
:deep(.search-input .el-input__inner) {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
height: 40px;
|
||||
padding: 0 22px;
|
||||
background-color: #295abc;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn-search:hover { background-color: rgba(41, 90, 188, 0.88); }
|
||||
|
||||
.btn-reset {
|
||||
height: 40px;
|
||||
padding: 0 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #295abc;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-reset:hover { opacity: 0.8; }
|
||||
|
||||
/* ── 工具栏 ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.btn-default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d5d5d5;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-default:hover {
|
||||
background-color: rgba(41, 90, 188, 0.08);
|
||||
border-color: #295abc;
|
||||
color: #295abc;
|
||||
}
|
||||
.btn-default:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-danger {
|
||||
border-color: #fc4f54;
|
||||
color: #fc4f54;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: rgba(252, 79, 84, 0.08);
|
||||
border-color: #fc4f54;
|
||||
color: #fc4f54;
|
||||
}
|
||||
.btn-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── 项目名称 ── */
|
||||
.project-name-text {
|
||||
color: #295abc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.inline-time-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -781,4 +611,15 @@ onBeforeUnmount(() => {
|
|||
:deep(.drag-chosen > td) {
|
||||
background-color: rgba(41, 90, 188, 0.06);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.meeting-meta-grid {
|
||||
grid-template-columns: repeat(2, minmax(140px, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.meeting-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -206,7 +206,12 @@
|
|||
>
|
||||
<div class="msg-bubble">
|
||||
<span class="msg-role">{{ msg.type === 'user' ? '您' : 'AI' }}</span>
|
||||
<div class="msg-content" v-html="renderMsgContent(msg.content)"></div>
|
||||
<div class="msg-content">
|
||||
<div
|
||||
class="md-content"
|
||||
v-html="msg.type === 'user' ? renderUserMsgContent(msg.content) : renderAiMsgContent(msg.content)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流式生成中的气泡 -->
|
||||
|
|
@ -215,7 +220,7 @@
|
|||
<span class="msg-role">AI</span>
|
||||
<div class="msg-content">
|
||||
<span v-if="streamingLoading && !streamingText" class="streaming-dot">思考中…</span>
|
||||
<span v-else>{{ streamingText }}</span>
|
||||
<div v-else class="md-content" v-html="renderAiMsgContent(streamingText)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -274,6 +279,7 @@
|
|||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Document, Folder, InfoFilled, Link, Loading, Refresh, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import {
|
||||
getFileOpenUrl,
|
||||
|
|
@ -776,9 +782,23 @@ const scheduleOfficeWarmup = (files: ReviewMeetingFileRespVO[]) => {
|
|||
})
|
||||
}
|
||||
|
||||
const renderMsgContent = (text: string) =>
|
||||
text.replace(/\n/g, '<br/>').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/<br\/>/g, '<br/>')
|
||||
const markdown = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
const escapeHtml = (text: string) =>
|
||||
text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
const renderUserMsgContent = (text: string) =>
|
||||
escapeHtml(text || '').replace(/\n/g, '<br/>')
|
||||
|
||||
const renderAiMsgContent = (text: string) =>
|
||||
markdown.render(text || '')
|
||||
|
||||
loadCatalog()
|
||||
</script>
|
||||
|
|
@ -1003,9 +1023,52 @@ loadCatalog()
|
|||
word-break: break-word;
|
||||
}
|
||||
.user-msg .msg-content { background: #2563eb; color: #fff; border-bottom-right-radius: 3px; }
|
||||
.ai-msg .msg-content { background: #f3f4f6; color: #1f2937; border-bottom-left-radius: 3px; }
|
||||
.ai-msg .msg-content { background: #f3f4f6; color: #1f2937; border-bottom-left-radius: 3px; overflow-x: auto; }
|
||||
.streaming-dot { color: #9ca3af; }
|
||||
|
||||
.msg-content :deep(.md-content > *:first-child) { margin-top: 0; }
|
||||
.msg-content :deep(.md-content > *:last-child) { margin-bottom: 0; }
|
||||
.msg-content :deep(p) { margin: 0 0 6px; }
|
||||
.msg-content :deep(ul),
|
||||
.msg-content :deep(ol) { margin: 0 0 6px 18px; padding: 0; }
|
||||
.msg-content :deep(li) { margin: 0 0 4px; }
|
||||
.msg-content :deep(code) {
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.msg-content :deep(pre) {
|
||||
margin: 6px 0;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.msg-content :deep(pre code) {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.msg-content :deep(table) {
|
||||
width: 100%;
|
||||
min-width: 560px;
|
||||
border-collapse: collapse;
|
||||
margin: 6px 0;
|
||||
background: #fff;
|
||||
}
|
||||
.msg-content :deep(th),
|
||||
.msg-content :deep(td) {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
border: 1px solid #d1d5db;
|
||||
vertical-align: top;
|
||||
}
|
||||
.msg-content :deep(th) { background: #f8fafc; font-weight: 600; }
|
||||
.user-msg .msg-content :deep(code) { background: rgba(255, 255, 255, 0.18); }
|
||||
.user-msg .msg-content :deep(a) { color: #dbeafe; }
|
||||
.ai-msg .msg-content :deep(a) { color: #1d4ed8; }
|
||||
|
||||
/* 快捷问题 */
|
||||
.quick-questions {
|
||||
flex-shrink: 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue