admin-vue3/src/views/review/meeting/MeetingEdit.vue

965 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<ContentWrap>
<div class="page-title-bar">
<span class="back-btn" @click="handleBack">&#8249;</span>
<span class="page-title">{{ pageTitle }}</span>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="isView ? {} : rules"
label-width="100px"
v-loading="formLoading"
>
<!-- 基本信息 -->
<div class="section-header"><span class="section-title">基本信息</span></div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入会议名称" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组织单位" prop="organizationUnit">
<el-input
v-model="formData.organizationUnit"
placeholder="请输入组织单位"
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议时间" prop="meetingTimeRange">
<el-date-picker
v-model="formData.meetingTimeRange"
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="location">
<el-select
v-model="formData.location"
placeholder="请选择或输入会议地点"
filterable
allow-create
default-first-option
clearable
:disabled="isView"
style="width: 100%"
>
<el-option
v-for="item in meetingLocationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议主持人" prop="host">
<el-input v-model="formData.host" placeholder="请输入会议主持人" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="议程附件" prop="agendaAttachmentUrl">
<div class="agenda-attachment-wrap">
<div v-if="!isView" class="agenda-action-row">
<el-upload
:auto-upload="false"
:show-file-list="false"
accept=".docx,.pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
:on-change="handleAgendaAttachmentChange"
>
<button type="button" class="btn-upload">上传议程附件</button>
</el-upload>
<button
type="button"
class="btn-generate"
:disabled="formLoading"
@click="handleGenerateAgenda"
>
自动生成议程
</button>
</div>
<span class="upload-hint">支持 DOCX、PDF、图片固定单附件可生成后再手工替换</span>
<div v-if="formData.agendaAttachmentUrl" class="agenda-file-line">
<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
>
</div>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="会议纪要" prop="minutesAttachmentUrl">
<div class="agenda-attachment-wrap">
<el-upload
v-if="!isView"
:auto-upload="false"
:show-file-list="false"
accept=".doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
:on-change="handleMinutesAttachmentChange"
>
<button type="button" class="btn-upload">上传会议纪要</button>
</el-upload>
<span class="upload-hint">支持 Word、PDF、图片结束会议前必须上传</span>
<span v-if="!meetingId && !isView" class="upload-hint">请先保存会议后再上传纪要</span>
<div v-if="formData.minutesAttachmentUrl" class="agenda-file-line">
<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
>
</div>
<div
v-if="formData.minutesAttachmentUrl && formData.minutesAiStatusName"
class="minutes-ai-line"
>
<el-tag size="small" :type="getMinutesAiTagType(formData.minutesAiStatus)">
{{ formData.minutesAiStatusName }}
</el-tag>
<el-text v-if="formData.minutesAiUpdatedTime" type="info" size="small">
最近生成:{{ formData.minutesAiUpdatedTime }}
</el-text>
<el-text v-if="formData.minutesAiErrorMessage" type="danger" size="small">
{{ formData.minutesAiErrorMessage }}
</el-text>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 评审专家 -->
<div class="section-header"><span class="section-title">评审专家</span></div>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="参会专家" prop="expertIds">
<ExpertSelectTable
v-model="formData.expertIds"
:experts="expertOptions"
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 评审项目 -->
<div class="section-header"><span class="section-title">评审项目</span></div>
<div v-if="!isView" class="import-section">
<el-upload
:auto-upload="false"
:on-change="handleExcelChange"
:show-file-list="false"
accept=".xls,.xlsx"
>
<button type="button" class="btn-default">导入验收申请 Excel</button>
</el-upload>
<button type="button" class="btn-default" @click="handleDownloadTemplate"
>下载导入模板</button
>
<span class="import-hint">格式:序号、议程分类、项目标题、汇报人、报告人单位</span>
</div>
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
<el-table :data="formData.projects" border max-height="360" class="projects-preview-table">
<el-table-column label="序号" prop="seqNo" width="80" align="center" />
<el-table-column label="起止时间" width="180" align="center">
<template #default="{ row }">
{{ row.startTime || '--:--' }} - {{ row.endTime || '--:--' }}
</template>
</el-table-column>
<el-table-column label="议程分类" prop="agendaCategory" width="160" />
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
<el-table-column label="汇报人" prop="reporter" width="120" />
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="220" />
</el-table>
</div>
<el-empty v-else-if="isView" description="暂无评审项目" :image-size="60" />
</el-form>
<!-- 底部操作区 -->
<div class="form-footer">
<button
v-if="!isView"
type="button"
class="btn-primary"
:disabled="formLoading"
@click="submitForm"
>
{{ formLoading ? '保存中...' : '保存草稿' }}
</button>
<button type="button" class="btn-default" @click="handleBack">返回</button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormRules } from 'element-plus'
import type { UploadFile } from 'element-plus'
import {
createReviewMeeting,
updateReviewMeeting,
getReviewMeeting,
importProjectsFromExcel,
getImportTemplate,
generateAgendaAttachment,
uploadAgendaAttachment,
uploadMinutesAttachment,
type ReviewMeetingAgendaGenerateReqVO,
type ReviewMeetingSaveReqVO,
type ReviewProjectItemVO
} from '@/api/review/meeting'
import { getReviewProjectPage, updateReviewProjectTimeBatch } from '@/api/review/project'
import { getExpertUserList } from '@/api/system/user/index'
import download from '@/utils/download'
import ExpertSelectTable from './components/ExpertSelectTable.vue'
import { applyDefaultReviewDate } from './projectReviewDate'
import {
buildScheduledProjectItems,
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
} from './projectSchedule'
import {
buildProjectTimeBatchPayload,
mapCopiedProjectItems,
mapPersistedProjectItems,
shouldSyncPersistedProjectTimes,
type MeetingEditProjectItem
} from './meetingEditHelpers'
defineOptions({ name: 'ReviewMeetingEdit' })
const router = useRouter()
const route = useRoute()
const formLoading = ref(false)
const expertOptions = ref<any[]>([])
const isProjectsModified = ref(false)
const meetingLocationOptions = ['东5楼326', '南6楼203', '南6楼207']
// 从路由参数判断模式
const meetingId = computed(() => {
const id = route.params.id
return id ? Number(id) : undefined
})
const copyFromId = computed(() => {
const queryValue = route.query.copyFromId
const raw = Array.isArray(queryValue) ? queryValue[0] : queryValue
const parsed = raw ? Number(raw) : NaN
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
})
const isView = computed(() => route.query.mode === 'view')
const isEdit = computed(() => !!meetingId.value && !isView.value)
const isCopyMode = computed(() => !meetingId.value && !!copyFromId.value && !isView.value)
const pageTitle = computed(() => {
if (isView.value) return '查看会议'
if (isCopyMode.value) return '复制会议'
return isEdit.value ? '编辑会议' : '新建会议'
})
type FormData = ReviewMeetingSaveReqVO & {
organizationUnit?: string
meetingTimeRange?: any[]
minutesAiStatus?: number
minutesAiStatusName?: string
minutesAiErrorMessage?: string
minutesAiUpdatedTime?: string
projects: MeetingEditProjectItem[]
}
const formData = reactive<FormData>({
id: undefined,
name: '',
organizationUnit: undefined,
startTime: undefined,
endTime: undefined,
location: '东5楼326',
host: undefined,
agendaAttachmentName: undefined,
agendaAttachmentUrl: undefined,
agendaAttachmentType: undefined,
agendaAttachmentSize: undefined,
minutesAttachmentName: undefined,
minutesAttachmentUrl: undefined,
minutesAttachmentType: undefined,
minutesAttachmentSize: undefined,
minutesAiStatus: undefined,
minutesAiStatusName: undefined,
minutesAiErrorMessage: undefined,
minutesAiUpdatedTime: undefined,
expertIds: [],
meetingTimeRange: undefined,
projects: []
})
const rules: FormRules = {
name: [{ required: true, message: '会议名称不能为空', trigger: 'blur' }],
organizationUnit: [{ required: true, message: '组织单位不能为空', trigger: 'blur' }],
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
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' }
]
}
const formRef = ref()
const originalMeetingStart = ref<string | number | undefined>()
const getMinutesAiTagType = (status?: number) => {
if (status === 2) return 'success'
if (status === 3) return 'danger'
if (status === 1) return 'warning'
return 'info'
}
const resetProjectReviewDate = (projects: MeetingEditProjectItem[]): MeetingEditProjectItem[] =>
(projects || []).map((item) => ({
...item,
reviewDate: undefined
}))
const buildPreviewScheduledProjects = (
projects: MeetingEditProjectItem[] = formData.projects
): MeetingEditProjectItem[] => {
const projectsWithReviewDate = applyDefaultReviewDate(
projects,
formData.meetingTimeRange
) as MeetingEditProjectItem[]
return (
buildScheduledProjectItems(
projectsWithReviewDate,
formData.meetingTimeRange?.[0],
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) || projectsWithReviewDate
)
}
const stripProjectTransientFields = (projects: MeetingEditProjectItem[]): ReviewProjectItemVO[] =>
(projects || []).map(({ id: _id, ...item }) => item)
const loadDetail = async (id: number) => {
formLoading.value = true
try {
const [detail, projectData] = await Promise.all([
getReviewMeeting(id),
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
])
Object.assign(formData, detail)
formData.projects = mapPersistedProjectItems(projectData?.list ?? [])
if (detail.startTime && detail.endTime) {
formData.meetingTimeRange = [
new Date(detail.startTime.replace(' ', 'T')).getTime(),
new Date(detail.endTime.replace(' ', 'T')).getTime()
]
}
originalMeetingStart.value = formData.meetingTimeRange?.[0]
} finally {
formLoading.value = false
}
}
const loadCopySource = async (id: number) => {
formLoading.value = true
try {
const [detail, projectData] = await Promise.all([
getReviewMeeting(id),
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
])
if (![2, 3].includes(detail.status)) {
ElMessage.warning('仅支持从已结束或已取消会议复制')
router.push({ name: 'ReviewMeeting' })
return
}
formData.id = undefined
formData.name = detail.name
formData.organizationUnit = detail.organizationUnit
formData.startTime = detail.startTime
formData.endTime = detail.endTime
formData.location = detail.location || '东5楼326'
formData.host = detail.host
formData.agendaAttachmentName = undefined
formData.agendaAttachmentUrl = undefined
formData.agendaAttachmentType = undefined
formData.agendaAttachmentSize = undefined
formData.minutesAttachmentName = undefined
formData.minutesAttachmentUrl = undefined
formData.minutesAttachmentType = undefined
formData.minutesAttachmentSize = undefined
formData.minutesAiStatus = undefined
formData.minutesAiStatusName = undefined
formData.minutesAiErrorMessage = undefined
formData.minutesAiUpdatedTime = undefined
formData.expertIds = detail.expertIds || []
formData.projects = resetProjectReviewDate(mapCopiedProjectItems(projectData?.list ?? []))
isProjectsModified.value = false
if (detail.startTime && detail.endTime) {
formData.meetingTimeRange = [
new Date(detail.startTime.replace(' ', 'T')).getTime(),
new Date(detail.endTime.replace(' ', 'T')).getTime()
]
} else {
formData.meetingTimeRange = undefined
}
originalMeetingStart.value = formData.meetingTimeRange?.[0]
ElMessage.info(
'已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制'
)
} finally {
formLoading.value = false
}
}
onMounted(async () => {
expertOptions.value = await getExpertUserList().catch(() => [])
if (meetingId.value) {
await loadDetail(meetingId.value)
} else if (copyFromId.value) {
await loadCopySource(copyFromId.value)
} else if (!formData.location) {
formData.location = '东5楼326'
}
})
watch(
() => formData.meetingTimeRange?.[0],
(newValue, oldValue) => {
if (newValue === oldValue || oldValue === undefined || !formData.projects.length) {
return
}
formData.projects = buildPreviewScheduledProjects(formData.projects)
}
)
const handleExcelChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
if (formData.projects && formData.projects.length > 0) {
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', {
type: 'warning'
})
}
formLoading.value = true
try {
const result = await importProjectsFromExcel(uploadFile.raw)
const projects = applyDefaultReviewDate(
result as ReviewProjectItemVO[],
formData.meetingTimeRange
)
formData.projects = (buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) ||
projects) as MeetingEditProjectItem[]
isProjectsModified.value = true
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
} catch {
ElMessage.error('Excel 解析失败,请检查文件格式')
} finally {
formLoading.value = false
}
}
const handleDownloadTemplate = async () => {
try {
const data = await getImportTemplate()
download.excel(data, '评审项目导入模板.xls')
} catch {
ElMessage.error('下载模板失败')
}
}
const handleAgendaAttachmentChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
const ext = uploadFile.raw.name.includes('.')
? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase()
: ''
const isDocx = ext === 'docx'
const isPdf = ext === 'pdf' || uploadFile.raw.type === 'application/pdf'
const isImage = uploadFile.raw.type?.startsWith('image/')
if (!isDocx && !isPdf && !isImage) {
ElMessage.error('议程附件仅支持 DOCX、PDF 或图片')
return
}
formLoading.value = true
try {
const attachment = await uploadAgendaAttachment(uploadFile.raw)
formData.agendaAttachmentName = attachment.name
formData.agendaAttachmentUrl = attachment.url
formData.agendaAttachmentType = attachment.type
formData.agendaAttachmentSize = attachment.size
formRef.value?.clearValidate('agendaAttachmentUrl')
ElMessage.success('议程附件上传成功')
} finally {
formLoading.value = false
}
}
const clearAgendaAttachment = () => {
formData.agendaAttachmentName = undefined
formData.agendaAttachmentUrl = undefined
formData.agendaAttachmentType = undefined
formData.agendaAttachmentSize = undefined
formRef.value?.validateField('agendaAttachmentUrl')
}
const handleMinutesAttachmentChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
if (!meetingId.value) {
ElMessage.warning('请先保存会议后再上传纪要')
return
}
const ext = uploadFile.raw.name.includes('.')
? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase()
: ''
const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
if (!allowed.includes(ext)) {
ElMessage.error('会议纪要仅支持 Word、PDF 或图片')
return
}
formLoading.value = true
try {
const attachment = await uploadMinutesAttachment(meetingId.value, uploadFile.raw)
formData.minutesAttachmentName = attachment.name
formData.minutesAttachmentUrl = attachment.url
formData.minutesAttachmentType = attachment.type
formData.minutesAttachmentSize = attachment.size
formData.minutesAiStatus = 1
formData.minutesAiStatusName = '生成中'
formData.minutesAiErrorMessage = undefined
formData.minutesAiUpdatedTime = undefined
ElMessage.success('会议纪要上传成功,整改通知后台生成中')
} finally {
formLoading.value = false
}
}
const clearMinutesAttachment = () => {
formData.minutesAttachmentName = undefined
formData.minutesAttachmentUrl = undefined
formData.minutesAttachmentType = undefined
formData.minutesAttachmentSize = undefined
formData.minutesAiStatus = undefined
formData.minutesAiStatusName = undefined
formData.minutesAiErrorMessage = undefined
formData.minutesAiUpdatedTime = undefined
}
const previewAgendaAttachment = () => {
if (formData.agendaAttachmentUrl) {
window.open(formData.agendaAttachmentUrl, '_blank')
}
}
const previewMinutesAttachment = () => {
if (formData.minutesAttachmentUrl) {
window.open(formData.minutesAttachmentUrl, '_blank')
}
}
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '-'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
const buildScheduledProjects = (): MeetingEditProjectItem[] => {
const projectsWithReviewDate = applyDefaultReviewDate(
formData.projects,
formData.meetingTimeRange
) as MeetingEditProjectItem[]
const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime)
if (hasCompleteSchedule) {
return projectsWithReviewDate
}
return (
buildScheduledProjectItems(
projectsWithReviewDate,
formData.meetingTimeRange?.[0],
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) || projectsWithReviewDate
)
}
const buildAgendaGenerateData = (): ReviewMeetingAgendaGenerateReqVO | undefined => {
if (!formData.name?.trim()) {
ElMessage.warning('请先填写会议名称')
return
}
if (!formData.organizationUnit?.trim()) {
ElMessage.warning('请先填写组织单位')
return
}
if (!formData.location?.trim()) {
ElMessage.warning('请先填写会议地点')
return
}
if (!formData.host?.trim()) {
ElMessage.warning('请先填写会议主持人')
return
}
if (!formData.meetingTimeRange || formData.meetingTimeRange.length !== 2) {
ElMessage.warning('请先填写会议时间')
return
}
if (!formData.expertIds?.length) {
ElMessage.warning('请先选择参会专家')
return
}
if (!formData.projects?.length) {
ElMessage.warning('请先导入评审项目')
return
}
const projects = buildScheduledProjects()
const hasMissingSchedule = projects.some((item) => !item.startTime || !item.endTime)
if (hasMissingSchedule) {
ElMessage.warning('评审项目起止时间未生成完成,请检查会议时间和项目列表')
return
}
return {
name: formData.name,
organizationUnit: formData.organizationUnit,
startTime: formData.meetingTimeRange[0],
endTime: formData.meetingTimeRange[1],
location: formData.location,
host: formData.host,
expertIds: formData.expertIds,
projects
}
}
const handleGenerateAgenda = async () => {
if (formLoading.value) return
const generateData = buildAgendaGenerateData()
if (!generateData) return
formLoading.value = true
try {
const attachment = await generateAgendaAttachment(generateData)
formData.agendaAttachmentName = attachment.name
formData.agendaAttachmentUrl = attachment.url
formData.agendaAttachmentType = attachment.type
formData.agendaAttachmentSize = attachment.size
formRef.value?.clearValidate('agendaAttachmentUrl')
ElMessage.success('议程附件已生成')
} finally {
formLoading.value = false
}
}
const submitForm = async () => {
if (formLoading.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
if (formData.meetingTimeRange?.length === 2) {
formData.startTime = formData.meetingTimeRange[0]
formData.endTime = formData.meetingTimeRange[1]
}
formLoading.value = true
try {
const projects = buildScheduledProjects()
const submitProjects = stripProjectTransientFields(projects)
const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = {
...formData,
projects: submitProjects
}
if (isEdit.value && !isProjectsModified.value) {
delete submitData.projects
}
if (isEdit.value) {
await updateReviewMeeting(submitData)
const shouldSyncProjectTimes = shouldSyncPersistedProjectTimes({
isEdit: isEdit.value,
isProjectsModified: isProjectsModified.value,
originalMeetingStart: originalMeetingStart.value,
currentMeetingStart: formData.meetingTimeRange?.[0],
projects
})
if (shouldSyncProjectTimes) {
const timeBatchPayload = buildProjectTimeBatchPayload(projects)
if (timeBatchPayload) {
await updateReviewProjectTimeBatch(timeBatchPayload)
}
}
originalMeetingStart.value = formData.meetingTimeRange?.[0]
ElMessage.success('更新成功')
} else {
await createReviewMeeting(submitData)
originalMeetingStart.value = formData.meetingTimeRange?.[0]
ElMessage.success('创建成功,会议已保存为草稿')
}
router.push({ name: 'ReviewMeeting' })
} finally {
formLoading.value = false
}
}
const handleBack = () => {
router.back()
}
</script>
<style scoped>
/* ── 页面标题 ── */
.page-title-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e1e7f0;
}
.back-btn {
font-size: 30px;
color: #295abc;
cursor: pointer;
line-height: 1;
font-weight: 400;
}
.back-btn:hover {
opacity: 0.75;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
}
/* ── 分区标题 ── */
.section-header {
margin: 18px 0 12px;
}
.section-title {
position: relative;
font-size: 18px;
font-weight: 600;
color: #333;
padding-left: 12px;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
width: 3px;
border-radius: 2px;
background-color: #295abc;
}
/* ── 表单 item 字号 ── */
:deep(.el-form-item__label) {
font-size: 16px;
color: #555;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner),
:deep(.el-select__selected-item),
:deep(.el-select__placeholder),
:deep(.el-range-input) {
font-size: 16px;
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper),
:deep(.el-range-editor.el-input__wrapper) {
min-height: 42px;
}
:deep(.el-form-item__content) {
line-height: 1.5;
}
/* ── 议程附件 ── */
.agenda-attachment-wrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.agenda-file-line {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: 16px;
}
.minutes-ai-line {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.agenda-action-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.upload-hint {
font-size: 14px;
color: #999;
}
/* ── 导入区 ── */
.import-section {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.import-hint {
font-size: 14px;
color: #999;
}
.mt-10 {
margin-top: 10px;
}
/* ── 按钮 ── */
.btn-upload {
height: 38px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #295abc;
border-radius: 6px;
color: #295abc;
font-size: 15px;
cursor: pointer;
transition: all 0.2s;
}
.btn-upload:hover {
background-color: rgba(41, 90, 188, 0.08);
}
.btn-generate {
display: inline-flex;
align-items: center;
height: 38px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #3aa76d;
border-radius: 6px;
color: #3aa76d;
font-size: 15px;
cursor: pointer;
transition: all 0.2s;
}
.btn-generate:hover {
background-color: rgba(58, 167, 109, 0.08);
}
.btn-generate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-default {
display: inline-flex;
align-items: center;
height: 40px;
padding: 0 18px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 15px;
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-primary {
display: inline-flex;
align-items: center;
height: 40px;
padding: 0 20px;
background-color: #295abc;
border: none;
border-radius: 6px;
font-size: 15px;
color: #fff;
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;
}
/* ── 底部 ── */
.form-footer {
margin-top: 20px;
display: flex;
gap: 12px;
}
/* ── 项目预览表格 ── */
:deep(.projects-preview-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 18px;
border-color: #e1e7f0;
}
:deep(.projects-preview-table .el-table__header-wrapper th.el-table__cell) {
padding-top: 12px;
padding-bottom: 12px;
}
:deep(.projects-preview-table .el-table__header-wrapper th .cell) {
justify-content: center;
text-align: center;
}
:deep(.projects-preview-table .el-table__body td) {
font-size: 18px;
color: #333;
border-color: #e1e7f0;
padding-top: 12px;
padding-bottom: 12px;
}
:deep(.projects-preview-table .el-table__body td .cell),
:deep(.projects-preview-table .el-table__header-wrapper th .cell) {
line-height: 1.5;
}
</style>