feat(meeting-ui): merge feature/meeting-ui-redesign → master
会议管理 UI 重构,6 个 commit: - 会议列表页 UI 重构:状态文案对齐、操作入口改为路由跳转 - 新增会议编辑全页面 MeetingEdit.vue,替代弹窗形式 - 新增专家选择组件 ExpertSelectTable(搜索 + checkbox 多选) - 项目列表页改造:状态文案对齐、操作列接入项目详情跳转 - 新增项目详情页 ProjectDetail.vue + 路由 - 修复若干 bug Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>pull/874/head
commit
302772ecb9
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -747,6 +747,42 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||
activeMenu: '/review/meeting'
|
||||
},
|
||||
component: () => import('@/views/review/meeting/ProjectList.vue')
|
||||
},
|
||||
{
|
||||
path: 'review-meeting/edit',
|
||||
name: 'ReviewMeetingEdit',
|
||||
meta: {
|
||||
title: '新建会议',
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
activeMenu: '/review/meeting'
|
||||
},
|
||||
component: () => import('@/views/review/meeting/MeetingEdit.vue')
|
||||
},
|
||||
{
|
||||
path: 'review-meeting/edit/:id(\\d+)',
|
||||
name: 'ReviewMeetingEditById',
|
||||
meta: {
|
||||
title: '编辑会议',
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
activeMenu: '/review/meeting'
|
||||
},
|
||||
component: () => import('@/views/review/meeting/MeetingEdit.vue')
|
||||
},
|
||||
{
|
||||
path: 'review-meeting/project/:meetingId(\\d+)/detail/:projectId(\\d+)',
|
||||
name: 'ReviewProjectDetail',
|
||||
meta: {
|
||||
title: '项目详情',
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
activeMenu: '/review/meeting'
|
||||
},
|
||||
component: () => import('@/views/review/meeting/ProjectDetail.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<div class="page-title-bar">
|
||||
<span class="back-btn" @click="handleBack">‹</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-input v-model="formData.location" placeholder="请输入会议地点" :disabled="isView" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
|
||||
<el-date-picker
|
||||
v-model="formData.materialViewTimeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="x"
|
||||
:disabled="isView"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="议程附件" prop="agendaAttachmentUrl">
|
||||
<div class="agenda-attachment-wrap">
|
||||
<el-upload
|
||||
v-if="!isView"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
|
||||
:on-change="handleAgendaAttachmentChange"
|
||||
>
|
||||
<button type="button" class="btn-upload">上传议程附件</button>
|
||||
</el-upload>
|
||||
<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 }}
|
||||
</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-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="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-column label="议程分类" prop="agendaCategory" width="110" />
|
||||
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
|
||||
<el-table-column label="汇报人" prop="reporter" width="80" />
|
||||
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
|
||||
</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 } 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,
|
||||
uploadAgendaAttachment,
|
||||
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'
|
||||
|
||||
defineOptions({ name: 'ReviewMeetingEdit' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const formLoading = ref(false)
|
||||
const expertOptions = ref<any[]>([])
|
||||
const isProjectsModified = ref(false)
|
||||
|
||||
// 从路由参数判断模式
|
||||
const meetingId = computed(() => {
|
||||
const id = route.params.id
|
||||
return id ? Number(id) : undefined
|
||||
})
|
||||
const isView = computed(() => route.query.mode === 'view')
|
||||
const isEdit = computed(() => !!meetingId.value && !isView.value)
|
||||
const pageTitle = computed(() => {
|
||||
if (isView.value) return '查看会议'
|
||||
return isEdit.value ? '编辑会议' : '新建会议'
|
||||
})
|
||||
|
||||
type FormData = ReviewMeetingSaveReqVO & {
|
||||
organizationUnit?: string
|
||||
meetingTimeRange?: any[]
|
||||
materialViewTimeRange?: any[]
|
||||
}
|
||||
|
||||
const formData = reactive<FormData>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
organizationUnit: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
location: '',
|
||||
agendaAttachmentName: undefined,
|
||||
agendaAttachmentUrl: undefined,
|
||||
agendaAttachmentType: undefined,
|
||||
agendaAttachmentSize: undefined,
|
||||
materialViewStartTime: undefined,
|
||||
materialViewEndTime: undefined,
|
||||
materialViewRemark: undefined,
|
||||
expertIds: [],
|
||||
meetingTimeRange: undefined,
|
||||
materialViewTimeRange: 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' }],
|
||||
agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }],
|
||||
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
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 = (projectData?.list ?? []) as ReviewProjectItemVO[]
|
||||
if (detail.startTime && detail.endTime) {
|
||||
formData.meetingTimeRange = [
|
||||
new Date(detail.startTime.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 {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
expertOptions.value = await getExpertUserList().catch(() => [])
|
||||
if (meetingId.value) {
|
||||
await loadDetail(meetingId.value)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
formData.projects = result as ReviewProjectItemVO[]
|
||||
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 isPdf = ext === 'pdf' || uploadFile.raw.type === 'application/pdf'
|
||||
const isImage = uploadFile.raw.type?.startsWith('image/')
|
||||
if (!isPdf && !isImage) {
|
||||
ElMessage.error('议程附件仅支持图片或 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 previewAgendaAttachment = () => {
|
||||
if (formData.agendaAttachmentUrl) {
|
||||
window.open(formData.agendaAttachmentUrl, '_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 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]
|
||||
}
|
||||
if (formData.materialViewTimeRange?.length === 2) {
|
||||
formData.materialViewStartTime = formData.materialViewTimeRange[0]
|
||||
formData.materialViewEndTime = formData.materialViewTimeRange[1]
|
||||
} else {
|
||||
formData.materialViewStartTime = undefined
|
||||
formData.materialViewEndTime = undefined
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const submitData = { ...formData }
|
||||
if (isEdit.value && !isProjectsModified.value) {
|
||||
delete submitData.projects
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await updateReviewMeeting(submitData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createReviewMeeting(submitData)
|
||||
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: 20px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #e1e7f0;
|
||||
}
|
||||
.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: 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: 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>
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 页面标题带返回 -->
|
||||
<div class="page-title">
|
||||
<span class="back-btn" @click="handleBack">‹</span>
|
||||
项目详情
|
||||
</div>
|
||||
|
||||
<!-- 蓝色信息面板 -->
|
||||
<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>
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="false"
|
||||
multiple
|
||||
accept=".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx"
|
||||
>
|
||||
<button class="btn-upload">上传资料</button>
|
||||
</el-upload>
|
||||
<span class="upload-hint">支持 doc、docx、xls、xlsx、pdf、ppt、pptx,单文件不超过 50MB</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<el-table-column label="类型" prop="fileType" width="70" align="center" />
|
||||
<el-table-column label="上传人" prop="creator" width="90" />
|
||||
<el-table-column label="上传时间" prop="createTime" width="165" />
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<a class="op-link" @click="handleDownload(row)">下载</a>
|
||||
<a class="op-link op-danger" @click="handleDelete(row)">删除</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!fileLoading && fileList.length === 0" description="暂无资料文件" :image-size="60" />
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="form-footer">
|
||||
<button class="btn-default" @click="handleBack">返回</button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import {
|
||||
getMeetingFileList,
|
||||
uploadMeetingFile,
|
||||
deleteMeetingFile,
|
||||
type ReviewMeetingFileRespVO
|
||||
} from '@/api/review/project'
|
||||
|
||||
defineOptions({ name: 'ReviewProjectDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const reviewMeetingId = Number(route.params.meetingId)
|
||||
const reviewMeetingProjectId = Number(route.params.projectId)
|
||||
|
||||
const state = window.history.state || {}
|
||||
const projectInfo = ref({
|
||||
projectTitle: state.projectTitle as string | undefined,
|
||||
seqNo: state.seqNo as number | undefined,
|
||||
startTime: state.startTime as string | undefined,
|
||||
endTime: state.endTime as string | undefined,
|
||||
agendaCategory: state.agendaCategory as string | undefined,
|
||||
reporter: state.reporter as string | undefined,
|
||||
reporterUnit: state.reporterUnit as string | undefined,
|
||||
host: state.host as string | undefined,
|
||||
meetingName: state.meetingName as string | undefined
|
||||
})
|
||||
|
||||
const fileList = ref<ReviewMeetingFileRespVO[]>([])
|
||||
const fileLoading = ref(false)
|
||||
|
||||
const loadFiles = async () => {
|
||||
fileLoading.value = true
|
||||
try {
|
||||
fileList.value = await getMeetingFileList(reviewMeetingProjectId)
|
||||
} finally {
|
||||
fileLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) return
|
||||
if (uploadFile.raw.size > 50 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过 50MB')
|
||||
return
|
||||
}
|
||||
fileLoading.value = true
|
||||
try {
|
||||
await uploadMeetingFile(reviewMeetingId, reviewMeetingProjectId, uploadFile.raw)
|
||||
ElMessage.success('上传成功')
|
||||
await loadFiles()
|
||||
} catch {
|
||||
ElMessage.error('上传失败')
|
||||
} finally {
|
||||
fileLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = (row: ReviewMeetingFileRespVO) => {
|
||||
window.open(row.fileUrl, '_blank')
|
||||
}
|
||||
|
||||
const handleDelete = async (row: ReviewMeetingFileRespVO) => {
|
||||
await ElMessageBox.confirm(`确定要删除文件「${row.fileName}」吗?`, '提示', { type: 'warning' })
|
||||
await deleteMeetingFile(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
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 handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFiles()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ── 页面标题 ── */
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #e1e7f0;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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: 12px;
|
||||
}
|
||||
.section-title {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
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>
|
||||
|
|
@ -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') : '-' }}
|
||||
~
|
||||
{{ 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="160" 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="openFileDialog(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,25 +134,21 @@
|
|||
|
||||
<!-- 分页 -->
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 文件管理弹窗 -->
|
||||
<FileListDialog ref="fileDialogRef" :review-meeting-id="reviewMeetingId" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
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' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const reviewMeetingId = Number(route.params.meetingId)
|
||||
|
||||
const loading = ref(false)
|
||||
|
|
@ -139,8 +156,7 @@ const list = ref<ReviewMeetingProjectRespVO[]>([])
|
|||
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: 'info', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
|
|
@ -151,9 +167,6 @@ const queryParams = reactive({
|
|||
reporter: undefined as string | undefined
|
||||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const fileDialogRef = ref()
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -166,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[]) => {
|
||||
|
|
@ -205,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 () => {
|
||||
|
|
@ -227,8 +244,22 @@ const submitForm = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const openFileDialog = (row: ReviewMeetingProjectRespVO) => {
|
||||
fileDialogRef.value?.open(row.id, row.projectTitle)
|
||||
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
||||
router.push({
|
||||
name: 'ReviewProjectDetail',
|
||||
params: { meetingId: 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: meetingInfo.value?.name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -238,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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div class="expert-select-wrap">
|
||||
<!-- 已选标签行 -->
|
||||
<div v-if="selectedExperts.length > 0" class="selected-row">
|
||||
<span class="selected-label">已选 {{ selectedExperts.length }} 人:</span>
|
||||
<el-tag
|
||||
v-for="expert in selectedExperts"
|
||||
:key="expert.id"
|
||||
closable
|
||||
:disable-transitions="false"
|
||||
class="expert-tag"
|
||||
@close="removeExpert(expert.id)"
|
||||
>
|
||||
{{ expert.nickname }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<el-input
|
||||
v-if="!disabled"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索姓名、单位、职称"
|
||||
clearable
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 专家表格 -->
|
||||
<el-table
|
||||
v-if="!disabled"
|
||||
ref="tableRef"
|
||||
:data="filteredExperts"
|
||||
border
|
||||
size="small"
|
||||
max-height="300"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="姓名" prop="nickname" width="100" />
|
||||
<el-table-column label="单位" prop="deptName" show-overflow-tooltip />
|
||||
<el-table-column label="职称" prop="title" width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 查看模式:仅展示已选专家列表 -->
|
||||
<el-table v-if="disabled && selectedExperts.length > 0" :data="selectedExperts" border size="small">
|
||||
<el-table-column label="姓名" prop="nickname" width="100" />
|
||||
<el-table-column label="单位" prop="deptName" show-overflow-tooltip />
|
||||
<el-table-column label="职称" prop="title" width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-empty v-if="disabled && selectedExperts.length === 0" description="暂无参会专家" :image-size="60" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
interface Expert {
|
||||
id: number
|
||||
nickname: string
|
||||
deptName?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number[]
|
||||
experts: Expert[]
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: number[]): void
|
||||
}>()
|
||||
|
||||
const tableRef = ref()
|
||||
const searchKeyword = ref('')
|
||||
const isUpdating = ref(false)
|
||||
|
||||
const filteredExperts = computed(() => {
|
||||
const kw = searchKeyword.value.trim().toLowerCase()
|
||||
if (!kw) return props.experts
|
||||
return props.experts.filter(
|
||||
(e) =>
|
||||
e.nickname?.toLowerCase().includes(kw) ||
|
||||
e.deptName?.toLowerCase().includes(kw) ||
|
||||
e.title?.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
|
||||
const selectedExperts = computed(() =>
|
||||
props.experts.filter((e) => props.modelValue.includes(e.id))
|
||||
)
|
||||
|
||||
// 同步表格选中状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
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)
|
||||
const keptIds = props.modelValue.filter((id) => !filteredIds.includes(id))
|
||||
emit('update:modelValue', [...new Set([...keptIds, ...selectedIds])])
|
||||
}
|
||||
|
||||
const removeExpert = (id: number) => {
|
||||
emit('update:modelValue', props.modelValue.filter((v) => v !== id))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.expert-select-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 已选标签行 */
|
||||
.selected-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
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: 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>
|
||||
|
|
@ -1,107 +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="openForm('create')">
|
||||
<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="130" align="center">
|
||||
<el-table-column label="操作" width="300" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="goToProjectList(row)">进入项目列表</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="430" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 草稿状态 -->
|
||||
<template v-if="row.status === 0">
|
||||
<el-button v-hasPermi="['review:meeting:update']" type="primary" link @click="openForm('update', 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="openForm('view', 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="openForm('view', 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>
|
||||
|
|
@ -110,9 +85,6 @@
|
|||
<!-- 分页 -->
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<MeetingForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 短信状态弹窗 -->
|
||||
<SmsStatusDialog ref="smsStatusRef" />
|
||||
|
||||
|
|
@ -125,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,
|
||||
|
|
@ -135,7 +106,6 @@ import {
|
|||
type ReviewMeetingRespVO,
|
||||
type ReviewMeetingPageReqVO
|
||||
} from '@/api/review/meeting'
|
||||
import MeetingForm from './MeetingForm.vue'
|
||||
import SmsStatusDialog from './SmsStatusDialog.vue'
|
||||
import MailStatusDialog from './MailStatusDialog.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
|
@ -148,13 +118,12 @@ const list = ref<ReviewMeetingRespVO[]>([])
|
|||
const total = ref(0)
|
||||
|
||||
const MEETING_STATUS_OPTIONS = [
|
||||
{ value: 0, label: '草稿' },
|
||||
{ value: 1, label: '已邀约' },
|
||||
{ value: 0, label: '待召开' },
|
||||
{ value: 1, label: '正在召开' },
|
||||
{ value: 2, label: '已结束' },
|
||||
{ value: 3, label: '已取消' }
|
||||
]
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '草稿', 1: '已邀约', 2: '已结束', 3: '已取消' }
|
||||
const STATUS_TAG_TYPE: Record<number, string> = { 0: 'info', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||
|
||||
const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize: number }>({
|
||||
pageNo: 1,
|
||||
|
|
@ -165,7 +134,6 @@ const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize
|
|||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const smsStatusRef = ref()
|
||||
const mailStatusRef = ref()
|
||||
|
||||
|
|
@ -181,10 +149,19 @@ 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 openForm = (type: 'create' | 'update' | 'view', row?: ReviewMeetingRespVO) => {
|
||||
formRef.value?.open(type, row?.id)
|
||||
const goToEdit = (id?: number, mode?: string) => {
|
||||
if (id) {
|
||||
router.push({ name: 'ReviewMeetingEditById', params: { id }, query: mode ? { mode } : {} })
|
||||
} else {
|
||||
router.push({ name: 'ReviewMeetingEdit' })
|
||||
}
|
||||
}
|
||||
|
||||
const goToProjectList = (row: ReviewMeetingRespVO) => {
|
||||
|
|
@ -224,7 +201,7 @@ const handleSendMail = async (row: ReviewMeetingRespVO) => {
|
|||
)
|
||||
await sendMailInvitation(row.id)
|
||||
row.mailSent = true
|
||||
ElMessage.success('邮件邀请函发送任务已触发,请点击“邮件状态”查看结果')
|
||||
ElMessage.success('邮件邀请函发送任务已触发,请点击"邮件状态"查看结果')
|
||||
}
|
||||
|
||||
const openSmsStatus = (row: ReviewMeetingRespVO) => {
|
||||
|
|
@ -237,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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue