feat(review-meeting): support active tablet meeting selection
parent
823e50e52f
commit
25eef30456
|
|
@ -26,9 +26,6 @@ export interface ReviewMeetingSaveReqVO {
|
|||
organizationUnit?: string
|
||||
startTime?: string | number
|
||||
endTime?: string | number
|
||||
materialViewStartTime?: string | number
|
||||
materialViewEndTime?: string | number
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
host?: string
|
||||
agendaAttachmentName?: string
|
||||
|
|
@ -59,9 +56,6 @@ export interface ReviewMeetingRespVO {
|
|||
organizationUnit?: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
materialViewStartTime?: string
|
||||
materialViewEndTime?: string
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
host?: string
|
||||
agendaAttachmentName?: string
|
||||
|
|
@ -81,6 +75,7 @@ export interface ReviewMeetingRespVO {
|
|||
expertCount: number
|
||||
projectCount: number
|
||||
mailSent?: boolean
|
||||
tabletActive?: boolean
|
||||
createTime: string
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +154,14 @@ export const cancelReviewMeeting = (id: number) =>
|
|||
export const finishReviewMeeting = (id: number) =>
|
||||
request.put({ url: '/project/review-meeting/finish', params: { id } })
|
||||
|
||||
/** 设为当前平板评审会议(单选) */
|
||||
export const setTabletActiveMeeting = (id: number) =>
|
||||
request.put({ url: '/project/review-meeting/tablet-active', params: { id } })
|
||||
|
||||
/** 清空当前平板评审会议 */
|
||||
export const clearTabletActiveMeeting = () =>
|
||||
request.delete({ url: '/project/review-meeting/tablet-active' })
|
||||
|
||||
/** 复制会议(仅已结束/已取消) */
|
||||
export const copyReviewMeeting = (id: number) =>
|
||||
request.post({ url: '/project/review-meeting/copy', params: { id } })
|
||||
|
|
@ -199,7 +202,10 @@ export const getMailLogList = (reviewMeetingId: number) =>
|
|||
export const importProjectsFromExcel = async (file: File): Promise<ReviewProjectItemVO[]> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await request.upload<any>({ url: '/project/review-meeting/import-projects', data: formData })
|
||||
const res = await request.upload<any>({
|
||||
url: '/project/review-meeting/import-projects',
|
||||
data: formData
|
||||
})
|
||||
return res?.data || []
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +218,10 @@ export const uploadAgendaAttachment = async (
|
|||
|
||||
/** 根据当前表单内容自动生成议程附件 */
|
||||
export const generateAgendaAttachment = (data: ReviewMeetingAgendaGenerateReqVO) =>
|
||||
request.post<ReviewMeetingAgendaAttachmentRespVO>({ url: '/project/review-meeting/generate-agenda', data })
|
||||
request.post<ReviewMeetingAgendaAttachmentRespVO>({
|
||||
url: '/project/review-meeting/generate-agenda',
|
||||
data
|
||||
})
|
||||
|
||||
/** 上传会议纪要附件(Word/PDF/图片) */
|
||||
export const uploadMinutesAttachment = (
|
||||
|
|
@ -283,5 +292,7 @@ const uploadMeetingAttachmentByPresignedUrl = async (
|
|||
}
|
||||
|
||||
const resolveAttachmentType = (fileName: string): string => {
|
||||
return fileName.includes('.') ? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase() : ''
|
||||
return fileName.includes('.')
|
||||
? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||
: ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export interface ReviewTabletOpenUrlVO {
|
|||
visitUrl: string
|
||||
}
|
||||
|
||||
export const getTodayCatalog = () => request.get({ url: '/project/review-tablet/catalog/today' })
|
||||
export const getActiveCatalog = () => request.get({ url: '/project/review-tablet/catalog/active' })
|
||||
|
||||
export const getProjectFiles = (reviewMeetingProjectId: number) =>
|
||||
request.get({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@
|
|||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="组织单位" prop="organizationUnit">
|
||||
<el-input v-model="formData.organizationUnit" placeholder="请输入组织单位" :disabled="isView" />
|
||||
<el-input
|
||||
v-model="formData.organizationUnit"
|
||||
placeholder="请输入组织单位"
|
||||
:disabled="isView"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -54,7 +58,12 @@
|
|||
:disabled="isView"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in meetingLocationOptions" :key="item" :label="item" :value="item" />
|
||||
<el-option
|
||||
v-for="item in meetingLocationOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
|
@ -65,21 +74,6 @@
|
|||
<el-input v-model="formData.host" placeholder="请输入会议主持人" :disabled="isView" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
|
||||
<el-date-picker
|
||||
v-model="formData.materialViewTimeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="x"
|
||||
:disabled="isView"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="议程附件" prop="agendaAttachmentUrl">
|
||||
<div class="agenda-attachment-wrap">
|
||||
|
|
@ -92,7 +86,12 @@
|
|||
>
|
||||
<button type="button" class="btn-upload">上传议程附件</button>
|
||||
</el-upload>
|
||||
<button type="button" class="btn-generate" :disabled="formLoading" @click="handleGenerateAgenda">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-generate"
|
||||
:disabled="formLoading"
|
||||
@click="handleGenerateAgenda"
|
||||
>
|
||||
自动生成议程
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -101,9 +100,15 @@
|
|||
<el-link type="primary" :underline="false" @click="previewAgendaAttachment">
|
||||
{{ formData.agendaAttachmentName }}
|
||||
</el-link>
|
||||
<el-tag size="small">{{ (formData.agendaAttachmentType || '').toUpperCase() }}</el-tag>
|
||||
<el-text type="info" size="small">{{ formatFileSize(formData.agendaAttachmentSize) }}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment">移除</el-button>
|
||||
<el-tag size="small">{{
|
||||
(formData.agendaAttachmentType || '').toUpperCase()
|
||||
}}</el-tag>
|
||||
<el-text type="info" size="small">{{
|
||||
formatFileSize(formData.agendaAttachmentSize)
|
||||
}}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
|
@ -126,11 +131,20 @@
|
|||
<el-link type="primary" :underline="false" @click="previewMinutesAttachment">
|
||||
{{ formData.minutesAttachmentName }}
|
||||
</el-link>
|
||||
<el-tag size="small">{{ (formData.minutesAttachmentType || '').toUpperCase() }}</el-tag>
|
||||
<el-text type="info" size="small">{{ formatFileSize(formData.minutesAttachmentSize) }}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearMinutesAttachment">移除</el-button>
|
||||
<el-tag size="small">{{
|
||||
(formData.minutesAttachmentType || '').toUpperCase()
|
||||
}}</el-tag>
|
||||
<el-text type="info" size="small">{{
|
||||
formatFileSize(formData.minutesAttachmentSize)
|
||||
}}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearMinutesAttachment"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="formData.minutesAttachmentUrl && formData.minutesAiStatusName" class="minutes-ai-line">
|
||||
<div
|
||||
v-if="formData.minutesAttachmentUrl && formData.minutesAiStatusName"
|
||||
class="minutes-ai-line"
|
||||
>
|
||||
<el-tag size="small" :type="getMinutesAiTagType(formData.minutesAiStatus)">
|
||||
{{ formData.minutesAiStatusName }}
|
||||
</el-tag>
|
||||
|
|
@ -171,7 +185,9 @@
|
|||
>
|
||||
<button type="button" class="btn-default">导入验收申请 Excel</button>
|
||||
</el-upload>
|
||||
<button type="button" class="btn-default" @click="handleDownloadTemplate">下载导入模板</button>
|
||||
<button type="button" class="btn-default" @click="handleDownloadTemplate"
|
||||
>下载导入模板</button
|
||||
>
|
||||
<span class="import-hint">格式:序号、议程分类、项目标题、汇报人、报告人单位</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -194,7 +210,13 @@
|
|||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="form-footer">
|
||||
<button v-if="!isView" type="button" class="btn-primary" :disabled="formLoading" @click="submitForm">
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
:disabled="formLoading"
|
||||
@click="submitForm"
|
||||
>
|
||||
{{ formLoading ? '保存中...' : '保存草稿' }}
|
||||
</button>
|
||||
<button type="button" class="btn-default" @click="handleBack">返回</button>
|
||||
|
|
@ -271,7 +293,6 @@ const pageTitle = computed(() => {
|
|||
type FormData = ReviewMeetingSaveReqVO & {
|
||||
organizationUnit?: string
|
||||
meetingTimeRange?: any[]
|
||||
materialViewTimeRange?: any[]
|
||||
minutesAiStatus?: number
|
||||
minutesAiStatusName?: string
|
||||
minutesAiErrorMessage?: string
|
||||
|
|
@ -299,12 +320,8 @@ const formData = reactive<FormData>({
|
|||
minutesAiStatusName: undefined,
|
||||
minutesAiErrorMessage: undefined,
|
||||
minutesAiUpdatedTime: undefined,
|
||||
materialViewStartTime: undefined,
|
||||
materialViewEndTime: undefined,
|
||||
materialViewRemark: undefined,
|
||||
expertIds: [],
|
||||
meetingTimeRange: undefined,
|
||||
materialViewTimeRange: undefined,
|
||||
projects: []
|
||||
})
|
||||
|
||||
|
|
@ -315,7 +332,9 @@ const rules: FormRules = {
|
|||
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
|
||||
host: [{ required: true, message: '会议主持人不能为空', trigger: 'blur' }],
|
||||
agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }],
|
||||
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
|
||||
expertIds: [
|
||||
{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
|
|
@ -334,8 +353,13 @@ const resetProjectReviewDate = (projects: MeetingEditProjectItem[]): MeetingEdit
|
|||
reviewDate: undefined
|
||||
}))
|
||||
|
||||
const buildPreviewScheduledProjects = (projects: MeetingEditProjectItem[] = formData.projects): MeetingEditProjectItem[] => {
|
||||
const projectsWithReviewDate = applyDefaultReviewDate(projects, formData.meetingTimeRange) as MeetingEditProjectItem[]
|
||||
const buildPreviewScheduledProjects = (
|
||||
projects: MeetingEditProjectItem[] = formData.projects
|
||||
): MeetingEditProjectItem[] => {
|
||||
const projectsWithReviewDate = applyDefaultReviewDate(
|
||||
projects,
|
||||
formData.meetingTimeRange
|
||||
) as MeetingEditProjectItem[]
|
||||
return (
|
||||
buildScheduledProjectItems(
|
||||
projectsWithReviewDate,
|
||||
|
|
@ -363,12 +387,6 @@ const loadDetail = async (id: number) => {
|
|||
new Date(detail.endTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
}
|
||||
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||
formData.materialViewTimeRange = [
|
||||
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
}
|
||||
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
|
|
@ -393,9 +411,6 @@ const loadCopySource = async (id: number) => {
|
|||
formData.organizationUnit = detail.organizationUnit
|
||||
formData.startTime = detail.startTime
|
||||
formData.endTime = detail.endTime
|
||||
formData.materialViewStartTime = detail.materialViewStartTime
|
||||
formData.materialViewEndTime = detail.materialViewEndTime
|
||||
formData.materialViewRemark = detail.materialViewRemark
|
||||
formData.location = detail.location || '东5楼326'
|
||||
formData.host = detail.host
|
||||
formData.agendaAttachmentName = undefined
|
||||
|
|
@ -422,17 +437,11 @@ const loadCopySource = async (id: number) => {
|
|||
} else {
|
||||
formData.meetingTimeRange = undefined
|
||||
}
|
||||
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||
formData.materialViewTimeRange = [
|
||||
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
} else {
|
||||
formData.materialViewTimeRange = undefined
|
||||
}
|
||||
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||
|
||||
ElMessage.info('已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制')
|
||||
ElMessage.info(
|
||||
'已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制'
|
||||
)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
|
|
@ -462,12 +471,17 @@ watch(
|
|||
const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) return
|
||||
if (formData.projects && formData.projects.length > 0) {
|
||||
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', { type: 'warning' })
|
||||
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const result = await importProjectsFromExcel(uploadFile.raw)
|
||||
const projects = applyDefaultReviewDate(result as ReviewProjectItemVO[], formData.meetingTimeRange)
|
||||
const projects = applyDefaultReviewDate(
|
||||
result as ReviewProjectItemVO[],
|
||||
formData.meetingTimeRange
|
||||
)
|
||||
formData.projects = (buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) ||
|
||||
projects) as MeetingEditProjectItem[]
|
||||
isProjectsModified.value = true
|
||||
|
|
@ -584,7 +598,10 @@ const formatFileSize = (bytes?: number): string => {
|
|||
}
|
||||
|
||||
const buildScheduledProjects = (): MeetingEditProjectItem[] => {
|
||||
const projectsWithReviewDate = applyDefaultReviewDate(formData.projects, formData.meetingTimeRange) as MeetingEditProjectItem[]
|
||||
const projectsWithReviewDate = applyDefaultReviewDate(
|
||||
formData.projects,
|
||||
formData.meetingTimeRange
|
||||
) as MeetingEditProjectItem[]
|
||||
const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime)
|
||||
if (hasCompleteSchedule) {
|
||||
return projectsWithReviewDate
|
||||
|
|
@ -671,13 +688,6 @@ const submitForm = async () => {
|
|||
formData.startTime = formData.meetingTimeRange[0]
|
||||
formData.endTime = formData.meetingTimeRange[1]
|
||||
}
|
||||
if (formData.materialViewTimeRange?.length === 2) {
|
||||
formData.materialViewStartTime = formData.materialViewTimeRange[0]
|
||||
formData.materialViewEndTime = formData.materialViewTimeRange[1]
|
||||
} else {
|
||||
formData.materialViewStartTime = undefined
|
||||
formData.materialViewEndTime = undefined
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const projects = buildScheduledProjects()
|
||||
|
|
@ -739,7 +749,9 @@ const handleBack = () => {
|
|||
line-height: 1;
|
||||
font-weight: 400;
|
||||
}
|
||||
.back-btn:hover { opacity: 0.75; }
|
||||
.back-btn:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
|
|
@ -850,7 +862,9 @@ const handleBack = () => {
|
|||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-upload:hover { background-color: rgba(41, 90, 188, 0.08); }
|
||||
.btn-upload:hover {
|
||||
background-color: rgba(41, 90, 188, 0.08);
|
||||
}
|
||||
|
||||
.btn-generate {
|
||||
display: inline-flex;
|
||||
|
|
@ -905,8 +919,13 @@ const handleBack = () => {
|
|||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn-primary:hover { background-color: rgba(41, 90, 188, 0.88); }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-primary:hover {
|
||||
background-color: rgba(41, 90, 188, 0.88);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── 底部 ── */
|
||||
.form-footer {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
<template>
|
||||
<el-dialog v-model="visible" :title="dialogTitle" width="860px" :close-on-click-modal="false" top="5vh">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="90px" v-loading="formLoading">
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="860px"
|
||||
:close-on-click-modal="false"
|
||||
top="5vh"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="90px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
|
|
@ -51,31 +63,20 @@
|
|||
<el-link type="primary" :underline="false" @click="previewAgendaAttachment">
|
||||
{{ formData.agendaAttachmentName }}
|
||||
</el-link>
|
||||
<el-tag size="small">{{ (formData.agendaAttachmentType || '').toUpperCase() }}</el-tag>
|
||||
<el-text type="info" size="small">{{ formatFileSize(formData.agendaAttachmentSize) }}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment">移除</el-button>
|
||||
<el-tag size="small">{{
|
||||
(formData.agendaAttachmentType || '').toUpperCase()
|
||||
}}</el-tag>
|
||||
<el-text type="info" size="small">{{
|
||||
formatFileSize(formData.agendaAttachmentSize)
|
||||
}}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
|
||||
<el-date-picker
|
||||
v-model="formData.materialViewTimeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="x"
|
||||
:disabled="isView"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="参会专家" prop="expertIds">
|
||||
|
|
@ -110,7 +111,9 @@
|
|||
<el-button type="primary" plain>导入验收申请 Excel</el-button>
|
||||
</el-upload>
|
||||
<el-button type="success" plain @click="handleDownloadTemplate">下载导入模板</el-button>
|
||||
<el-text type="info" size="small" class="ml-10">格式:序号、议程分类、项目标题、汇报人、报告人单位</el-text>
|
||||
<el-text type="info" size="small" class="ml-10"
|
||||
>格式:序号、议程分类、项目标题、汇报人、报告人单位</el-text
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 评审项目预览列表 -->
|
||||
|
|
@ -168,12 +171,15 @@ const formLoading = ref(false)
|
|||
const formType = ref<'create' | 'update' | 'view'>('create')
|
||||
const isView = computed(() => formType.value === 'view')
|
||||
const dialogTitle = computed(() =>
|
||||
formType.value === 'create' ? '新增会议邀约' : formType.value === 'update' ? '编辑评审会议' : '查看评审会议'
|
||||
formType.value === 'create'
|
||||
? '新增会议邀约'
|
||||
: formType.value === 'update'
|
||||
? '编辑评审会议'
|
||||
: '查看评审会议'
|
||||
)
|
||||
|
||||
type FormData = ReviewMeetingSaveReqVO & {
|
||||
meetingTimeRange?: any[]
|
||||
materialViewTimeRange?: any[]
|
||||
}
|
||||
|
||||
const formData = reactive<FormData>({
|
||||
|
|
@ -186,12 +192,8 @@ const formData = reactive<FormData>({
|
|||
agendaAttachmentUrl: undefined,
|
||||
agendaAttachmentType: undefined,
|
||||
agendaAttachmentSize: undefined,
|
||||
materialViewStartTime: undefined,
|
||||
materialViewEndTime: undefined,
|
||||
materialViewRemark: undefined,
|
||||
expertIds: [],
|
||||
meetingTimeRange: undefined,
|
||||
materialViewTimeRange: undefined,
|
||||
projects: []
|
||||
})
|
||||
|
||||
|
|
@ -199,13 +201,16 @@ const rules: FormRules = {
|
|||
name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }],
|
||||
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
|
||||
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
|
||||
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
|
||||
expertIds: [
|
||||
{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const isProjectsModified = ref(false)
|
||||
const formRef = ref()
|
||||
const expertOptions = ref<any[]>([])
|
||||
const expertLabel = (e: any) => `${e.nickname}${e.title ? `(${e.title})` : ''}${e.deptName ? ` ${e.deptName}` : ''}`
|
||||
const expertLabel = (e: any) =>
|
||||
`${e.nickname}${e.title ? `(${e.title})` : ''}${e.deptName ? ` ${e.deptName}` : ''}`
|
||||
|
||||
const open = async (type: 'create' | 'update' | 'view', id?: number) => {
|
||||
formType.value = type
|
||||
|
|
@ -225,12 +230,6 @@ const open = async (type: 'create' | 'update' | 'view', id?: number) => {
|
|||
new Date(detail.endTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
}
|
||||
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||
formData.materialViewTimeRange = [
|
||||
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
|
|
@ -247,12 +246,8 @@ const resetForm = () => {
|
|||
formData.agendaAttachmentUrl = undefined
|
||||
formData.agendaAttachmentType = undefined
|
||||
formData.agendaAttachmentSize = undefined
|
||||
formData.materialViewStartTime = undefined
|
||||
formData.materialViewEndTime = undefined
|
||||
formData.materialViewRemark = undefined
|
||||
formData.expertIds = []
|
||||
formData.meetingTimeRange = undefined
|
||||
formData.materialViewTimeRange = undefined
|
||||
formData.projects = []
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
|
@ -260,7 +255,9 @@ const resetForm = () => {
|
|||
const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) return
|
||||
if (formData.projects && formData.projects.length > 0) {
|
||||
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', { type: 'warning' })
|
||||
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
|
|
@ -269,7 +266,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
|||
((result as any).data || result) as ReviewProjectItemVO[],
|
||||
formData.meetingTimeRange
|
||||
)
|
||||
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
|
||||
formData.projects =
|
||||
buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
|
||||
isProjectsModified.value = true
|
||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||
} catch (e) {
|
||||
|
|
@ -339,24 +337,14 @@ const submitForm = async () => {
|
|||
formData.startTime = formData.meetingTimeRange[0]
|
||||
formData.endTime = formData.meetingTimeRange[1]
|
||||
}
|
||||
if (formData.materialViewTimeRange?.length === 2) {
|
||||
formData.materialViewStartTime = formData.materialViewTimeRange[0]
|
||||
formData.materialViewEndTime = formData.materialViewTimeRange[1]
|
||||
} else if (formData.meetingTimeRange?.length === 2) {
|
||||
// 默认与会议时间一致,便于管理员快速配置
|
||||
formData.materialViewStartTime = formData.meetingTimeRange[0]
|
||||
formData.materialViewEndTime = formData.meetingTimeRange[1]
|
||||
} else {
|
||||
formData.materialViewStartTime = undefined
|
||||
formData.materialViewEndTime = undefined
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const projects = buildScheduledProjectItems(
|
||||
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
|
||||
formData.meetingTimeRange?.[0],
|
||||
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
|
||||
const projects =
|
||||
buildScheduledProjectItems(
|
||||
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
|
||||
formData.meetingTimeRange?.[0],
|
||||
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
|
||||
const submitData = {
|
||||
...formData,
|
||||
projects
|
||||
|
|
@ -382,9 +370,26 @@ defineExpose({ open })
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-section { margin-top: 8px; display: flex; align-items: center; gap: 8px; }
|
||||
.ml-10 { margin-left: 10px; }
|
||||
.mt-10 { margin-top: 10px; }
|
||||
.agenda-attachment-wrap { display: flex; flex-direction: column; gap: 6px; }
|
||||
.agenda-file-line { display: flex; align-items: center; gap: 8px; }
|
||||
.import-section {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.ml-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.agenda-attachment-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.agenda-file-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,19 @@
|
|||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="search-datepicker"
|
||||
/>
|
||||
<el-select v-model="queryParams.status" size="large" placeholder="会议状态" clearable class="search-select">
|
||||
<el-option v-for="item in MEETING_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
size="large"
|
||||
placeholder="会议状态"
|
||||
clearable
|
||||
class="search-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in MEETING_STATUS_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<button class="btn-reset" @click="resetQuery">重置</button>
|
||||
<button class="btn-search" @click="handleQuery">查询</button>
|
||||
|
|
@ -35,6 +46,14 @@
|
|||
<button v-hasPermi="['review:meeting:create']" class="btn-default" @click="goToEdit()">
|
||||
<span class="btn-icon">+</span> 新建会议
|
||||
</button>
|
||||
<button
|
||||
v-hasPermi="['review:meeting:update']"
|
||||
class="btn-default"
|
||||
:disabled="!selectedTabletMeetingId || settingTabletActive"
|
||||
@click="handleClearTabletActive"
|
||||
>
|
||||
清空平板评审会议
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
|
|
@ -54,27 +73,77 @@
|
|||
<el-table-column label="会议地点" prop="location" width="184" show-overflow-tooltip />
|
||||
<el-table-column label="参会专家数" prop="expertCount" width="112" align="center" />
|
||||
<el-table-column label="项目数" prop="projectCount" width="96" align="center" />
|
||||
<el-table-column label="平板评审会议" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-radio
|
||||
class="tablet-active-radio"
|
||||
:model-value="selectedTabletMeetingId"
|
||||
:label="row.id"
|
||||
:disabled="row.status !== 1 || settingTabletActive"
|
||||
@change="() => handleSetTabletActive(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="116" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="600" align="center" :fixed="useFixedActionColumn ? 'right' : false">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="600"
|
||||
align="center"
|
||||
:fixed="useFixedActionColumn ? 'right' : false"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 0">
|
||||
<div class="op-group">
|
||||
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="goToEdit(row.id)">编辑</a>
|
||||
<a v-hasPermi="['review:meeting:send-sms']" class="op-link" @click="handleSendSms(row)">发送短信邀约</a>
|
||||
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)">取消</a>
|
||||
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="goToEdit(row.id)"
|
||||
>编辑</a
|
||||
>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:send-sms']"
|
||||
class="op-link"
|
||||
@click="handleSendSms(row)"
|
||||
>发送短信邀约</a
|
||||
>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:cancel']"
|
||||
class="op-link op-danger"
|
||||
@click="handleCancel(row)"
|
||||
>取消</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="row.status === 1">
|
||||
<div class="op-group">
|
||||
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a>
|
||||
<a v-hasPermi="['review:meeting:send-sms']" class="op-link" @click="openSmsStatus(row)">短信状态</a>
|
||||
<a v-if="row.mailSent" v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="openMailStatus(row)">邮件状态</a>
|
||||
<a v-else v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="handleSendMail(row)">发送议程</a>
|
||||
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="triggerUploadMinutes(row)">上传纪要</a>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:send-sms']"
|
||||
class="op-link"
|
||||
@click="openSmsStatus(row)"
|
||||
>短信状态</a
|
||||
>
|
||||
<a
|
||||
v-if="row.mailSent"
|
||||
v-hasPermi="['review:meeting:send-mail']"
|
||||
class="op-link"
|
||||
@click="openMailStatus(row)"
|
||||
>邮件状态</a
|
||||
>
|
||||
<a
|
||||
v-else
|
||||
v-hasPermi="['review:meeting:send-mail']"
|
||||
class="op-link"
|
||||
@click="handleSendMail(row)"
|
||||
>发送议程</a
|
||||
>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:update']"
|
||||
class="op-link"
|
||||
@click="triggerUploadMinutes(row)"
|
||||
>上传纪要</a
|
||||
>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:export']"
|
||||
class="op-link"
|
||||
|
|
@ -83,14 +152,23 @@
|
|||
>
|
||||
{{ getExportText(row.id) }}
|
||||
</a>
|
||||
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)">结束</a>
|
||||
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)">取消</a>
|
||||
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)"
|
||||
>结束</a
|
||||
>
|
||||
<a
|
||||
v-hasPermi="['review:meeting:cancel']"
|
||||
class="op-link op-danger"
|
||||
@click="handleCancel(row)"
|
||||
>取消</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="op-group">
|
||||
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a>
|
||||
<a v-hasPermi="['review:meeting:create']" class="op-link" @click="handleCopy(row)">复制新会议</a>
|
||||
<a v-hasPermi="['review:meeting:create']" class="op-link" @click="handleCopy(row)"
|
||||
>复制新会议</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -98,7 +176,12 @@
|
|||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 短信状态弹窗 -->
|
||||
<SmsStatusDialog ref="smsStatusRef" />
|
||||
|
|
@ -121,9 +204,11 @@ import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import {
|
||||
clearTabletActiveMeeting,
|
||||
getReviewMeetingPage,
|
||||
cancelReviewMeeting,
|
||||
finishReviewMeeting,
|
||||
setTabletActiveMeeting,
|
||||
sendSmsInvitation,
|
||||
sendMailInvitation,
|
||||
uploadMinutesAttachment,
|
||||
|
|
@ -146,6 +231,8 @@ const loading = ref(false)
|
|||
const list = ref<ReviewMeetingRespVO[]>([])
|
||||
const total = ref(0)
|
||||
const useFixedActionColumn = ref(true)
|
||||
const selectedTabletMeetingId = ref<number>()
|
||||
const settingTabletActive = ref(false)
|
||||
|
||||
const MEETING_STATUS_OPTIONS = [
|
||||
{ value: 0, label: '待召开' },
|
||||
|
|
@ -153,7 +240,12 @@ const MEETING_STATUS_OPTIONS = [
|
|||
{ value: 2, label: '已结束' },
|
||||
{ value: 3, label: '已取消' }
|
||||
]
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||
const STATUS_LABEL: Record<number, string> = {
|
||||
0: '待召开',
|
||||
1: '正在召开',
|
||||
2: '已结束',
|
||||
3: '已取消'
|
||||
}
|
||||
|
||||
const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize: number }>({
|
||||
pageNo: 1,
|
||||
|
|
@ -167,7 +259,9 @@ const smsStatusRef = ref()
|
|||
const mailStatusRef = ref()
|
||||
const minutesUploadInputRef = ref<HTMLInputElement>()
|
||||
const pendingMinutesMeeting = ref<ReviewMeetingRespVO>()
|
||||
const exportTaskMap = reactive<Record<number, ReviewMeetingMaterialExportTaskRespVO | undefined>>({})
|
||||
const exportTaskMap = reactive<Record<number, ReviewMeetingMaterialExportTaskRespVO | undefined>>(
|
||||
{}
|
||||
)
|
||||
const exportPollingMap = new Map<number, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const syncActionColumnMode = () => {
|
||||
|
|
@ -180,12 +274,16 @@ const getList = async () => {
|
|||
const data = await getReviewMeetingPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
selectedTabletMeetingId.value = data.list.find((item) => item.tabletActive)?.id
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
const resetQuery = () => {
|
||||
queryParams.name = undefined
|
||||
queryParams.status = undefined
|
||||
|
|
@ -202,7 +300,11 @@ const goToEdit = (id?: number, mode?: string) => {
|
|||
}
|
||||
|
||||
const goToProjectList = (row: ReviewMeetingRespVO) => {
|
||||
router.push({ name: 'ReviewMeetingProject', params: { meetingId: row.id }, query: { meetingName: row.name } })
|
||||
router.push({
|
||||
name: 'ReviewMeetingProject',
|
||||
params: { meetingId: row.id },
|
||||
query: { meetingName: row.name }
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = async (row: ReviewMeetingRespVO) => {
|
||||
|
|
@ -219,6 +321,34 @@ const handleFinish = async (row: ReviewMeetingRespVO) => {
|
|||
getList()
|
||||
}
|
||||
|
||||
const handleSetTabletActive = async (row: ReviewMeetingRespVO) => {
|
||||
if (settingTabletActive.value || row.status !== 1 || selectedTabletMeetingId.value === row.id) {
|
||||
return
|
||||
}
|
||||
settingTabletActive.value = true
|
||||
try {
|
||||
await setTabletActiveMeeting(row.id)
|
||||
ElMessage.success('已设置当前平板评审会议')
|
||||
await getList()
|
||||
} finally {
|
||||
settingTabletActive.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearTabletActive = async () => {
|
||||
if (!selectedTabletMeetingId.value || settingTabletActive.value) {
|
||||
return
|
||||
}
|
||||
settingTabletActive.value = true
|
||||
try {
|
||||
await clearTabletActiveMeeting()
|
||||
ElMessage.success('已清空当前平板评审会议')
|
||||
await getList()
|
||||
} finally {
|
||||
settingTabletActive.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopy = async (row: ReviewMeetingRespVO) => {
|
||||
router.push({
|
||||
name: 'ReviewMeetingEdit',
|
||||
|
|
@ -237,7 +367,9 @@ const handleMinutesFileChange = async (event: Event) => {
|
|||
if (!file || !pendingMinutesMeeting.value) {
|
||||
return
|
||||
}
|
||||
const ext = file.name.includes('.') ? file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase() : ''
|
||||
const ext = file.name.includes('.')
|
||||
? file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()
|
||||
: ''
|
||||
const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||
if (!allowed.includes(ext)) {
|
||||
ElMessage.error('会议纪要仅支持 Word、PDF 或图片')
|
||||
|
|
@ -517,10 +649,18 @@ onBeforeUnmount(() => {
|
|||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-0 { color: #ecae4b; }
|
||||
.status-1 { color: #73c047; }
|
||||
.status-2 { color: #999; }
|
||||
.status-3 { color: #999; }
|
||||
.status-0 {
|
||||
color: #ecae4b;
|
||||
}
|
||||
.status-1 {
|
||||
color: #73c047;
|
||||
}
|
||||
.status-2 {
|
||||
color: #999;
|
||||
}
|
||||
.status-3 {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* ── 会议名称链接 ── */
|
||||
.meeting-name-link {
|
||||
|
|
@ -549,7 +689,11 @@ onBeforeUnmount(() => {
|
|||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, border-color 0.2s, color 0.2s, opacity 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
border-color 0.2s,
|
||||
color 0.2s,
|
||||
opacity 0.2s;
|
||||
}
|
||||
.op-link:hover {
|
||||
background: rgba(41, 90, 188, 0.1);
|
||||
|
|
@ -643,4 +787,8 @@ onBeforeUnmount(() => {
|
|||
:deep(.review-table .el-table__fixed::before) {
|
||||
background-color: #e1e7f0;
|
||||
}
|
||||
|
||||
:deep(.tablet-active-radio .el-radio__label) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<div class="left-title">专家评审资料</div>
|
||||
<el-button text :icon="Refresh" @click="loadCatalog" />
|
||||
</div>
|
||||
<div class="left-sub">当日项目编目:{{ projectCount }}篇</div>
|
||||
<div class="left-sub">当前评审会议编目:{{ projectCount }}篇</div>
|
||||
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
|
|
@ -26,7 +26,10 @@
|
|||
/>
|
||||
|
||||
<div class="left-tree-wrap">
|
||||
<el-empty v-if="treeData.length === 0" description="当前时段暂无可查看资料" />
|
||||
<el-empty
|
||||
v-if="treeData.length === 0"
|
||||
description="管理员未选择当前评审会议,请联系管理员设置"
|
||||
/>
|
||||
<el-tree
|
||||
v-else
|
||||
ref="treeRef"
|
||||
|
|
@ -146,7 +149,15 @@
|
|||
</el-result>
|
||||
|
||||
<template v-else-if="previewUrl">
|
||||
<TabletPdfViewer
|
||||
v-if="previewMode === 'pdf'"
|
||||
:key="`pdf-${previewFrameKey}`"
|
||||
:src="previewUrl"
|
||||
@loading-change="handlePdfLoadingChange"
|
||||
@error="handlePdfRenderError"
|
||||
/>
|
||||
<iframe
|
||||
v-else
|
||||
:key="previewFrameKey"
|
||||
:src="previewUrl"
|
||||
class="preview-iframe"
|
||||
|
|
@ -155,7 +166,10 @@
|
|||
@load="handlePreviewFrameLoad"
|
||||
@error="handlePreviewFrameError"
|
||||
></iframe>
|
||||
<div v-if="previewFrameLoading && !previewError" class="preview-loading-mask">
|
||||
<div
|
||||
v-if="previewMode !== 'pdf' && previewFrameLoading && !previewError"
|
||||
class="preview-loading-mask"
|
||||
>
|
||||
<div class="welcome-title">{{ previewLoadingTitle }}</div>
|
||||
<div class="welcome-sub">{{ previewLoadingSubtitle }}</div>
|
||||
</div>
|
||||
|
|
@ -426,10 +440,12 @@ import {
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import TabletPdfViewer from './TabletPdfViewer.vue'
|
||||
import { resolvePdfSourceUrl, resolveTabletPreviewMode } from './previewMode.mjs'
|
||||
import {
|
||||
getFileOpenUrl,
|
||||
getProjectFiles,
|
||||
getTodayCatalog,
|
||||
getActiveCatalog,
|
||||
type ReviewTabletCatalogVO,
|
||||
type ReviewTabletOpenUrlVO
|
||||
} from '@/api/review/tablet'
|
||||
|
|
@ -598,13 +614,20 @@ const activeFileDesc = computed(() => {
|
|||
return segs.join(' ')
|
||||
})
|
||||
const previewLoadingTitle = computed(() =>
|
||||
isOfficeFile(activeFileNode.value?.fileType) ? '正在转换文档预览' : '正在加载预览'
|
||||
previewMode.value === 'pdf'
|
||||
? '正在加载 PDF 预览'
|
||||
: isOfficeFile(activeFileNode.value?.fileType)
|
||||
? '正在转换文档预览'
|
||||
: '正在加载预览'
|
||||
)
|
||||
const previewLoadingSubtitle = computed(() =>
|
||||
isOfficeFile(activeFileNode.value?.fileType)
|
||||
? 'Office 文件首次打开可能需要一些时间,请稍候'
|
||||
: '正在为您打开当前资料'
|
||||
previewMode.value === 'pdf'
|
||||
? '正在渲染文档内容,请稍候'
|
||||
: isOfficeFile(activeFileNode.value?.fileType)
|
||||
? 'Office 文件首次打开可能需要一些时间,请稍候'
|
||||
: '正在为您打开当前资料'
|
||||
)
|
||||
const previewMode = computed(() => resolveTabletPreviewMode(previewPayload.value))
|
||||
const isTouchTablet = computed(() => coarsePointer.value)
|
||||
const isCompactTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1400)
|
||||
const isDenseTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1180)
|
||||
|
|
@ -763,14 +786,22 @@ const loadPreview = async (fileId: number) => {
|
|||
const payload = await getFileOpenUrl(fileId)
|
||||
if (requestToken !== previewRequestToken.value) return
|
||||
previewPayload.value = payload
|
||||
if (!payload?.openUrl) {
|
||||
const mode = resolveTabletPreviewMode(payload)
|
||||
const sourceUrl = resolvePdfSourceUrl(payload, payload.openUrl)
|
||||
if (!sourceUrl) {
|
||||
previewUrl.value = ''
|
||||
previewFrameLoading.value = false
|
||||
previewError.value = '未获取到预览地址'
|
||||
return
|
||||
}
|
||||
previewUrl.value = payload.openUrl
|
||||
startPreviewFrameTimeout(requestToken)
|
||||
previewUrl.value = sourceUrl
|
||||
if (mode === 'iframe') {
|
||||
startPreviewFrameTimeout(requestToken)
|
||||
} else {
|
||||
clearPreviewFrameTimeout()
|
||||
// PDF 改为组件内部加载态,避免父层遮罩在部分平板浏览器上卡住
|
||||
previewFrameLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
if (requestToken !== previewRequestToken.value) return
|
||||
previewUrl.value = ''
|
||||
|
|
@ -791,7 +822,7 @@ const selectFileNode = async (fileNode: FileTreeNode) => {
|
|||
}
|
||||
|
||||
const openInNewWindow = () => {
|
||||
const url = previewPayload.value?.openUrl || previewUrl.value
|
||||
const url = previewUrl.value || previewPayload.value?.openUrl
|
||||
if (!url) {
|
||||
ElMessage.warning('暂无可打开的预览地址')
|
||||
return
|
||||
|
|
@ -814,6 +845,18 @@ const handlePreviewFrameError = () => {
|
|||
previewError.value = '预览内容加载失败,请稍后重试'
|
||||
}
|
||||
|
||||
const handlePdfLoadingChange = (loading: boolean) => {
|
||||
if (previewMode.value !== 'pdf') return
|
||||
previewFrameLoading.value = loading
|
||||
if (!loading) clearPreviewFrameTimeout()
|
||||
}
|
||||
|
||||
const handlePdfRenderError = (message: string) => {
|
||||
if (previewMode.value !== 'pdf') return
|
||||
previewFrameLoading.value = false
|
||||
previewError.value = message || ''
|
||||
}
|
||||
|
||||
const handleNodeExpand = async (data: TreeNode) => {
|
||||
if (data.type !== 'project') return
|
||||
if (!expandedKeys.value.includes(data.id)) expandedKeys.value.push(data.id)
|
||||
|
|
@ -876,7 +919,7 @@ const loadCatalog = async () => {
|
|||
aiProjectCache.value = {}
|
||||
resetProjectAiState()
|
||||
try {
|
||||
const catalog = await getTodayCatalog()
|
||||
const catalog = await getActiveCatalog()
|
||||
treeData.value = catalog
|
||||
.sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0))
|
||||
.map((item) => buildProjectNode(item))
|
||||
|
|
|
|||
Loading…
Reference in New Issue