feat(review-ui): 优化会议项目页与邮件状态并提升AI对话可读性
parent
0e0124872c
commit
00f3b0f7ec
|
|
@ -152,6 +152,10 @@ export const sendMailInvitation = (id: number) =>
|
||||||
export const retrySmsLog = (smsLogId: number) =>
|
export const retrySmsLog = (smsLogId: number) =>
|
||||||
request.post({ url: '/project/review-meeting/retry-sms', params: { smsLogId } })
|
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) =>
|
export const getSmsLogList = (reviewMeetingId: number) =>
|
||||||
request.get({ url: '/project/review-meeting/sms-log-list', params: { reviewMeetingId } })
|
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="retryCount" width="90" align="center" />
|
||||||
<el-table-column label="最后发送时间" prop="sendTime" width="170" />
|
<el-table-column label="最后发送时间" prop="sendTime" width="170" />
|
||||||
<el-table-column label="失败原因" prop="errorMsg" min-width="150" show-overflow-tooltip />
|
<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-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
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' })
|
defineOptions({ name: 'MailStatusDialog' })
|
||||||
|
|
||||||
|
|
@ -26,6 +46,7 @@ const loading = ref(false)
|
||||||
const meetingName = ref('')
|
const meetingName = ref('')
|
||||||
const reviewMeetingId = ref<number>()
|
const reviewMeetingId = ref<number>()
|
||||||
const logList = ref<ReviewMeetingMailLogRespVO[]>([])
|
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_LABEL: Record<number, string> = { 0: '待发送', 1: '成功', 2: '失败', 3: '已忽略' }
|
||||||
const MAIL_STATUS_TYPE: Record<number, string> = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' }
|
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 })
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -4,55 +4,37 @@
|
||||||
<div class="page-title">项目列表</div>
|
<div class="page-title">项目列表</div>
|
||||||
|
|
||||||
<!-- 会议摘要信息 -->
|
<!-- 会议摘要信息 -->
|
||||||
<div class="meeting-summary-bar">
|
<div class="meeting-info-card">
|
||||||
<span class="summary-label">所属会议:</span>
|
<div class="meeting-info-head">
|
||||||
<span class="summary-meeting-name">{{ meetingInfo.name || '-' }}</span>
|
<div class="meeting-title-wrap">
|
||||||
<span class="summary-sep">|</span>
|
<div class="meeting-caption">所属会议</div>
|
||||||
<span class="summary-label">会议时间:</span>
|
<div class="meeting-name">{{ meetingInfo.name || '-' }}</div>
|
||||||
<span class="summary-value">
|
<div class="meeting-time">
|
||||||
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
{{ 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') : '-' }}
|
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
||||||
</span>
|
</div>
|
||||||
<span class="summary-sep">|</span>
|
</div>
|
||||||
<span class="summary-label">会议地点:</span>
|
<span :class="`status-pill status-pill-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] || '-' }}</span>
|
||||||
<span class="summary-value">{{ meetingInfo.location || '-' }}</span>
|
</div>
|
||||||
<span class="summary-sep">|</span>
|
<div class="meeting-meta-grid">
|
||||||
<span class="summary-label">状态:</span>
|
<div class="meta-item">
|
||||||
<span :class="`status-text status-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] }}</span>
|
<span class="meta-label">会议地点</span>
|
||||||
</div>
|
<span class="meta-value">{{ meetingInfo.location || '-' }}</span>
|
||||||
|
</div>
|
||||||
<!-- 搜索栏 -->
|
<div class="meta-item">
|
||||||
<div class="search-bar">
|
<span class="meta-label">组织单位</span>
|
||||||
<el-input
|
<span class="meta-value">{{ meetingInfo.organizationUnit || '-' }}</span>
|
||||||
v-model="queryParams.projectTitle"
|
</div>
|
||||||
placeholder="项目标题"
|
<div class="meta-item">
|
||||||
clearable
|
<span class="meta-label">会议主持人</span>
|
||||||
class="search-input"
|
<span class="meta-value">{{ meetingInfo.host || '-' }}</span>
|
||||||
@keyup.enter="handleQuery"
|
</div>
|
||||||
/>
|
<div class="meta-item">
|
||||||
<el-input
|
<span class="meta-label">参会专家数</span>
|
||||||
v-model="queryParams.reporter"
|
<span class="meta-value">{{ meetingInfo.expertCount ?? 0 }}</span>
|
||||||
placeholder="报告人"
|
</div>
|
||||||
clearable
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
|
|
@ -64,9 +46,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
border
|
border
|
||||||
class="review-table"
|
class="review-table"
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="46" align="center" />
|
|
||||||
<el-table-column label="拖拽" width="54" align="center">
|
<el-table-column label="拖拽" width="54" align="center">
|
||||||
<template #default>
|
<template #default>
|
||||||
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
||||||
|
|
@ -195,49 +175,6 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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" />
|
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
|
|
@ -254,7 +191,6 @@ import {
|
||||||
getReviewProjectPage,
|
getReviewProjectPage,
|
||||||
updateReviewProject,
|
updateReviewProject,
|
||||||
updateReviewProjectSeqBatch,
|
updateReviewProjectSeqBatch,
|
||||||
createReviewProject,
|
|
||||||
deleteReviewProject,
|
deleteReviewProject,
|
||||||
type ReviewMeetingProjectRespVO
|
type ReviewMeetingProjectRespVO
|
||||||
} from '@/api/review/project'
|
} from '@/api/review/project'
|
||||||
|
|
@ -295,10 +231,7 @@ const inlineSnapshotMap = ref<Record<number, InlineEditableFields>>({})
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
reviewMeetingId,
|
reviewMeetingId
|
||||||
projectTitle: undefined as string | undefined,
|
|
||||||
agendaCategory: undefined as string | undefined,
|
|
||||||
reporter: undefined as string | undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
|
|
@ -397,11 +330,6 @@ const initSortable = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
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]
|
const moved = list.value.splice(oldIndex, 1)[0]
|
||||||
if (!moved) return
|
if (!moved) return
|
||||||
list.value.splice(newIndex, 0, moved)
|
list.value.splice(newIndex, 0, moved)
|
||||||
|
|
@ -446,68 +374,13 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
const handleDelete = async (id: number) => {
|
||||||
const resetQuery = () => {
|
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
|
||||||
queryParams.projectTitle = undefined
|
await deleteReviewProject([id])
|
||||||
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)
|
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
getList()
|
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) => {
|
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ReviewProjectDetail',
|
name: 'ReviewProjectDetail',
|
||||||
|
|
@ -548,42 +421,93 @@ onBeforeUnmount(() => {
|
||||||
border-bottom: 1px solid #e1e7f0;
|
border-bottom: 1px solid #e1e7f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 会议摘要栏 ── */
|
/* ── 会议信息卡 ── */
|
||||||
.meeting-summary-bar {
|
.meeting-info-card {
|
||||||
display: flex;
|
padding: 16px;
|
||||||
flex-wrap: wrap;
|
background: linear-gradient(180deg, rgba(41, 90, 188, 0.1) 0%, rgba(41, 90, 188, 0.04) 100%);
|
||||||
align-items: center;
|
border: 1px solid rgba(41, 90, 188, 0.16);
|
||||||
gap: 6px;
|
border-radius: 10px;
|
||||||
padding: 10px 14px;
|
|
||||||
background-color: rgba(41, 90, 188, 0.05);
|
|
||||||
border: 1px solid rgba(41, 90, 188, 0.12);
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
.summary-label {
|
.meeting-info-head {
|
||||||
color: #666;
|
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;
|
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;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.summary-value {
|
.meeting-time-sep {
|
||||||
color: #333;
|
margin: 0 6px;
|
||||||
|
color: #7d91b4;
|
||||||
}
|
}
|
||||||
.summary-sep {
|
.status-pill {
|
||||||
color: #ddd;
|
align-self: flex-start;
|
||||||
margin: 0 4px;
|
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 {
|
.review-result-pass {
|
||||||
color: #67c23a;
|
color: #67c23a;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -603,100 +527,6 @@ onBeforeUnmount(() => {
|
||||||
font-size: 16px;
|
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 {
|
.inline-time-range {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -781,4 +611,15 @@ onBeforeUnmount(() => {
|
||||||
:deep(.drag-chosen > td) {
|
:deep(.drag-chosen > td) {
|
||||||
background-color: rgba(41, 90, 188, 0.06);
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,12 @@
|
||||||
>
|
>
|
||||||
<div class="msg-bubble">
|
<div class="msg-bubble">
|
||||||
<span class="msg-role">{{ msg.type === 'user' ? '您' : 'AI' }}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<!-- 流式生成中的气泡 -->
|
<!-- 流式生成中的气泡 -->
|
||||||
|
|
@ -215,7 +220,7 @@
|
||||||
<span class="msg-role">AI</span>
|
<span class="msg-role">AI</span>
|
||||||
<div class="msg-content">
|
<div class="msg-content">
|
||||||
<span v-if="streamingLoading && !streamingText" class="streaming-dot">思考中…</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -274,6 +279,7 @@
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { Document, Folder, InfoFilled, Link, Loading, Refresh, WarningFilled } from '@element-plus/icons-vue'
|
import { Document, Folder, InfoFilled, Link, Loading, Refresh, WarningFilled } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import {
|
import {
|
||||||
getFileOpenUrl,
|
getFileOpenUrl,
|
||||||
|
|
@ -776,9 +782,23 @@ const scheduleOfficeWarmup = (files: ReviewMeetingFileRespVO[]) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMsgContent = (text: string) =>
|
const markdown = new MarkdownIt({
|
||||||
text.replace(/\n/g, '<br/>').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
html: false,
|
||||||
.replace(/<br\/>/g, '<br/>')
|
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()
|
loadCatalog()
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1003,9 +1023,52 @@ loadCatalog()
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.user-msg .msg-content { background: #2563eb; color: #fff; border-bottom-right-radius: 3px; }
|
.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; }
|
.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 {
|
.quick-questions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue