修复bug

pull/874/head
Codewoc 2026-03-23 22:02:03 +08:00
parent 75be833554
commit 46a6289836
7 changed files with 1334 additions and 225 deletions

View File

@ -26,6 +26,17 @@ export interface ReviewMeetingProjectPageReqVO {
reporter?: string
}
/** 独立项目列表查询meetingId 可选,用于独立菜单页) */
export interface ReviewProjectPageReqVO {
pageNo?: number
pageSize?: number
reviewMeetingId?: number
projectTitle?: string
agendaCategory?: string
reporter?: string
reporterUnit?: string
}
export interface ReviewMeetingFileRespVO {
id: number
reviewMeetingProjectId: number
@ -41,10 +52,14 @@ export interface ReviewMeetingFileRespVO {
// API 调用
// ============================================================
/** 分页查询评审项目列表 */
/** 分页查询评审项目列表(需要 meetingId */
export const getReviewProjectPage = (params: ReviewMeetingProjectPageReqVO) =>
request.get({ url: '/project/review-project/page', params })
/** 独立分页查询评审项目列表meetingId 可选) */
export const getReviewProjectPageStandalone = (params: ReviewProjectPageReqVO) =>
request.get({ url: '/project/review-project/page', params })
/** 创建评审项目 */
export const createReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =>
request.post({ url: '/project/review-project/create', data })

View File

@ -0,0 +1,425 @@
<template>
<ContentWrap>
<!-- 页面标题 -->
<div class="page-title">项目列表</div>
<!-- 搜索栏 -->
<div class="search-bar">
<el-select
v-model="queryParams.reviewMeetingId"
placeholder="所属会议"
clearable
filterable
class="search-meeting-select"
>
<el-option
v-for="m in meetingOptions"
:key="m.id"
:label="m.name"
:value="m.id"
/>
</el-select>
<el-input
v-model="queryParams.projectTitle"
placeholder="项目标题"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<el-input
v-model="queryParams.reporter"
placeholder="报告人"
clearable
class="search-input-sm"
@keyup.enter="handleQuery"
/>
<el-input
v-model="queryParams.reporterUnit"
placeholder="报告单位"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<button class="btn-reset" @click="resetQuery"></button>
<button class="btn-search" @click="handleQuery"></button>
</div>
<!-- 操作按钮 -->
<div class="toolbar">
<button class="btn-default" @click="openForm('create')">
<span class="btn-icon">+</span> 新增评审项目
</button>
<button class="btn-default btn-danger" :disabled="selectedIds.length === 0" @click="handleDeleteBatch">
批量删除
</button>
</div>
<!-- 列表 -->
<el-table v-loading="loading" :data="list" border class="review-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="会中序号" prop="seqNo" width="80" align="center" />
<el-table-column label="起止时间" width="110" align="center">
<template #default="{ row }">
<span v-if="row.startTime || row.endTime">{{ row.startTime || '' }} - {{ row.endTime || '' }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="议程分类" prop="agendaCategory" width="110" />
<el-table-column label="项目名称" prop="projectTitle" show-overflow-tooltip min-width="180">
<template #default="{ row }">
<span class="project-name-text" @click="goToDetail(row)">{{ row.projectTitle }}</span>
</template>
</el-table-column>
<el-table-column label="报告人" prop="reporter" width="80" />
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
<el-table-column label="主持人" width="150">
<template #default="{ row }">
<el-input
v-model="row.host"
placeholder="填写主持人"
size="small"
@blur="handleHostBlur(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<a class="op-link" @click="openForm('update', row)">编辑</a>
<a class="op-link" @click="goToDetail(row)"></a>
<a class="op-link op-danger" @click="handleDelete(row.id)"></a>
</template>
</el-table-column>
</el-table>
<!-- 编辑/新增项目弹窗 -->
<el-dialog v-model="formVisible" :title="formType === 'create' ? '新增评审项目' : '编辑评审项目'" width="550px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="所属会议" prop="reviewMeetingId">
<el-select v-model="formData.reviewMeetingId" placeholder="请选择会议" filterable style="width: 100%">
<el-option v-for="m in meetingOptions" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item label="序号" prop="seqNo">
<el-input-number v-model="formData.seqNo" :min="1" />
</el-form-item>
<el-form-item label="时间范围">
<div style="display: flex; gap: 10px; width: 100%;">
<el-time-picker v-model="formData.startTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间" style="flex: 1" />
<span>-</span>
<el-time-picker v-model="formData.endTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间" style="flex: 1" />
</div>
</el-form-item>
<el-form-item label="议程分类" prop="agendaCategory">
<el-input v-model="formData.agendaCategory" placeholder="请输入议程分类" />
</el-form-item>
<el-form-item label="项目标题" prop="projectTitle">
<el-input v-model="formData.projectTitle" placeholder="请输入项目标题" />
</el-form-item>
<el-form-item label="汇报人" prop="reporter">
<el-input v-model="formData.reporter" placeholder="请输入汇报人" />
</el-form-item>
<el-form-item label="报告单位" prop="reporterUnit">
<el-input v-model="formData.reporterUnit" placeholder="请输入报告单位" />
</el-form-item>
<el-form-item label="主持人" prop="host">
<el-input v-model="formData.host" placeholder="请输入主持人" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formVisible = false"> </el-button>
<el-button type="primary" :loading="formLoading" @click="submitForm"> </el-button>
</template>
</el-dialog>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getReviewProjectPageStandalone,
updateProjectHost,
updateReviewProject,
createReviewProject,
deleteReviewProject,
type ReviewMeetingProjectRespVO,
type ReviewProjectPageReqVO
} from '@/api/review/project'
import { getReviewMeetingPage } from '@/api/review/meeting'
defineOptions({ name: 'ReviewAllProject' })
const router = useRouter()
const loading = ref(false)
const list = ref<ReviewMeetingProjectRespVO[]>([])
const total = ref(0)
const meetingOptions = ref<{ id: number; name: string }[]>([])
const queryParams = reactive<ReviewProjectPageReqVO & { pageNo: number; pageSize: number }>({
pageNo: 1,
pageSize: 10,
reviewMeetingId: undefined,
projectTitle: undefined,
agendaCategory: undefined,
reporter: undefined,
reporterUnit: undefined
})
const getList = async () => {
loading.value = true
try {
const data = await getReviewProjectPageStandalone(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const loadMeetingOptions = async () => {
const data = await getReviewMeetingPage({ pageNo: 1, pageSize: 200 }).catch(() => ({ list: [] }))
meetingOptions.value = (data.list || []).map((m: any) => ({ id: m.id, name: m.name }))
}
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => {
queryParams.reviewMeetingId = undefined
queryParams.projectTitle = undefined
queryParams.agendaCategory = undefined
queryParams.reporter = undefined
queryParams.reporterUnit = undefined
handleQuery()
}
const selectedIds = ref<number[]>([])
const handleSelectionChange = (val: ReviewMeetingProjectRespVO[]) => {
selectedIds.value = val.map(v => v.id)
}
const handleDelete = async (id: number) => {
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
await deleteReviewProject([id])
ElMessage.success('删除成功')
getList()
}
const handleDeleteBatch = async () => {
if (selectedIds.value.length === 0) return
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 个项目吗?`, '警告', { type: 'warning' })
await deleteReviewProject(selectedIds.value)
selectedIds.value = []
ElMessage.success('删除成功')
getList()
}
const handleHostBlur = async (row: ReviewMeetingProjectRespVO) => {
await updateProjectHost(row.id, row.host || '')
ElMessage.success('主持人已更新')
}
const formVisible = ref(false)
const formType = ref<'create' | 'update'>('create')
const formLoading = ref(false)
const formRef = ref()
const formData = reactive<Partial<ReviewMeetingProjectRespVO>>({})
const formRules = {
reviewMeetingId: [{ required: true, message: '请选择所属会议', trigger: 'change' }],
projectTitle: [{ required: true, message: '项目标题不能为空', trigger: 'blur' }]
}
const openForm = (type: 'create' | 'update', row?: ReviewMeetingProjectRespVO) => {
formType.value = type
if (type === 'create') {
Object.keys(formData).forEach(key => delete formData[key as keyof ReviewMeetingProjectRespVO])
} else if (row) {
Object.assign(formData, row)
}
formVisible.value = true
}
const submitForm = async () => {
const valid = await formRef.value?.validate()
if (!valid) return
formLoading.value = true
try {
if (formType.value === 'create') {
await createReviewProject(formData)
ElMessage.success('新增成功')
} else {
await updateReviewProject(formData)
ElMessage.success('修改成功')
}
formVisible.value = false
getList()
} finally {
formLoading.value = false
}
}
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
const meeting = meetingOptions.value.find(m => m.id === row.reviewMeetingId)
router.push({
name: 'ReviewProjectDetail',
params: { meetingId: row.reviewMeetingId, projectId: row.id },
state: {
projectTitle: row.projectTitle,
seqNo: row.seqNo,
startTime: row.startTime,
endTime: row.endTime,
agendaCategory: row.agendaCategory,
reporter: row.reporter,
reporterUnit: row.reporterUnit,
host: row.host,
meetingName: meeting?.name
}
})
}
onMounted(async () => {
await loadMeetingOptions()
await getList()
})
</script>
<style scoped>
/* ── 页面标题 ── */
.page-title {
font-size: 22px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #e1e7f0;
}
/* ── 搜索栏 ── */
.search-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.search-meeting-select {
width: 220px;
}
.search-input {
width: 160px;
}
.search-input-sm {
width: 120px;
}
:deep(.search-meeting-select .el-input__wrapper),
:deep(.search-input .el-input__wrapper),
:deep(.search-input-sm .el-input__wrapper) {
height: 40px;
border-radius: 6px;
border-color: #dcdedf;
}
:deep(.search-meeting-select .el-input__inner),
:deep(.search-input .el-input__inner),
:deep(.search-input-sm .el-input__inner) {
font-size: 15px;
color: #333;
}
.btn-search {
height: 40px;
padding: 0 22px;
background-color: #295abc;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-search:hover { background-color: rgba(41, 90, 188, 0.88); }
.btn-reset {
height: 40px;
padding: 0 8px;
background: none;
border: none;
color: #295abc;
font-size: 15px;
cursor: pointer;
}
.btn-reset:hover { opacity: 0.8; }
/* ── 工具栏 ── */
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.btn-default {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.btn-default:hover {
background-color: rgba(41, 90, 188, 0.08);
border-color: #295abc;
color: #295abc;
}
.btn-default:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-danger { border-color: #fc4f54; color: #fc4f54; }
.btn-danger:hover {
background-color: rgba(252, 79, 84, 0.08);
border-color: #fc4f54;
color: #fc4f54;
}
.btn-icon { font-size: 16px; line-height: 1; }
/* ── 项目名称 ── */
.project-name-text {
color: #295abc;
font-size: 14px;
cursor: pointer;
}
.project-name-text:hover { text-decoration: underline; }
/* ── 操作链接 ── */
.op-link {
display: inline-block;
color: #295abc;
font-size: 13px;
cursor: pointer;
margin: 0 4px;
transition: opacity 0.2s;
}
.op-link:hover { opacity: 0.8; text-decoration: underline; }
.op-danger { color: #fc4f54; }
/* ── 表格 ── */
:deep(.review-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body tr:hover > td) {
background-color: rgba(41, 90, 188, 0.04);
}
</style>

View File

@ -1,6 +1,7 @@
<template>
<ContentWrap>
<div class="meeting-edit-header">
<div class="page-title-bar">
<span class="back-btn" @click="handleBack">&#8249;</span>
<span class="page-title">{{ pageTitle }}</span>
</div>
@ -12,7 +13,7 @@
v-loading="formLoading"
>
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<div class="section-header"><span class="section-title">基本信息</span></div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议名称" prop="name">
@ -64,7 +65,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="议程附件">
<el-form-item label="议程附件" prop="agendaAttachmentUrl">
<div class="agenda-attachment-wrap">
<el-upload
v-if="!isView"
@ -73,9 +74,9 @@
accept=".pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
:on-change="handleAgendaAttachmentChange"
>
<el-button type="primary" plain size="small">上传议程附件</el-button>
<button type="button" class="btn-upload">上传议程附件</button>
</el-upload>
<el-text type="info" size="small">仅支持图片或 PDF固定单附件</el-text>
<span class="upload-hint">仅支持图片或 PDF固定单附件</span>
<div v-if="formData.agendaAttachmentUrl" class="agenda-file-line">
<el-link type="primary" :underline="false" @click="previewAgendaAttachment">
{{ formData.agendaAttachmentName }}
@ -90,7 +91,7 @@
</el-row>
<!-- 评审专家 -->
<el-divider content-position="left">评审专家</el-divider>
<div class="section-header"><span class="section-title">评审专家</span></div>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="参会专家" prop="expertIds">
@ -104,7 +105,7 @@
</el-row>
<!-- 评审项目 -->
<el-divider content-position="left">评审项目</el-divider>
<div class="section-header"><span class="section-title">评审项目</span></div>
<div v-if="!isView" class="import-section">
<el-upload
:auto-upload="false"
@ -112,14 +113,14 @@
:show-file-list="false"
accept=".xls,.xlsx"
>
<el-button type="primary" plain>导入验收申请 Excel</el-button>
<button type="button" class="btn-default">导入验收申请 Excel</button>
</el-upload>
<el-button type="success" plain @click="handleDownloadTemplate"></el-button>
<el-text type="info" size="small">格式序号开始时间结束时间议程分类项目标题汇报人报告人单位</el-text>
<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 size="small" max-height="360">
<el-table :data="formData.projects" border max-height="360" class="projects-preview-table">
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
<el-table-column label="开始时间" prop="startTime" width="80" align="center" />
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
@ -134,8 +135,10 @@
<!-- 底部操作区 -->
<div class="form-footer">
<el-button v-if="!isView" type="primary" :loading="formLoading" @click="submitForm">稿</el-button>
<el-button @click="handleBack"></el-button>
<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>
@ -156,6 +159,7 @@ import {
type ReviewMeetingSaveReqVO,
type ReviewProjectItemVO
} from '@/api/review/meeting'
import { getReviewProjectPage } from '@/api/review/project'
import { getExpertUserList } from '@/api/system/user/index'
import download from '@/utils/download'
import ExpertSelectTable from './components/ExpertSelectTable.vue'
@ -209,8 +213,10 @@ const formData = reactive<FormData>({
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' }],
agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }],
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
}
@ -219,8 +225,12 @@ const formRef = ref()
const loadDetail = async (id: number) => {
formLoading.value = true
try {
const detail = await getReviewMeeting(id)
const [detail, projectData] = await Promise.all([
getReviewMeeting(id),
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
])
Object.assign(formData, detail)
formData.projects = (projectData?.list ?? []) as ReviewProjectItemVO[]
if (detail.startTime && detail.endTime) {
formData.meetingTimeRange = [
new Date(detail.startTime.replace(' ', 'T')).getTime(),
@ -290,6 +300,7 @@ const handleAgendaAttachmentChange = async (uploadFile: UploadFile) => {
formData.agendaAttachmentUrl = attachment.url
formData.agendaAttachmentType = attachment.type
formData.agendaAttachmentSize = attachment.size
formRef.value?.clearValidate('agendaAttachmentUrl')
ElMessage.success('议程附件上传成功')
} finally {
formLoading.value = false
@ -301,6 +312,7 @@ const clearAgendaAttachment = () => {
formData.agendaAttachmentUrl = undefined
formData.agendaAttachmentType = undefined
formData.agendaAttachmentSize = undefined
formRef.value?.validateField('agendaAttachmentUrl')
}
const previewAgendaAttachment = () => {
@ -317,6 +329,7 @@ const formatFileSize = (bytes?: number): string => {
}
const submitForm = async () => {
if (formLoading.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
if (formData.meetingTimeRange?.length === 2) {
@ -326,9 +339,6 @@ const submitForm = async () => {
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
@ -358,37 +368,159 @@ const handleBack = () => {
</script>
<style scoped>
.meeting-edit-header {
margin-bottom: 16px;
}
.page-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.import-section {
margin-bottom: 10px;
/* ── 页面标题 ── */
.page-title-bar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #e1e7f0;
}
.mt-10 {
margin-top: 10px;
.back-btn {
font-size: 28px;
color: #295abc;
cursor: pointer;
line-height: 1;
font-weight: 400;
}
.back-btn:hover { opacity: 0.75; }
.page-title {
font-size: 22px;
font-weight: 600;
color: #333;
}
/* ── 分区标题 ── */
.section-header {
margin: 20px 0 14px;
}
.section-title {
position: relative;
font-size: 16px;
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: 14px;
color: #555;
}
:deep(.el-input__inner) {
font-size: 14px;
}
/* ── 议程附件 ── */
.agenda-attachment-wrap {
display: flex;
flex-direction: column;
gap: 6px;
gap: 8px;
}
.agenda-file-line {
display: flex;
align-items: center;
gap: 8px;
}
.upload-hint {
font-size: 12px;
color: #999;
}
/* ── 导入区 ── */
.import-section {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.import-hint {
font-size: 12px;
color: #999;
}
.mt-10 {
margin-top: 10px;
}
/* ── 按钮 ── */
.btn-upload {
height: 34px;
padding: 0 14px;
background-color: #fff;
border: 1px solid #295abc;
border-radius: 6px;
color: #295abc;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-upload:hover { background-color: rgba(41, 90, 188, 0.08); }
.btn-default {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.btn-default:hover {
background-color: rgba(41, 90, 188, 0.08);
border-color: #295abc;
color: #295abc;
}
.btn-primary {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 20px;
background-color: #295abc;
border: none;
border-radius: 6px;
font-size: 14px;
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: 24px;
display: flex;
gap: 10px;
gap: 12px;
}
/* ── 项目预览表格 ── */
:deep(.el-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.el-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
</style>

View File

@ -1,28 +1,45 @@
<template>
<ContentWrap>
<!-- 项目基本信息 -->
<div class="detail-header">
<span class="page-title">项目详情</span>
<!-- 页面标题带返回 -->
<div class="page-title">
<span class="back-btn" @click="handleBack">&#8249;</span>
项目详情
</div>
<el-descriptions :column="3" border size="small" class="project-info">
<el-descriptions-item label="项目标题" :span="3">
<span class="project-title-text">{{ projectInfo.projectTitle || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="会中序号">{{ projectInfo.seqNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="起止时间">
{{ projectInfo.startTime && projectInfo.endTime ? `${projectInfo.startTime} - ${projectInfo.endTime}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="议程分类">{{ projectInfo.agendaCategory || '-' }}</el-descriptions-item>
<el-descriptions-item label="汇报人">{{ projectInfo.reporter || '-' }}</el-descriptions-item>
<el-descriptions-item label="报告单位">{{ projectInfo.reporterUnit || '-' }}</el-descriptions-item>
<el-descriptions-item label="主持人">{{ projectInfo.host || '-' }}</el-descriptions-item>
<el-descriptions-item label="所属会议" :span="2">{{ projectInfo.meetingName || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 蓝色信息面板 -->
<div class="info-panel">
<div class="info-panel-title">{{ projectInfo.projectTitle || '-' }}</div>
<div class="info-panel-meta">
<div class="meta-left">
<span v-if="projectInfo.startTime || projectInfo.endTime" class="meta-tag">
{{ projectInfo.startTime || '' }}{{ projectInfo.endTime ? ` - ${projectInfo.endTime}` : '' }}
</span>
<span class="meta-item">会中序号{{ projectInfo.seqNo ?? '-' }}</span>
</div>
<div class="meta-right">所属会议{{ projectInfo.meetingName || '-' }}</div>
</div>
<div class="info-panel-footer">
<div class="info-footer-item">
<span class="footer-label">议程分类</span>
<span class="footer-value">{{ projectInfo.agendaCategory || '-' }}</span>
</div>
<div class="info-footer-item">
<span class="footer-label">报告人</span>
<span class="footer-value">
{{ projectInfo.reporter || '-' }}
<em v-if="projectInfo.reporterUnit">{{ projectInfo.reporterUnit }}</em>
</span>
</div>
<div class="info-footer-item">
<span class="footer-label">主持人</span>
<span class="footer-value">{{ projectInfo.host || '-' }}</span>
</div>
</div>
</div>
<!-- 项目资料 -->
<!-- 上传资料区 -->
<div class="section-header">
<span class="section-title">项目资料</span>
<span class="section-title">上传资料</span>
<el-upload
:auto-upload="false"
:on-change="handleFileChange"
@ -30,13 +47,13 @@
multiple
accept=".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx"
>
<el-button type="primary" plain size="small">上传资料</el-button>
<button class="btn-upload">上传资料</button>
</el-upload>
<el-text type="info" size="small">支持 docdocxxlsxlsxpdfpptpptx单文件不超过 50MB</el-text>
<span class="upload-hint">支持 docdocxxlsxlsxpdfpptpptx单文件不超过 50MB</span>
</div>
<el-table :data="fileList" v-loading="fileLoading" border size="small">
<el-table-column label="文件" prop="fileName" show-overflow-tooltip min-width="220" />
<el-table :data="fileList" v-loading="fileLoading" border class="file-table">
<el-table-column label="资料文件" prop="fileName" show-overflow-tooltip min-width="220" />
<el-table-column label="大小" width="90" align="right">
<template #default="{ row }">{{ formatFileSize(row.fileSize) }}</template>
</el-table-column>
@ -45,8 +62,8 @@
<el-table-column label="上传时间" prop="createTime" width="165" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleDownload(row)"></el-button>
<el-button type="danger" link @click="handleDelete(row)"></el-button>
<a class="op-link" @click="handleDownload(row)"></a>
<a class="op-link op-danger" @click="handleDelete(row)"></a>
</template>
</el-table-column>
</el-table>
@ -54,7 +71,7 @@
<!-- 底部操作 -->
<div class="form-footer">
<el-button @click="handleBack"></el-button>
<button class="btn-default" @click="handleBack"></button>
</div>
</ContentWrap>
</template>
@ -79,7 +96,6 @@ const router = useRouter()
const reviewMeetingId = Number(route.params.meetingId)
const reviewMeetingProjectId = Number(route.params.projectId)
// history state ProjectList.vue
const state = window.history.state || {}
const projectInfo = ref({
projectTitle: state.projectTitle as string | undefined,
@ -151,32 +167,190 @@ onMounted(() => {
</script>
<style scoped>
.detail-header {
margin-bottom: 16px;
}
/* ── 页面标题 ── */
.page-title {
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
font-size: 22px;
font-weight: 600;
color: var(--el-text-color-primary);
color: #333;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #e1e7f0;
}
.project-info {
.back-btn {
font-size: 28px;
color: #295abc;
cursor: pointer;
line-height: 1;
margin-right: 2px;
font-weight: 400;
}
.back-btn:hover { opacity: 0.75; }
/* ── 蓝色信息面板 ── */
.info-panel {
background: linear-gradient(135deg, rgba(41, 90, 188, 0.06) 0%, rgba(41, 90, 188, 0.03) 100%);
border: 1px solid rgba(41, 90, 188, 0.15);
border-radius: 8px;
padding: 18px 20px;
margin-bottom: 24px;
}
.project-title-text {
.info-panel-title {
font-size: 18px;
font-weight: 600;
color: #295abc;
margin-bottom: 10px;
}
.info-panel-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.meta-left {
display: flex;
align-items: center;
gap: 14px;
}
.meta-tag {
display: inline-block;
padding: 2px 10px;
background-color: rgba(41, 90, 188, 0.1);
color: #295abc;
border-radius: 4px;
font-size: 13px;
}
.meta-item {
font-size: 13px;
color: #666;
}
.meta-right {
font-size: 13px;
color: #666;
}
.info-panel-footer {
display: flex;
gap: 32px;
padding-top: 12px;
border-top: 1px solid rgba(41, 90, 188, 0.12);
}
.info-footer-item {
display: flex;
align-items: baseline;
gap: 8px;
}
.footer-label {
font-size: 13px;
color: #999;
white-space: nowrap;
}
.footer-value {
font-size: 14px;
color: #333;
}
.footer-value em {
font-style: normal;
color: #999;
font-size: 12px;
margin-left: 6px;
}
/* ── 资料区标题 ── */
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
margin-bottom: 12px;
}
.section-title {
font-size: 14px;
position: relative;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
color: #333;
padding-left: 10px;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
width: 3px;
border-radius: 2px;
background-color: #295abc;
}
.btn-upload {
height: 34px;
padding: 0 14px;
background-color: #fff;
border: 1px solid #295abc;
border-radius: 6px;
color: #295abc;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-upload:hover {
background-color: rgba(41, 90, 188, 0.08);
}
.upload-hint {
font-size: 12px;
color: #999;
}
/* ── 操作链接 ── */
.op-link {
display: inline-block;
color: #295abc;
font-size: 13px;
cursor: pointer;
margin: 0 4px;
transition: opacity 0.2s;
}
.op-link:hover { opacity: 0.8; text-decoration: underline; }
.op-danger { color: #fc4f54; }
/* ── 文件表格 ── */
:deep(.file-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.file-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
:deep(.file-table .el-table__body tr:hover > td) {
background-color: rgba(41, 90, 188, 0.04);
}
/* ── 底部按钮 ── */
.form-footer {
margin-top: 24px;
}
.btn-default {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 20px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.btn-default:hover {
background-color: rgba(41, 90, 188, 0.08);
border-color: #295abc;
color: #295abc;
}
</style>

View File

@ -1,61 +1,82 @@
<template>
<ContentWrap>
<!-- 面包屑信息 -->
<div class="meeting-summary">
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="会议名称">{{ meetingInfo.name }}</el-descriptions-item>
<el-descriptions-item label="会议时间">
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '' }} ~
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '' }}
</el-descriptions-item>
<el-descriptions-item label="会议地点">{{ meetingInfo.location }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="STATUS_TAG_TYPE[meetingInfo.status]">{{ STATUS_LABEL[meetingInfo.status] }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 页面标题 -->
<div class="page-title">项目列表</div>
<!-- 会议摘要信息 -->
<div class="meeting-summary-bar">
<span class="summary-label">所属会议</span>
<span class="summary-meeting-name">{{ meetingInfo.name || '-' }}</span>
<span class="summary-sep">|</span>
<span class="summary-label">会议时间</span>
<span class="summary-value">
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '-' }}
&nbsp;~&nbsp;
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '-' }}
</span>
<span class="summary-sep">|</span>
<span class="summary-label">会议地点</span>
<span class="summary-value">{{ meetingInfo.location || '-' }}</span>
<span class="summary-sep">|</span>
<span class="summary-label">状态</span>
<span :class="`status-text status-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] }}</span>
</div>
<!-- 搜索栏 -->
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" :inline="true" class="mt-10">
<el-form-item label="项目标题" prop="projectTitle">
<el-input v-model="queryParams.projectTitle" placeholder="请输入项目标题" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="议程分类" prop="agendaCategory">
<el-input v-model="queryParams.agendaCategory" placeholder="请输入议程分类" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="汇报人" prop="reporter">
<el-input v-model="queryParams.reporter" placeholder="请输入汇报人" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"></el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<div class="search-bar">
<el-input
v-model="queryParams.projectTitle"
placeholder="项目标题"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<el-input
v-model="queryParams.reporter"
placeholder="报告人"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<el-input
v-model="queryParams.agendaCategory"
placeholder="议程分类"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<button class="btn-reset" @click="resetQuery"></button>
<button class="btn-search" @click="handleQuery"></button>
</div>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain @click="openForm('create')">
<el-icon><Plus /></el-icon>
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain :disabled="selectedIds.length === 0" @click="handleDelete()">
<el-icon><Delete /></el-icon>
</el-button>
</el-col>
</el-row>
<div class="toolbar">
<button class="btn-default" @click="openForm('create')">
<span class="btn-icon">+</span> 新增评审项目
</button>
<button class="btn-default btn-danger" :disabled="selectedIds.length === 0" @click="handleDelete()">
批量删除
</button>
</div>
<!-- 列表 -->
<el-table v-loading="loading" :data="list" border @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" prop="id" width="80" align="center" />
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
<el-table-column label="开始时间" prop="startTime" width="80" align="center" />
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
<el-table v-loading="loading" :data="list" border class="review-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="立项编号" prop="id" width="80" align="center" />
<el-table-column label="会中序号" prop="seqNo" width="80" align="center" />
<el-table-column label="起止时间" width="110" align="center">
<template #default="{ row }">
<span v-if="row.startTime || row.endTime">{{ row.startTime || '' }} - {{ row.endTime || '' }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="议程分类" prop="agendaCategory" width="110" />
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip min-width="180" />
<el-table-column label="汇报人" prop="reporter" width="80" />
<el-table-column label="项目名称" prop="projectTitle" show-overflow-tooltip min-width="180">
<template #default="{ row }">
<span class="project-name-text">{{ row.projectTitle }}</span>
</template>
</el-table-column>
<el-table-column label="报告人" prop="reporter" width="80" />
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
<el-table-column label="主持人" width="150">
<template #default="{ row }">
@ -67,11 +88,11 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="openForm('update', row)">编辑</el-button>
<el-button type="primary" link @click="goToDetail(row)"></el-button>
<el-button type="danger" link @click="handleDelete(row.id)"></el-button>
<a class="op-link" @click="openForm('update', row)">编辑</a>
<a class="op-link" @click="goToDetail(row)"></a>
<a class="op-link op-danger" @click="handleDelete(row.id)"></a>
</template>
</el-table-column>
</el-table>
@ -113,9 +134,6 @@
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 文件管理弹窗 -->
<FileListDialog ref="fileDialogRef" :review-meeting-id="reviewMeetingId" />
</ContentWrap>
</template>
@ -123,10 +141,8 @@
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue'
import { getReviewMeeting } from '@/api/review/meeting'
import { getReviewProjectPage, updateProjectHost, updateReviewProject, createReviewProject, deleteReviewProject, type ReviewMeetingProjectRespVO } from '@/api/review/project'
import FileListDialog from './FileListDialog.vue'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'ReviewMeetingProject' })
@ -141,7 +157,6 @@ const total = ref(0)
const meetingInfo = ref<any>({})
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
const STATUS_TAG_TYPE: Record<number, string> = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger' }
const queryParams = reactive({
pageNo: 1,
@ -152,9 +167,6 @@ const queryParams = reactive({
reporter: undefined as string | undefined
})
const queryFormRef = ref()
const fileDialogRef = ref()
const getList = async () => {
loading.value = true
try {
@ -167,7 +179,12 @@ const getList = async () => {
}
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery() }
const resetQuery = () => {
queryParams.projectTitle = undefined
queryParams.agendaCategory = undefined
queryParams.reporter = undefined
handleQuery()
}
const selectedIds = ref<number[]>([])
const handleSelectionChange = (val: ReviewMeetingProjectRespVO[]) => {
@ -206,7 +223,6 @@ const openForm = (type: 'create' | 'update', row?: ReviewMeetingProjectRespVO) =
Object.assign(formData, row)
}
formVisible.value = true
// Reset form validation state if needed by deferring to next tick
}
const submitForm = async () => {
@ -228,10 +244,6 @@ const submitForm = async () => {
}
}
const openFileDialog = (row: ReviewMeetingProjectRespVO) => {
fileDialogRef.value?.open(row.id, row.projectTitle)
}
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
router.push({
name: 'ReviewProjectDetail',
@ -257,6 +269,172 @@ onMounted(async () => {
</script>
<style scoped>
.meeting-summary { margin-bottom: 16px; }
.mt-10 { margin-top: 10px; }
/* ── 页面标题 ── */
.page-title {
font-size: 22px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px solid #e1e7f0;
}
/* ── 会议摘要栏 ── */
.meeting-summary-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 10px 14px;
background-color: rgba(41, 90, 188, 0.05);
border: 1px solid rgba(41, 90, 188, 0.12);
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
color: #333;
}
.summary-label {
color: #666;
}
.summary-meeting-name {
color: #295abc;
font-weight: 500;
}
.summary-value {
color: #333;
}
.summary-sep {
color: #ddd;
margin: 0 4px;
}
/* ── 状态文字 ── */
.status-text { font-size: 14px; font-weight: 500; }
.status-0 { color: #ecae4b; }
.status-1 { color: #73c047; }
.status-2 { color: #999; }
.status-3 { color: #999; }
/* ── 搜索栏 ── */
.search-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.search-input {
width: 180px;
}
:deep(.search-input .el-input__wrapper) {
height: 40px;
border-radius: 6px;
border-color: #dcdedf;
}
:deep(.search-input .el-input__inner) {
font-size: 15px;
color: #333;
}
.btn-search {
height: 40px;
padding: 0 22px;
background-color: #295abc;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-search:hover { background-color: rgba(41, 90, 188, 0.88); }
.btn-reset {
height: 40px;
padding: 0 8px;
background: none;
border: none;
color: #295abc;
font-size: 15px;
cursor: pointer;
}
.btn-reset:hover { opacity: 0.8; }
/* ── 工具栏 ── */
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.btn-default {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.btn-default:hover {
background-color: rgba(41, 90, 188, 0.08);
border-color: #295abc;
color: #295abc;
}
.btn-default:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-danger {
border-color: #fc4f54;
color: #fc4f54;
}
.btn-danger:hover {
background-color: rgba(252, 79, 84, 0.08);
border-color: #fc4f54;
color: #fc4f54;
}
.btn-icon {
font-size: 16px;
line-height: 1;
}
/* ── 项目名称 ── */
.project-name-text {
color: #295abc;
font-size: 14px;
}
/* ── 操作链接 ── */
.op-link {
display: inline-block;
color: #295abc;
font-size: 13px;
cursor: pointer;
margin: 0 4px;
transition: opacity 0.2s;
}
.op-link:hover { opacity: 0.8; text-decoration: underline; }
.op-danger { color: #fc4f54; }
/* ── 表格 ── */
:deep(.review-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body tr:hover > td) {
background-color: rgba(41, 90, 188, 0.04);
}
</style>

View File

@ -77,6 +77,7 @@ const emit = defineEmits<{
const tableRef = ref()
const searchKeyword = ref('')
const isUpdating = ref(false)
const filteredExperts = computed(() => {
const kw = searchKeyword.value.trim().toLowerCase()
@ -99,17 +100,20 @@ watch(
async () => {
await nextTick()
if (!tableRef.value) return
isUpdating.value = true
tableRef.value.clearSelection()
filteredExperts.value.forEach((row) => {
if (props.modelValue.includes(row.id)) {
tableRef.value.toggleRowSelection(row, true)
}
})
isUpdating.value = false
},
{ immediate: true }
)
const handleSelectionChange = (selected: Expert[]) => {
if (isUpdating.value) return
const selectedIds = selected.map((e) => e.id)
//
const filteredIds = filteredExperts.value.map((e) => e.id)
@ -127,23 +131,66 @@ const removeExpert = (id: number) => {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
}
/* 已选标签行 */
.selected-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
gap: 8px;
padding: 8px 12px;
background-color: rgba(41, 90, 188, 0.04);
border: 1px solid rgba(41, 90, 188, 0.12);
border-radius: 6px;
}
.selected-label {
font-size: 13px;
color: var(--el-text-color-secondary);
font-size: 14px;
color: #666;
white-space: nowrap;
}
.expert-tag {
margin: 0;
background-color: rgba(41, 90, 188, 0.1);
border-color: rgba(41, 90, 188, 0.2);
color: #295abc;
}
/* 搜索框 */
.search-input {
max-width: 320px;
}
:deep(.search-input .el-input__wrapper) {
height: 38px;
border-radius: 6px;
border-color: #dcdedf;
}
:deep(.search-input .el-input__inner) {
font-size: 14px;
}
/* 专家表格 */
:deep(.el-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.el-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
:deep(.el-table .el-table__body tr.active > td) {
background-color: rgba(41, 90, 188, 0.06);
}
:deep(.el-table .el-table__body .el-checkbox__inner) {
border-color: #295abc;
}
:deep(.el-table .el-table__body .el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #295abc;
border-color: #295abc;
}
</style>

View File

@ -1,105 +1,82 @@
<template>
<ContentWrap>
<!-- 页面标题 -->
<div class="page-title">会议列表</div>
<!-- 搜索栏 -->
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" :inline="true">
<el-form-item label="会议名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入会议名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="item in MEETING_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="会议时间" prop="startTime">
<el-date-picker
v-model="queryParams.startTime"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"></el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<div class="search-bar">
<el-input
v-model="queryParams.name"
placeholder="搜索会议名称"
clearable
class="search-input"
@keyup.enter="handleQuery"
/>
<el-date-picker
v-model="queryParams.startTime"
type="datetimerange"
range-separator="至"
start-placeholder="会议开始时间"
end-placeholder="会议结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
class="search-datepicker"
/>
<el-select v-model="queryParams.status" 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>
</div>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button v-hasPermi="['review:meeting:create']" type="primary" plain @click="goToEdit()">
<el-icon><Plus /></el-icon>
</el-button>
</el-col>
</el-row>
<div class="toolbar">
<button v-hasPermi="['review:meeting:create']" class="btn-default" @click="goToEdit()">
<span class="btn-icon">+</span> 新建会议
</button>
</div>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" prop="id" width="80" align="center" />
<el-table-column label="会议名称" min-width="180">
<el-table v-loading="loading" :data="list" class="review-table" border>
<el-table-column label="编号" prop="id" width="70" align="center" />
<el-table-column label="会议名称" min-width="200">
<template #default="{ row }">
<el-link type="primary" @click="goToProjectList(row)">{{ row.name }}</el-link>
<span class="meeting-name-link" @click="goToProjectList(row)">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column label="开始时间" width="145">
<template #default="{ row }">
{{ formatDate(row.startTime, 'YYYY-MM-DD HH:mm') }}
</template>
<el-table-column label="会议开始时间" width="150">
<template #default="{ row }">{{ formatDate(row.startTime, 'YYYY-MM-DD HH:mm') }}</template>
</el-table-column>
<el-table-column label="结束时间" width="145">
<template #default="{ row }">
{{ formatDate(row.endTime, 'YYYY-MM-DD HH:mm') }}
</template>
<el-table-column label="会议结束时间" width="150">
<template #default="{ row }">{{ formatDate(row.endTime, 'YYYY-MM-DD HH:mm') }}</template>
</el-table-column>
<el-table-column label="会议地点" prop="location" show-overflow-tooltip />
<el-table-column label="参会专家数" prop="expertCount" width="100" align="center" />
<el-table-column label="评审项目数" prop="projectCount" width="100" align="center" />
<el-table-column label="状态" prop="status" width="100" align="center">
<el-table-column label="项目数" prop="projectCount" width="80" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="STATUS_TAG_TYPE[row.status]">{{ STATUS_LABEL[row.status] }}</el-tag>
<span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="380" align="center" fixed="right">
<el-table-column label="操作" width="300" align="center" fixed="right">
<template #default="{ row }">
<!-- 待召开 -->
<template v-if="row.status === 0">
<el-button v-hasPermi="['review:meeting:update']" type="primary" link @click="goToEdit(row.id)"></el-button>
<el-button type="primary" link @click="goToProjectList(row)"></el-button>
<el-button v-hasPermi="['review:meeting:send-sms']" type="success" link @click="handleSendSms(row)"></el-button>
<el-button v-hasPermi="['review:meeting:cancel']" type="danger" link @click="handleCancel(row)"></el-button>
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="goToEdit(row.id)"></a>
<a class="op-link" @click="goToProjectList(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>
</template>
<!-- 正在召开 -->
<template v-else-if="row.status === 1">
<el-button type="info" link @click="goToEdit(row.id, 'view')">查看</el-button>
<el-button type="primary" link @click="goToProjectList(row)"></el-button>
<el-button v-hasPermi="['review:meeting:send-sms']" type="info" link @click="openSmsStatus(row)"></el-button>
<el-button
v-if="row.mailSent"
v-hasPermi="['review:meeting:send-mail']"
type="info"
link
@click="openMailStatus(row)"
>
邮件状态
</el-button>
<el-button
v-else
v-hasPermi="['review:meeting:send-mail']"
type="primary"
link
@click="handleSendMail(row)"
>
发送议程
</el-button>
<el-button v-hasPermi="['review:meeting:finish']" type="warning" link @click="handleFinish(row)"></el-button>
<el-button v-hasPermi="['review:meeting:cancel']" type="danger" link @click="handleCancel(row)"></el-button>
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a>
<a class="op-link" @click="goToProjectList(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:finish']" class="op-link" @click="handleFinish(row)"></a>
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)"></a>
</template>
<!-- 终态已结束/已取消 -->
<template v-else>
<el-button type="info" link @click="goToEdit(row.id, 'view')">查看</el-button>
<el-button type="primary" link @click="goToProjectList(row)"></el-button>
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a>
<a class="op-link" @click="goToProjectList(row)"></a>
</template>
</template>
</el-table-column>
@ -120,7 +97,6 @@
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
getReviewMeetingPage,
cancelReviewMeeting,
@ -148,7 +124,6 @@ const MEETING_STATUS_OPTIONS = [
{ value: 3, label: '已取消' }
]
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
const STATUS_TAG_TYPE: Record<number, string> = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger' }
const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize: number }>({
pageNo: 1,
@ -174,7 +149,12 @@ const getList = async () => {
}
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery() }
const resetQuery = () => {
queryParams.name = undefined
queryParams.status = undefined
queryParams.startTime = undefined
handleQuery()
}
const goToEdit = (id?: number, mode?: string) => {
if (id) {
@ -234,3 +214,161 @@ const openMailStatus = (row: ReviewMeetingRespVO) => {
onMounted(() => getList())
</script>
<style scoped>
/* ── 页面标题 ── */
.page-title {
font-size: 22px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #e1e7f0;
}
/* ── 搜索栏 ── */
.search-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.search-input {
width: 220px;
}
.search-datepicker {
width: 340px;
}
.search-select {
width: 140px;
}
:deep(.search-input .el-input__wrapper),
:deep(.search-select .el-input__wrapper) {
height: 40px;
border-radius: 6px;
border-color: #dcdedf;
font-size: 15px;
}
:deep(.search-datepicker.el-date-editor) {
height: 40px;
border-radius: 6px;
border-color: #dcdedf;
}
:deep(.search-input .el-input__inner),
:deep(.search-select .el-input__inner) {
font-size: 15px;
color: #333;
}
.btn-search {
height: 40px;
padding: 0 22px;
background-color: #295abc;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-search:hover {
background-color: rgba(41, 90, 188, 0.88);
}
.btn-reset {
height: 40px;
padding: 0 8px;
background: none;
border: none;
color: #295abc;
font-size: 15px;
cursor: pointer;
}
.btn-reset:hover {
opacity: 0.8;
}
/* ── 工具栏 ── */
.toolbar {
margin-bottom: 12px;
}
.btn-default {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 6px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.btn-default:hover {
background-color: rgba(41, 90, 188, 0.08);
border-color: #295abc;
color: #295abc;
}
.btn-icon {
font-size: 16px;
line-height: 1;
}
/* ── 状态文字 ── */
.status-text {
font-size: 14px;
font-weight: 500;
}
.status-0 { color: #ecae4b; }
.status-1 { color: #73c047; }
.status-2 { color: #999; }
.status-3 { color: #999; }
/* ── 会议名称链接 ── */
.meeting-name-link {
color: #295abc;
cursor: pointer;
font-size: 14px;
}
.meeting-name-link:hover {
color: rgba(41, 90, 188, 0.85);
text-decoration: underline;
}
/* ── 操作链接 ── */
.op-link {
display: inline-block;
color: #295abc;
font-size: 13px;
cursor: pointer;
margin: 0 4px;
transition: opacity 0.2s;
}
.op-link:hover {
opacity: 0.8;
text-decoration: underline;
}
.op-danger {
color: #fc4f54;
}
/* ── 表格样式 ── */
:deep(.review-table .el-table__header-wrapper th) {
background-color: #eef2fb;
color: #333;
font-weight: 600;
font-size: 14px;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body td) {
font-size: 14px;
color: #333;
border-color: #e1e7f0;
}
:deep(.review-table .el-table__body tr:hover > td) {
background-color: rgba(41, 90, 188, 0.04);
}
</style>