feat(review-meeting): support active tablet meeting selection

pull/874/head
Codewoc 2026-04-07 09:16:11 +08:00
parent 823e50e52f
commit 25eef30456
6 changed files with 404 additions and 178 deletions

View File

@ -26,9 +26,6 @@ export interface ReviewMeetingSaveReqVO {
organizationUnit?: string organizationUnit?: string
startTime?: string | number startTime?: string | number
endTime?: string | number endTime?: string | number
materialViewStartTime?: string | number
materialViewEndTime?: string | number
materialViewRemark?: string
location: string location: string
host?: string host?: string
agendaAttachmentName?: string agendaAttachmentName?: string
@ -59,9 +56,6 @@ export interface ReviewMeetingRespVO {
organizationUnit?: string organizationUnit?: string
startTime: string startTime: string
endTime: string endTime: string
materialViewStartTime?: string
materialViewEndTime?: string
materialViewRemark?: string
location: string location: string
host?: string host?: string
agendaAttachmentName?: string agendaAttachmentName?: string
@ -81,6 +75,7 @@ export interface ReviewMeetingRespVO {
expertCount: number expertCount: number
projectCount: number projectCount: number
mailSent?: boolean mailSent?: boolean
tabletActive?: boolean
createTime: string createTime: string
} }
@ -159,6 +154,14 @@ export const cancelReviewMeeting = (id: number) =>
export const finishReviewMeeting = (id: number) => export const finishReviewMeeting = (id: number) =>
request.put({ url: '/project/review-meeting/finish', params: { id } }) 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) => export const copyReviewMeeting = (id: number) =>
request.post({ url: '/project/review-meeting/copy', params: { id } }) 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[]> => { export const importProjectsFromExcel = async (file: File): Promise<ReviewProjectItemVO[]> => {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) 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 || [] return res?.data || []
} }
@ -212,7 +218,10 @@ export const uploadAgendaAttachment = async (
/** 根据当前表单内容自动生成议程附件 */ /** 根据当前表单内容自动生成议程附件 */
export const generateAgendaAttachment = (data: ReviewMeetingAgendaGenerateReqVO) => 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/图片) */ /** 上传会议纪要附件Word/PDF/图片) */
export const uploadMinutesAttachment = ( export const uploadMinutesAttachment = (
@ -283,5 +292,7 @@ const uploadMeetingAttachmentByPresignedUrl = async (
} }
const resolveAttachmentType = (fileName: string): string => { const resolveAttachmentType = (fileName: string): string => {
return fileName.includes('.') ? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase() : '' return fileName.includes('.')
? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
: ''
} }

View File

@ -32,7 +32,7 @@ export interface ReviewTabletOpenUrlVO {
visitUrl: string 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) => export const getProjectFiles = (reviewMeetingProjectId: number) =>
request.get({ request.get({

View File

@ -22,7 +22,11 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="组织单位" prop="organizationUnit"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -54,7 +58,12 @@
:disabled="isView" :disabled="isView"
style="width: 100%" 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-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -65,21 +74,6 @@
<el-input v-model="formData.host" placeholder="请输入会议主持人" :disabled="isView" /> <el-input v-model="formData.host" placeholder="请输入会议主持人" :disabled="isView" />
</el-form-item> </el-form-item>
</el-col> </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-col :span="12">
<el-form-item label="议程附件" prop="agendaAttachmentUrl"> <el-form-item label="议程附件" prop="agendaAttachmentUrl">
<div class="agenda-attachment-wrap"> <div class="agenda-attachment-wrap">
@ -92,7 +86,12 @@
> >
<button type="button" class="btn-upload">上传议程附件</button> <button type="button" class="btn-upload">上传议程附件</button>
</el-upload> </el-upload>
<button type="button" class="btn-generate" :disabled="formLoading" @click="handleGenerateAgenda"> <button
type="button"
class="btn-generate"
:disabled="formLoading"
@click="handleGenerateAgenda"
>
自动生成议程 自动生成议程
</button> </button>
</div> </div>
@ -101,9 +100,15 @@
<el-link type="primary" :underline="false" @click="previewAgendaAttachment"> <el-link type="primary" :underline="false" @click="previewAgendaAttachment">
{{ formData.agendaAttachmentName }} {{ formData.agendaAttachmentName }}
</el-link> </el-link>
<el-tag size="small">{{ (formData.agendaAttachmentType || '').toUpperCase() }}</el-tag> <el-tag size="small">{{
<el-text type="info" size="small">{{ formatFileSize(formData.agendaAttachmentSize) }}</el-text> (formData.agendaAttachmentType || '').toUpperCase()
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment"></el-button> }}</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>
</div> </div>
</el-form-item> </el-form-item>
@ -126,11 +131,20 @@
<el-link type="primary" :underline="false" @click="previewMinutesAttachment"> <el-link type="primary" :underline="false" @click="previewMinutesAttachment">
{{ formData.minutesAttachmentName }} {{ formData.minutesAttachmentName }}
</el-link> </el-link>
<el-tag size="small">{{ (formData.minutesAttachmentType || '').toUpperCase() }}</el-tag> <el-tag size="small">{{
<el-text type="info" size="small">{{ formatFileSize(formData.minutesAttachmentSize) }}</el-text> (formData.minutesAttachmentType || '').toUpperCase()
<el-button v-if="!isView" type="danger" link @click="clearMinutesAttachment"></el-button> }}</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>
<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)"> <el-tag size="small" :type="getMinutesAiTagType(formData.minutesAiStatus)">
{{ formData.minutesAiStatusName }} {{ formData.minutesAiStatusName }}
</el-tag> </el-tag>
@ -171,7 +185,9 @@
> >
<button type="button" class="btn-default">导入验收申请 Excel</button> <button type="button" class="btn-default">导入验收申请 Excel</button>
</el-upload> </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> <span class="import-hint">格式序号议程分类项目标题汇报人报告人单位</span>
</div> </div>
@ -194,7 +210,13 @@
<!-- 底部操作区 --> <!-- 底部操作区 -->
<div class="form-footer"> <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 ? '保存中...' : '保存草稿' }} {{ formLoading ? '保存中...' : '保存草稿' }}
</button> </button>
<button type="button" class="btn-default" @click="handleBack"></button> <button type="button" class="btn-default" @click="handleBack"></button>
@ -271,7 +293,6 @@ const pageTitle = computed(() => {
type FormData = ReviewMeetingSaveReqVO & { type FormData = ReviewMeetingSaveReqVO & {
organizationUnit?: string organizationUnit?: string
meetingTimeRange?: any[] meetingTimeRange?: any[]
materialViewTimeRange?: any[]
minutesAiStatus?: number minutesAiStatus?: number
minutesAiStatusName?: string minutesAiStatusName?: string
minutesAiErrorMessage?: string minutesAiErrorMessage?: string
@ -299,12 +320,8 @@ const formData = reactive<FormData>({
minutesAiStatusName: undefined, minutesAiStatusName: undefined,
minutesAiErrorMessage: undefined, minutesAiErrorMessage: undefined,
minutesAiUpdatedTime: undefined, minutesAiUpdatedTime: undefined,
materialViewStartTime: undefined,
materialViewEndTime: undefined,
materialViewRemark: undefined,
expertIds: [], expertIds: [],
meetingTimeRange: undefined, meetingTimeRange: undefined,
materialViewTimeRange: undefined,
projects: [] projects: []
}) })
@ -315,7 +332,9 @@ const rules: FormRules = {
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }], location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
host: [{ required: true, message: '会议主持人不能为空', trigger: 'blur' }], host: [{ required: true, message: '会议主持人不能为空', trigger: 'blur' }],
agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }], 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() const formRef = ref()
@ -334,8 +353,13 @@ const resetProjectReviewDate = (projects: MeetingEditProjectItem[]): MeetingEdit
reviewDate: undefined reviewDate: undefined
})) }))
const buildPreviewScheduledProjects = (projects: MeetingEditProjectItem[] = formData.projects): MeetingEditProjectItem[] => { const buildPreviewScheduledProjects = (
const projectsWithReviewDate = applyDefaultReviewDate(projects, formData.meetingTimeRange) as MeetingEditProjectItem[] projects: MeetingEditProjectItem[] = formData.projects
): MeetingEditProjectItem[] => {
const projectsWithReviewDate = applyDefaultReviewDate(
projects,
formData.meetingTimeRange
) as MeetingEditProjectItem[]
return ( return (
buildScheduledProjectItems( buildScheduledProjectItems(
projectsWithReviewDate, projectsWithReviewDate,
@ -363,12 +387,6 @@ const loadDetail = async (id: number) => {
new Date(detail.endTime.replace(' ', 'T')).getTime() 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] originalMeetingStart.value = formData.meetingTimeRange?.[0]
} finally { } finally {
formLoading.value = false formLoading.value = false
@ -393,9 +411,6 @@ const loadCopySource = async (id: number) => {
formData.organizationUnit = detail.organizationUnit formData.organizationUnit = detail.organizationUnit
formData.startTime = detail.startTime formData.startTime = detail.startTime
formData.endTime = detail.endTime formData.endTime = detail.endTime
formData.materialViewStartTime = detail.materialViewStartTime
formData.materialViewEndTime = detail.materialViewEndTime
formData.materialViewRemark = detail.materialViewRemark
formData.location = detail.location || '东5楼326' formData.location = detail.location || '东5楼326'
formData.host = detail.host formData.host = detail.host
formData.agendaAttachmentName = undefined formData.agendaAttachmentName = undefined
@ -422,17 +437,11 @@ const loadCopySource = async (id: number) => {
} else { } else {
formData.meetingTimeRange = undefined 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] originalMeetingStart.value = formData.meetingTimeRange?.[0]
ElMessage.info('已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制') ElMessage.info(
'已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制'
)
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -462,12 +471,17 @@ watch(
const handleExcelChange = async (uploadFile: UploadFile) => { const handleExcelChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return if (!uploadFile.raw) return
if (formData.projects && formData.projects.length > 0) { if (formData.projects && formData.projects.length > 0) {
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', { type: 'warning' }) await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', {
type: 'warning'
})
} }
formLoading.value = true formLoading.value = true
try { try {
const result = await importProjectsFromExcel(uploadFile.raw) 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]) || formData.projects = (buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) ||
projects) as MeetingEditProjectItem[] projects) as MeetingEditProjectItem[]
isProjectsModified.value = true isProjectsModified.value = true
@ -584,7 +598,10 @@ const formatFileSize = (bytes?: number): string => {
} }
const buildScheduledProjects = (): MeetingEditProjectItem[] => { 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) const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime)
if (hasCompleteSchedule) { if (hasCompleteSchedule) {
return projectsWithReviewDate return projectsWithReviewDate
@ -671,13 +688,6 @@ const submitForm = async () => {
formData.startTime = formData.meetingTimeRange[0] formData.startTime = formData.meetingTimeRange[0]
formData.endTime = formData.meetingTimeRange[1] 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 formLoading.value = true
try { try {
const projects = buildScheduledProjects() const projects = buildScheduledProjects()
@ -739,7 +749,9 @@ const handleBack = () => {
line-height: 1; line-height: 1;
font-weight: 400; font-weight: 400;
} }
.back-btn:hover { opacity: 0.75; } .back-btn:hover {
opacity: 0.75;
}
.page-title { .page-title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
@ -850,7 +862,9 @@ const handleBack = () => {
cursor: pointer; cursor: pointer;
transition: all 0.2s; 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 { .btn-generate {
display: inline-flex; display: inline-flex;
@ -905,8 +919,13 @@ const handleBack = () => {
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.btn-primary:hover { background-color: rgba(41, 90, 188, 0.88); } .btn-primary:hover {
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } background-color: rgba(41, 90, 188, 0.88);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* ── 底部 ── */ /* ── 底部 ── */
.form-footer { .form-footer {

View File

@ -1,6 +1,18 @@
<template> <template>
<el-dialog v-model="visible" :title="dialogTitle" width="860px" :close-on-click-modal="false" top="5vh"> <el-dialog
<el-form ref="formRef" :model="formData" :rules="rules" label-width="90px" v-loading="formLoading"> 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-row :gutter="16">
<el-col :span="24"> <el-col :span="24">
@ -51,31 +63,20 @@
<el-link type="primary" :underline="false" @click="previewAgendaAttachment"> <el-link type="primary" :underline="false" @click="previewAgendaAttachment">
{{ formData.agendaAttachmentName }} {{ formData.agendaAttachmentName }}
</el-link> </el-link>
<el-tag size="small">{{ (formData.agendaAttachmentType || '').toUpperCase() }}</el-tag> <el-tag size="small">{{
<el-text type="info" size="small">{{ formatFileSize(formData.agendaAttachmentSize) }}</el-text> (formData.agendaAttachmentType || '').toUpperCase()
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment"></el-button> }}</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>
</div> </div>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </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-row :gutter="16">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="参会专家" prop="expertIds"> <el-form-item label="参会专家" prop="expertIds">
@ -110,7 +111,9 @@
<el-button type="primary" plain>导入验收申请 Excel</el-button> <el-button type="primary" plain>导入验收申请 Excel</el-button>
</el-upload> </el-upload>
<el-button type="success" plain @click="handleDownloadTemplate"></el-button> <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> </div>
<!-- 评审项目预览列表 --> <!-- 评审项目预览列表 -->
@ -168,12 +171,15 @@ const formLoading = ref(false)
const formType = ref<'create' | 'update' | 'view'>('create') const formType = ref<'create' | 'update' | 'view'>('create')
const isView = computed(() => formType.value === 'view') const isView = computed(() => formType.value === 'view')
const dialogTitle = computed(() => const dialogTitle = computed(() =>
formType.value === 'create' ? '新增会议邀约' : formType.value === 'update' ? '编辑评审会议' : '查看评审会议' formType.value === 'create'
? '新增会议邀约'
: formType.value === 'update'
? '编辑评审会议'
: '查看评审会议'
) )
type FormData = ReviewMeetingSaveReqVO & { type FormData = ReviewMeetingSaveReqVO & {
meetingTimeRange?: any[] meetingTimeRange?: any[]
materialViewTimeRange?: any[]
} }
const formData = reactive<FormData>({ const formData = reactive<FormData>({
@ -186,12 +192,8 @@ const formData = reactive<FormData>({
agendaAttachmentUrl: undefined, agendaAttachmentUrl: undefined,
agendaAttachmentType: undefined, agendaAttachmentType: undefined,
agendaAttachmentSize: undefined, agendaAttachmentSize: undefined,
materialViewStartTime: undefined,
materialViewEndTime: undefined,
materialViewRemark: undefined,
expertIds: [], expertIds: [],
meetingTimeRange: undefined, meetingTimeRange: undefined,
materialViewTimeRange: undefined,
projects: [] projects: []
}) })
@ -199,13 +201,16 @@ const rules: FormRules = {
name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }], name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }],
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }], meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }], 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 isProjectsModified = ref(false)
const formRef = ref() const formRef = ref()
const expertOptions = ref<any[]>([]) 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) => { const open = async (type: 'create' | 'update' | 'view', id?: number) => {
formType.value = type formType.value = type
@ -225,12 +230,6 @@ const open = async (type: 'create' | 'update' | 'view', id?: number) => {
new Date(detail.endTime.replace(' ', 'T')).getTime() 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 { } finally {
formLoading.value = false formLoading.value = false
} }
@ -247,12 +246,8 @@ const resetForm = () => {
formData.agendaAttachmentUrl = undefined formData.agendaAttachmentUrl = undefined
formData.agendaAttachmentType = undefined formData.agendaAttachmentType = undefined
formData.agendaAttachmentSize = undefined formData.agendaAttachmentSize = undefined
formData.materialViewStartTime = undefined
formData.materialViewEndTime = undefined
formData.materialViewRemark = undefined
formData.expertIds = [] formData.expertIds = []
formData.meetingTimeRange = undefined formData.meetingTimeRange = undefined
formData.materialViewTimeRange = undefined
formData.projects = [] formData.projects = []
formRef.value?.resetFields() formRef.value?.resetFields()
} }
@ -260,7 +255,9 @@ const resetForm = () => {
const handleExcelChange = async (uploadFile: UploadFile) => { const handleExcelChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return if (!uploadFile.raw) return
if (formData.projects && formData.projects.length > 0) { if (formData.projects && formData.projects.length > 0) {
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', { type: 'warning' }) await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', {
type: 'warning'
})
} }
formLoading.value = true formLoading.value = true
try { try {
@ -269,7 +266,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
((result as any).data || result) as ReviewProjectItemVO[], ((result as any).data || result) as ReviewProjectItemVO[],
formData.meetingTimeRange formData.meetingTimeRange
) )
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects formData.projects =
buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
isProjectsModified.value = true isProjectsModified.value = true
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`) ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
} catch (e) { } catch (e) {
@ -339,24 +337,14 @@ const submitForm = async () => {
formData.startTime = formData.meetingTimeRange[0] formData.startTime = formData.meetingTimeRange[0]
formData.endTime = formData.meetingTimeRange[1] 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 formLoading.value = true
try { try {
const projects = buildScheduledProjectItems( const projects =
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange), buildScheduledProjectItems(
formData.meetingTimeRange?.[0], applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES formData.meetingTimeRange?.[0],
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange) DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
const submitData = { const submitData = {
...formData, ...formData,
projects projects
@ -382,9 +370,26 @@ defineExpose({ open })
</script> </script>
<style scoped> <style scoped>
.import-section { margin-top: 8px; display: flex; align-items: center; gap: 8px; } .import-section {
.ml-10 { margin-left: 10px; } margin-top: 8px;
.mt-10 { margin-top: 10px; } display: flex;
.agenda-attachment-wrap { display: flex; flex-direction: column; gap: 6px; } align-items: center;
.agenda-file-line { display: flex; align-items: center; gap: 8px; } 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> </style>

View File

@ -23,8 +23,19 @@
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
class="search-datepicker" class="search-datepicker"
/> />
<el-select v-model="queryParams.status" size="large" placeholder="会议状态" clearable class="search-select"> <el-select
<el-option v-for="item in MEETING_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" /> 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> </el-select>
<button class="btn-reset" @click="resetQuery"></button> <button class="btn-reset" @click="resetQuery"></button>
<button class="btn-search" @click="handleQuery"></button> <button class="btn-search" @click="handleQuery"></button>
@ -35,6 +46,14 @@
<button v-hasPermi="['review:meeting:create']" class="btn-default" @click="goToEdit()"> <button v-hasPermi="['review:meeting:create']" class="btn-default" @click="goToEdit()">
<span class="btn-icon">+</span> 新建会议 <span class="btn-icon">+</span> 新建会议
</button> </button>
<button
v-hasPermi="['review:meeting:update']"
class="btn-default"
:disabled="!selectedTabletMeetingId || settingTabletActive"
@click="handleClearTabletActive"
>
清空平板评审会议
</button>
</div> </div>
<!-- 列表 --> <!-- 列表 -->
@ -54,27 +73,77 @@
<el-table-column label="会议地点" prop="location" width="184" show-overflow-tooltip /> <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="expertCount" width="112" align="center" />
<el-table-column label="项目数" prop="projectCount" width="96" 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"> <el-table-column label="状态" width="116" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span> <span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span>
</template> </template>
</el-table-column> </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 #default="{ row }">
<template v-if="row.status === 0"> <template v-if="row.status === 0">
<div class="op-group"> <div class="op-group">
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="goToEdit(row.id)"></a> <a v-hasPermi="['review:meeting:update']" class="op-link" @click="goToEdit(row.id)"
<a v-hasPermi="['review:meeting:send-sms']" class="op-link" @click="handleSendSms(row)"></a> >编辑</a
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)"></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> </div>
</template> </template>
<template v-else-if="row.status === 1"> <template v-else-if="row.status === 1">
<div class="op-group"> <div class="op-group">
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a> <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
<a v-if="row.mailSent" v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="openMailStatus(row)"></a> v-hasPermi="['review:meeting:send-sms']"
<a v-else v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="handleSendMail(row)"></a> class="op-link"
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="triggerUploadMinutes(row)"></a> @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 <a
v-hasPermi="['review:meeting:export']" v-hasPermi="['review:meeting:export']"
class="op-link" class="op-link"
@ -83,14 +152,23 @@
> >
{{ getExportText(row.id) }} {{ getExportText(row.id) }}
</a> </a>
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)"></a> <a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)"
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)"></a> >结束</a
>
<a
v-hasPermi="['review:meeting:cancel']"
class="op-link op-danger"
@click="handleCancel(row)"
>取消</a
>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="op-group"> <div class="op-group">
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a> <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> </div>
</template> </template>
</template> </template>
@ -98,7 +176,12 @@
</el-table> </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" /> <SmsStatusDialog ref="smsStatusRef" />
@ -121,9 +204,11 @@ import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { import {
clearTabletActiveMeeting,
getReviewMeetingPage, getReviewMeetingPage,
cancelReviewMeeting, cancelReviewMeeting,
finishReviewMeeting, finishReviewMeeting,
setTabletActiveMeeting,
sendSmsInvitation, sendSmsInvitation,
sendMailInvitation, sendMailInvitation,
uploadMinutesAttachment, uploadMinutesAttachment,
@ -146,6 +231,8 @@ const loading = ref(false)
const list = ref<ReviewMeetingRespVO[]>([]) const list = ref<ReviewMeetingRespVO[]>([])
const total = ref(0) const total = ref(0)
const useFixedActionColumn = ref(true) const useFixedActionColumn = ref(true)
const selectedTabletMeetingId = ref<number>()
const settingTabletActive = ref(false)
const MEETING_STATUS_OPTIONS = [ const MEETING_STATUS_OPTIONS = [
{ value: 0, label: '待召开' }, { value: 0, label: '待召开' },
@ -153,7 +240,12 @@ const MEETING_STATUS_OPTIONS = [
{ value: 2, label: '已结束' }, { value: 2, label: '已结束' },
{ value: 3, 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 }>({ const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize: number }>({
pageNo: 1, pageNo: 1,
@ -167,7 +259,9 @@ const smsStatusRef = ref()
const mailStatusRef = ref() const mailStatusRef = ref()
const minutesUploadInputRef = ref<HTMLInputElement>() const minutesUploadInputRef = ref<HTMLInputElement>()
const pendingMinutesMeeting = ref<ReviewMeetingRespVO>() 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 exportPollingMap = new Map<number, ReturnType<typeof setTimeout>>()
const syncActionColumnMode = () => { const syncActionColumnMode = () => {
@ -180,12 +274,16 @@ const getList = async () => {
const data = await getReviewMeetingPage(queryParams) const data = await getReviewMeetingPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
selectedTabletMeetingId.value = data.list.find((item) => item.tabletActive)?.id
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const handleQuery = () => { queryParams.pageNo = 1; getList() } const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => { const resetQuery = () => {
queryParams.name = undefined queryParams.name = undefined
queryParams.status = undefined queryParams.status = undefined
@ -202,7 +300,11 @@ const goToEdit = (id?: number, mode?: string) => {
} }
const goToProjectList = (row: ReviewMeetingRespVO) => { 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) => { const handleCancel = async (row: ReviewMeetingRespVO) => {
@ -219,6 +321,34 @@ const handleFinish = async (row: ReviewMeetingRespVO) => {
getList() 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) => { const handleCopy = async (row: ReviewMeetingRespVO) => {
router.push({ router.push({
name: 'ReviewMeetingEdit', name: 'ReviewMeetingEdit',
@ -237,7 +367,9 @@ const handleMinutesFileChange = async (event: Event) => {
if (!file || !pendingMinutesMeeting.value) { if (!file || !pendingMinutesMeeting.value) {
return 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'] const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
if (!allowed.includes(ext)) { if (!allowed.includes(ext)) {
ElMessage.error('会议纪要仅支持 Word、PDF 或图片') ElMessage.error('会议纪要仅支持 Word、PDF 或图片')
@ -517,10 +649,18 @@ onBeforeUnmount(() => {
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
} }
.status-0 { color: #ecae4b; } .status-0 {
.status-1 { color: #73c047; } color: #ecae4b;
.status-2 { color: #999; } }
.status-3 { color: #999; } .status-1 {
color: #73c047;
}
.status-2 {
color: #999;
}
.status-3 {
color: #999;
}
/* ── 会议名称链接 ── */ /* ── 会议名称链接 ── */
.meeting-name-link { .meeting-name-link {
@ -549,7 +689,11 @@ onBeforeUnmount(() => {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; 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 { .op-link:hover {
background: rgba(41, 90, 188, 0.1); background: rgba(41, 90, 188, 0.1);
@ -643,4 +787,8 @@ onBeforeUnmount(() => {
:deep(.review-table .el-table__fixed::before) { :deep(.review-table .el-table__fixed::before) {
background-color: #e1e7f0; background-color: #e1e7f0;
} }
:deep(.tablet-active-radio .el-radio__label) {
display: none;
}
</style> </style>

View File

@ -15,7 +15,7 @@
<div class="left-title">专家评审资料</div> <div class="left-title">专家评审资料</div>
<el-button text :icon="Refresh" @click="loadCatalog" /> <el-button text :icon="Refresh" @click="loadCatalog" />
</div> </div>
<div class="left-sub">日项目编目{{ projectCount }}</div> <div class="left-sub">前评审会议编目{{ projectCount }}</div>
<el-input <el-input
v-model="keyword" v-model="keyword"
@ -26,7 +26,10 @@
/> />
<div class="left-tree-wrap"> <div class="left-tree-wrap">
<el-empty v-if="treeData.length === 0" description="当前时段暂无可查看资料" /> <el-empty
v-if="treeData.length === 0"
description="管理员未选择当前评审会议,请联系管理员设置"
/>
<el-tree <el-tree
v-else v-else
ref="treeRef" ref="treeRef"
@ -146,7 +149,15 @@
</el-result> </el-result>
<template v-else-if="previewUrl"> <template v-else-if="previewUrl">
<TabletPdfViewer
v-if="previewMode === 'pdf'"
:key="`pdf-${previewFrameKey}`"
:src="previewUrl"
@loading-change="handlePdfLoadingChange"
@error="handlePdfRenderError"
/>
<iframe <iframe
v-else
:key="previewFrameKey" :key="previewFrameKey"
:src="previewUrl" :src="previewUrl"
class="preview-iframe" class="preview-iframe"
@ -155,7 +166,10 @@
@load="handlePreviewFrameLoad" @load="handlePreviewFrameLoad"
@error="handlePreviewFrameError" @error="handlePreviewFrameError"
></iframe> ></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-title">{{ previewLoadingTitle }}</div>
<div class="welcome-sub">{{ previewLoadingSubtitle }}</div> <div class="welcome-sub">{{ previewLoadingSubtitle }}</div>
</div> </div>
@ -426,10 +440,12 @@ import {
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import TabletPdfViewer from './TabletPdfViewer.vue'
import { resolvePdfSourceUrl, resolveTabletPreviewMode } from './previewMode.mjs'
import { import {
getFileOpenUrl, getFileOpenUrl,
getProjectFiles, getProjectFiles,
getTodayCatalog, getActiveCatalog,
type ReviewTabletCatalogVO, type ReviewTabletCatalogVO,
type ReviewTabletOpenUrlVO type ReviewTabletOpenUrlVO
} from '@/api/review/tablet' } from '@/api/review/tablet'
@ -598,13 +614,20 @@ const activeFileDesc = computed(() => {
return segs.join(' ') return segs.join(' ')
}) })
const previewLoadingTitle = computed(() => const previewLoadingTitle = computed(() =>
isOfficeFile(activeFileNode.value?.fileType) ? '正在转换文档预览' : '正在加载预览' previewMode.value === 'pdf'
? '正在加载 PDF 预览'
: isOfficeFile(activeFileNode.value?.fileType)
? '正在转换文档预览'
: '正在加载预览'
) )
const previewLoadingSubtitle = computed(() => const previewLoadingSubtitle = computed(() =>
isOfficeFile(activeFileNode.value?.fileType) previewMode.value === 'pdf'
? 'Office 文件首次打开可能需要一些时间,请稍候' ? '正在渲染文档内容,请稍候'
: '正在为您打开当前资料' : isOfficeFile(activeFileNode.value?.fileType)
? 'Office 文件首次打开可能需要一些时间,请稍候'
: '正在为您打开当前资料'
) )
const previewMode = computed(() => resolveTabletPreviewMode(previewPayload.value))
const isTouchTablet = computed(() => coarsePointer.value) const isTouchTablet = computed(() => coarsePointer.value)
const isCompactTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1400) const isCompactTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1400)
const isDenseTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1180) const isDenseTablet = computed(() => coarsePointer.value && viewportWidth.value <= 1180)
@ -763,14 +786,22 @@ const loadPreview = async (fileId: number) => {
const payload = await getFileOpenUrl(fileId) const payload = await getFileOpenUrl(fileId)
if (requestToken !== previewRequestToken.value) return if (requestToken !== previewRequestToken.value) return
previewPayload.value = payload previewPayload.value = payload
if (!payload?.openUrl) { const mode = resolveTabletPreviewMode(payload)
const sourceUrl = resolvePdfSourceUrl(payload, payload.openUrl)
if (!sourceUrl) {
previewUrl.value = '' previewUrl.value = ''
previewFrameLoading.value = false previewFrameLoading.value = false
previewError.value = '未获取到预览地址' previewError.value = '未获取到预览地址'
return return
} }
previewUrl.value = payload.openUrl previewUrl.value = sourceUrl
startPreviewFrameTimeout(requestToken) if (mode === 'iframe') {
startPreviewFrameTimeout(requestToken)
} else {
clearPreviewFrameTimeout()
// PDF
previewFrameLoading.value = false
}
} catch { } catch {
if (requestToken !== previewRequestToken.value) return if (requestToken !== previewRequestToken.value) return
previewUrl.value = '' previewUrl.value = ''
@ -791,7 +822,7 @@ const selectFileNode = async (fileNode: FileTreeNode) => {
} }
const openInNewWindow = () => { const openInNewWindow = () => {
const url = previewPayload.value?.openUrl || previewUrl.value const url = previewUrl.value || previewPayload.value?.openUrl
if (!url) { if (!url) {
ElMessage.warning('暂无可打开的预览地址') ElMessage.warning('暂无可打开的预览地址')
return return
@ -814,6 +845,18 @@ const handlePreviewFrameError = () => {
previewError.value = '预览内容加载失败,请稍后重试' 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) => { const handleNodeExpand = async (data: TreeNode) => {
if (data.type !== 'project') return if (data.type !== 'project') return
if (!expandedKeys.value.includes(data.id)) expandedKeys.value.push(data.id) if (!expandedKeys.value.includes(data.id)) expandedKeys.value.push(data.id)
@ -876,7 +919,7 @@ const loadCatalog = async () => {
aiProjectCache.value = {} aiProjectCache.value = {}
resetProjectAiState() resetProjectAiState()
try { try {
const catalog = await getTodayCatalog() const catalog = await getActiveCatalog()
treeData.value = catalog treeData.value = catalog
.sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0)) .sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0))
.map((item) => buildProjectNode(item)) .map((item) => buildProjectNode(item))