feat(review-meeting): 优化项目排程与导入展示

pull/874/head
Codewoc 2026-03-31 10:39:20 +08:00
parent 9113f446e6
commit 9251c64067
7 changed files with 348 additions and 53 deletions

View File

@ -10,12 +10,13 @@ import * as FileApi from '@/api/infra/file'
export interface ReviewProjectItemVO { export interface ReviewProjectItemVO {
sourceProjectId?: number sourceProjectId?: number
seqNo: number seqNo: number
startTime: string startTime?: string
endTime: string endTime?: string
agendaCategory: string agendaCategory: string
projectTitle: string projectTitle: string
reporter: string reporter: string
reporterUnit: string reporterUnit: string
reviewDate?: string
} }
// 会议保存 VO新增/编辑) // 会议保存 VO新增/编辑)

View File

@ -33,6 +33,19 @@ export interface ReviewMeetingProjectSeqUpdateReqVO {
seqNo: number seqNo: number
} }
export interface ReviewMeetingProjectResultBatchUpdateReqVO {
ids: number[]
reviewResult: 'PASS' | 'REJECT'
}
export interface ReviewMeetingProjectTimeBatchUpdateReqVO {
items: Array<{
id: number
startTime: string
endTime: string
}>
}
export interface ReviewMeetingProjectPageReqVO { export interface ReviewMeetingProjectPageReqVO {
pageNo?: number pageNo?: number
pageSize?: number pageSize?: number
@ -141,6 +154,14 @@ export const updateReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =
export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) => export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) =>
request.put({ url: '/project/review-project/update-seq-batch', data }) request.put({ url: '/project/review-project/update-seq-batch', data })
/** 批量更新评审结果 */
export const updateReviewProjectResultBatch = (data: ReviewMeetingProjectResultBatchUpdateReqVO) =>
request.put({ url: '/project/review-project/update-result-batch', data })
/** 批量更新评审项目起止时间 */
export const updateReviewProjectTimeBatch = (data: ReviewMeetingProjectTimeBatchUpdateReqVO) =>
request.put({ url: '/project/review-project/update-time-batch', data })
/** 删除评审项目 */ /** 删除评审项目 */
export const deleteReviewProject = (ids: number[]) => export const deleteReviewProject = (ids: number[]) =>
request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } }) request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } })

View File

@ -157,14 +157,17 @@
<button type="button" class="btn-default">导入验收申请 Excel</button> <button type="button" class="btn-default">导入验收申请 Excel</button>
</el-upload> </el-upload>
<button type="button" class="btn-default" @click="handleDownloadTemplate"></button> <button type="button" class="btn-default" @click="handleDownloadTemplate"></button>
<span class="import-hint">格式序号开始时间结束时间议程分类项目标题汇报人报告人单位</span> <span class="import-hint">格式序号议程分类项目标题汇报人报告人单位</span>
</div> </div>
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10"> <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 :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="seqNo" width="60" align="center" />
<el-table-column label="开始时间" prop="startTime" width="80" align="center" /> <el-table-column label="起止时间" width="120" align="center">
<el-table-column label="结束时间" prop="endTime" width="80" align="center" /> <template #default="{ row }">
{{ row.startTime || '--:--' }} - {{ row.endTime || '--:--' }}
</template>
</el-table-column>
<el-table-column label="议程分类" prop="agendaCategory" width="110" /> <el-table-column label="议程分类" prop="agendaCategory" width="110" />
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip /> <el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
<el-table-column label="汇报人" prop="reporter" width="80" /> <el-table-column label="汇报人" prop="reporter" width="80" />
@ -205,6 +208,11 @@ import { getReviewProjectPage } from '@/api/review/project'
import { getExpertUserList } from '@/api/system/user/index' import { getExpertUserList } from '@/api/system/user/index'
import download from '@/utils/download' import download from '@/utils/download'
import ExpertSelectTable from './components/ExpertSelectTable.vue' import ExpertSelectTable from './components/ExpertSelectTable.vue'
import { applyDefaultReviewDate } from './projectReviewDate'
import {
buildScheduledProjectItems,
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
} from './projectSchedule'
defineOptions({ name: 'ReviewMeetingEdit' }) defineOptions({ name: 'ReviewMeetingEdit' })
@ -288,7 +296,14 @@ const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] =>
agendaCategory: item.agendaCategory, agendaCategory: item.agendaCategory,
projectTitle: item.projectTitle, projectTitle: item.projectTitle,
reporter: item.reporter, reporter: item.reporter,
reporterUnit: item.reporterUnit reporterUnit: item.reporterUnit,
reviewDate: item.reviewDate
}))
const resetProjectReviewDate = (projects: ReviewProjectItemVO[]): ReviewProjectItemVO[] =>
(projects || []).map((item) => ({
...item,
reviewDate: undefined
})) }))
const loadDetail = async (id: number) => { const loadDetail = async (id: number) => {
@ -349,7 +364,7 @@ const loadCopySource = async (id: number) => {
formData.minutesAttachmentType = undefined formData.minutesAttachmentType = undefined
formData.minutesAttachmentSize = undefined formData.minutesAttachmentSize = undefined
formData.expertIds = detail.expertIds || [] formData.expertIds = detail.expertIds || []
formData.projects = mapProjectItems(projectData?.list ?? []) formData.projects = resetProjectReviewDate(mapProjectItems(projectData?.list ?? []))
isProjectsModified.value = false isProjectsModified.value = false
if (detail.startTime && detail.endTime) { if (detail.startTime && detail.endTime) {
@ -394,7 +409,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
formLoading.value = true formLoading.value = true
try { try {
const result = await importProjectsFromExcel(uploadFile.raw) const result = await importProjectsFromExcel(uploadFile.raw)
formData.projects = result as ReviewProjectItemVO[] const projects = applyDefaultReviewDate(result as ReviewProjectItemVO[], formData.meetingTimeRange)
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
isProjectsModified.value = true isProjectsModified.value = true
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`) ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
} catch { } catch {
@ -516,7 +532,15 @@ const submitForm = async () => {
} }
formLoading.value = true formLoading.value = true
try { try {
const submitData = { ...formData } const projects = buildScheduledProjectItems(
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
formData.meetingTimeRange?.[0],
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
const submitData = {
...formData,
projects
}
if (isEdit.value && !isProjectsModified.value) { if (isEdit.value && !isProjectsModified.value) {
delete submitData.projects delete submitData.projects
} }

View File

@ -110,15 +110,18 @@
<el-button type="primary" plain>导入验收申请 Excel</el-button> <el-button type="primary" plain>导入验收申请 Excel</el-button>
</el-upload> </el-upload>
<el-button type="success" plain @click="handleDownloadTemplate"></el-button> <el-button type="success" plain @click="handleDownloadTemplate"></el-button>
<el-text type="info" size="small" class="ml-10">格式序号开始时间结束时间议程分类项目标题汇报人报告人单位</el-text> <el-text type="info" size="small" class="ml-10">格式序号议程分类项目标题汇报人报告人单位</el-text>
</div> </div>
<!-- 评审项目预览列表 --> <!-- 评审项目预览列表 -->
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10"> <div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
<el-table :data="formData.projects" border size="small" max-height="300"> <el-table :data="formData.projects" border size="small" max-height="300">
<el-table-column label="序号" prop="seqNo" width="60" 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="起止时间" width="120" align="center">
<el-table-column label="结束时间" prop="endTime" width="80" align="center" /> <template #default="{ row }">
{{ row.startTime || '--:--' }} - {{ row.endTime || '--:--' }}
</template>
</el-table-column>
<el-table-column label="议程分类" prop="agendaCategory" width="100" /> <el-table-column label="议程分类" prop="agendaCategory" width="100" />
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip /> <el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
<el-table-column label="汇报人" prop="reporter" width="80" /> <el-table-column label="汇报人" prop="reporter" width="80" />
@ -151,6 +154,11 @@ import {
} from '@/api/review/meeting' } from '@/api/review/meeting'
import { getExpertUserList } from '@/api/system/user' import { getExpertUserList } from '@/api/system/user'
import download from '@/utils/download' import download from '@/utils/download'
import { applyDefaultReviewDate } from './projectReviewDate'
import {
buildScheduledProjectItems,
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
} from './projectSchedule'
defineOptions({ name: 'ReviewMeetingForm' }) defineOptions({ name: 'ReviewMeetingForm' })
const emit = defineEmits(['success']) const emit = defineEmits(['success'])
@ -257,7 +265,11 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
formLoading.value = true formLoading.value = true
try { try {
const result = await importProjectsFromExcel(uploadFile.raw) const result = await importProjectsFromExcel(uploadFile.raw)
formData.projects = ((result as any).data || result) as ReviewProjectItemVO[] const projects = applyDefaultReviewDate(
((result as any).data || result) as ReviewProjectItemVO[],
formData.meetingTimeRange
)
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
isProjectsModified.value = true isProjectsModified.value = true
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`) ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
} catch (e) { } catch (e) {
@ -340,7 +352,15 @@ const submitForm = async () => {
} }
formLoading.value = true formLoading.value = true
try { try {
const submitData = { ...formData } const projects = buildScheduledProjectItems(
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
formData.meetingTimeRange?.[0],
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
const submitData = {
...formData,
projects
}
if (formType.value === 'update' && !isProjectsModified.value) { if (formType.value === 'update' && !isProjectsModified.value) {
delete submitData.projects delete submitData.projects
} }

View File

@ -38,6 +38,35 @@
</div> </div>
<!-- 列表 --> <!-- 列表 -->
<div class="table-toolbar">
<div class="toolbar-left">
<div class="duration-card">
<span class="toolbar-label">项目汇报时长</span>
<el-select
v-model="scheduleInterval"
class="interval-select"
size="small"
@change="handleIntervalChange"
>
<el-option
v-for="item in REVIEW_MEETING_INTERVAL_OPTIONS"
:key="item"
:label="`${item}分钟`"
:value="item"
/>
</el-select>
</div>
<el-button
type="primary"
:disabled="selectedRows.length === 0"
:loading="batchPassLoading"
@click="handleBatchPass"
>
批量通过
</el-button>
</div>
<span class="selection-summary">已选 {{ selectedRows.length }} </span>
</div>
<el-table <el-table
ref="tableRef" ref="tableRef"
v-loading="loading || sortLoading" v-loading="loading || sortLoading"
@ -46,7 +75,9 @@
size="small" size="small"
border border
class="review-table" class="review-table"
@selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="52" align="center" />
<el-table-column label="拖拽" width="54" align="center"> <el-table-column label="拖拽" width="54" align="center">
<template #default> <template #default>
<span class="drag-handle" title="拖拽排序"></span> <span class="drag-handle" title="拖拽排序"></span>
@ -56,26 +87,10 @@
<el-table-column label="会中序号" prop="seqNo" width="72" align="center" /> <el-table-column label="会中序号" prop="seqNo" width="72" align="center" />
<el-table-column label="起止时间" width="236" align="center"> <el-table-column label="起止时间" width="236" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div class="inline-time-range"> <div class="readonly-time-range">
<el-time-picker <span class="readonly-time">{{ row.startTime || '--:--' }}</span>
v-model="row.startTime"
class="inline-time"
size="small"
format="HH:mm"
value-format="HH:mm"
placeholder="开始"
@change="saveInlineRow(row)"
/>
<span class="inline-time-sep">-</span> <span class="inline-time-sep">-</span>
<el-time-picker <span class="readonly-time">{{ row.endTime || '--:--' }}</span>
v-model="row.endTime"
class="inline-time"
size="small"
format="HH:mm"
value-format="HH:mm"
placeholder="结束"
@change="saveInlineRow(row)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@ -197,24 +212,34 @@ import {
REVIEW_AGENDA_CATEGORY_OPTIONS, REVIEW_AGENDA_CATEGORY_OPTIONS,
getReviewProjectPage, getReviewProjectPage,
updateReviewProject, updateReviewProject,
updateReviewProjectResultBatch,
updateReviewProjectSeqBatch, updateReviewProjectSeqBatch,
updateReviewProjectTimeBatch,
deleteReviewProject, deleteReviewProject,
type ReviewMeetingProjectRespVO type ReviewMeetingProjectRespVO
} from '@/api/review/project' } from '@/api/review/project'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import {
buildScheduledProjects,
REVIEW_MEETING_INTERVAL_OPTIONS,
type ReviewMeetingIntervalMinutes
} from './projectSchedule'
defineOptions({ name: 'ReviewMeetingProject' }) defineOptions({ name: 'ReviewMeetingProject' })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const reviewMeetingId = Number(route.params.meetingId) const reviewMeetingId = Number(route.params.meetingId)
const scheduleInterval = ref<ReviewMeetingIntervalMinutes>(15)
const loading = ref(false) const loading = ref(false)
const sortLoading = ref(false) const sortLoading = ref(false)
const batchPassLoading = ref(false)
const list = ref<ReviewMeetingProjectRespVO[]>([]) const list = ref<ReviewMeetingProjectRespVO[]>([])
const total = ref(0) const total = ref(0)
const meetingInfo = ref<any>({}) const meetingInfo = ref<any>({})
const tableRef = ref() const tableRef = ref()
const selectedRows = ref<ReviewMeetingProjectRespVO[]>([])
let sortableInstance: Sortable | null = null let sortableInstance: Sortable | null = null
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' } const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
@ -225,8 +250,6 @@ const getMaterialCompleteClass = (complete?: boolean) => {
} }
type InlineEditableFields = { type InlineEditableFields = {
seqNo?: number seqNo?: number
startTime?: string
endTime?: string
agendaCategory?: string agendaCategory?: string
projectTitle?: string projectTitle?: string
reporter?: string reporter?: string
@ -248,18 +271,18 @@ const getList = async () => {
const data = await getReviewProjectPage(queryParams) const data = await getReviewProjectPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
selectedRows.value = []
syncInlineSnapshots(data.list || []) syncInlineSnapshots(data.list || [])
} finally { } finally {
loading.value = false loading.value = false
await nextTick() await nextTick()
tableRef.value?.clearSelection?.()
initSortable() initSortable()
} }
} }
const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({ const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({
seqNo: row.seqNo, seqNo: row.seqNo,
startTime: row.startTime,
endTime: row.endTime,
agendaCategory: row.agendaCategory, agendaCategory: row.agendaCategory,
projectTitle: row.projectTitle, projectTitle: row.projectTitle,
reporter: row.reporter, reporter: row.reporter,
@ -279,8 +302,6 @@ const syncInlineSnapshots = (rows: ReviewMeetingProjectRespVO[]) => {
const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => { const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => {
const normalize = (value: unknown) => (value ?? '') as string | number const normalize = (value: unknown) => (value ?? '') as string | number
return normalize(current.seqNo) !== normalize(snapshot.seqNo) return normalize(current.seqNo) !== normalize(snapshot.seqNo)
|| normalize(current.startTime) !== normalize(snapshot.startTime)
|| normalize(current.endTime) !== normalize(snapshot.endTime)
|| normalize(current.agendaCategory) !== normalize(snapshot.agendaCategory) || normalize(current.agendaCategory) !== normalize(snapshot.agendaCategory)
|| normalize(current.projectTitle) !== normalize(snapshot.projectTitle) || normalize(current.projectTitle) !== normalize(snapshot.projectTitle)
|| normalize(current.reporter) !== normalize(snapshot.reporter) || normalize(current.reporter) !== normalize(snapshot.reporter)
@ -289,6 +310,35 @@ const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditable
|| normalize(current.reviewResult) !== normalize(snapshot.reviewResult) || normalize(current.reviewResult) !== normalize(snapshot.reviewResult)
} }
const getFullProjectList = async () => {
const data = await getReviewProjectPage({
reviewMeetingId,
pageNo: 1,
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
})
return [...(data.list || [])]
}
const persistScheduledTime = async (projects: ReviewMeetingProjectRespVO[]) => {
if (!meetingInfo.value?.startTime) {
ElMessage.warning('会议开始时间为空,无法自动编排起止时间')
return null
}
const scheduledProjects = buildScheduledProjects(projects, meetingInfo.value.startTime, scheduleInterval.value)
if (!scheduledProjects) {
ElMessage.warning('会议开始时间格式无效,无法自动编排起止时间')
return null
}
await updateReviewProjectTimeBatch({
items: scheduledProjects.map((project) => ({
id: project.id,
startTime: project.startTime,
endTime: project.endTime
}))
})
return scheduledProjects
}
const saveInlineRow = async (row: ReviewMeetingProjectRespVO, options: { reload?: boolean } = {}) => { const saveInlineRow = async (row: ReviewMeetingProjectRespVO, options: { reload?: boolean } = {}) => {
const snapshot = inlineSnapshotMap.value[row.id] const snapshot = inlineSnapshotMap.value[row.id]
if (!snapshot) return if (!snapshot) return
@ -346,12 +396,7 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
sortLoading.value = true sortLoading.value = true
try { try {
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
const allData = await getReviewProjectPage({ const fullList = await getFullProjectList()
reviewMeetingId,
pageNo: 1,
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
})
const fullList = [...(allData.list || [])]
list.value.forEach((item, index) => { list.value.forEach((item, index) => {
fullList[pageStart + index] = item fullList[pageStart + index] = item
}) })
@ -366,13 +411,20 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
}) })
if (changedRows.length === 0) return if (changedRows.length === 0) return
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
await updateReviewProjectSeqBatch( await updateReviewProjectSeqBatch(
changedRows.map((row) => ({ changedRows.map((row) => ({
id: row.id, id: row.id,
seqNo: row.seqNo seqNo: row.seqNo
})) }))
) )
const scheduledProjects = await persistScheduledTime(fullList)
if (scheduledProjects) {
list.value = scheduledProjects.slice(pageStart, pageStart + queryParams.pageSize)
syncInlineSnapshots(list.value)
} else {
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
syncInlineSnapshots(list.value)
}
ElMessage.success('排序已更新') ElMessage.success('排序已更新')
} catch { } catch {
ElMessage.error('排序保存失败,已恢复原列表') ElMessage.error('排序保存失败,已恢复原列表')
@ -382,6 +434,23 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
} }
} }
const handleIntervalChange = async () => {
sortLoading.value = true
try {
const fullList = await getFullProjectList()
const scheduledProjects = await persistScheduledTime(fullList)
if (!scheduledProjects) return
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
list.value = scheduledProjects.slice(pageStart, pageStart + queryParams.pageSize)
syncInlineSnapshots(list.value)
ElMessage.success('起止时间已按新间隔重算')
} catch {
ElMessage.error('时间重算失败,请重试')
} finally {
sortLoading.value = false
}
}
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' }) await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
await deleteReviewProject([id]) await deleteReviewProject([id])
@ -389,6 +458,30 @@ const handleDelete = async (id: number) => {
getList() getList()
} }
const handleSelectionChange = (rows: ReviewMeetingProjectRespVO[]) => {
selectedRows.value = rows
}
const handleBatchPass = async () => {
if (selectedRows.value.length === 0) return
await ElMessageBox.confirm(
`确认将当前页勾选的 ${selectedRows.value.length} 个项目统一设为通过吗?`,
'批量通过确认',
{ type: 'warning' }
)
batchPassLoading.value = true
try {
await updateReviewProjectResultBatch({
ids: selectedRows.value.map((row) => row.id),
reviewResult: 'PASS'
})
ElMessage.success('已批量设为通过')
await getList()
} finally {
batchPassLoading.value = false
}
}
const goToDetail = (row: ReviewMeetingProjectRespVO) => { const goToDetail = (row: ReviewMeetingProjectRespVO) => {
router.push({ router.push({
name: 'ReviewProjectDetail', name: 'ReviewProjectDetail',
@ -496,6 +589,41 @@ onBeforeUnmount(() => {
gap: 10px 14px; gap: 10px 14px;
grid-template-columns: repeat(4, minmax(140px, 1fr)); grid-template-columns: repeat(4, minmax(140px, 1fr));
} }
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.duration-card {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: linear-gradient(180deg, #f7faff 0%, #eef4ff 100%);
border: 1px solid #cfdcf8;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(41, 90, 188, 0.08);
}
.toolbar-label {
color: #295abc;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.interval-select {
width: 128px;
}
.selection-summary {
color: #5c6f91;
font-size: 13px;
}
.meta-item { .meta-item {
background: #fff; background: #fff;
border: 1px solid rgba(41, 90, 188, 0.1); border: 1px solid rgba(41, 90, 188, 0.1);
@ -570,33 +698,50 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.readonly-time-range {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.readonly-time {
min-width: 44px;
color: #20314f;
font-weight: 600;
}
.inline-time-sep { .inline-time-sep {
color: #999; color: #999;
} }
.inline-time {
width: 92px;
}
.inline-field { .inline-field {
width: 100%; width: 100%;
} }
:deep(.inline-field .el-input__wrapper), :deep(.inline-field .el-input__wrapper),
:deep(.inline-field .el-select__wrapper), :deep(.inline-field .el-select__wrapper),
:deep(.inline-time .el-input__wrapper) { :deep(.interval-select .el-select__wrapper) {
border-radius: 6px; border-radius: 6px;
border: 1px solid transparent; border: 1px solid transparent;
box-shadow: none; box-shadow: none;
background-color: #f8fafc; background-color: #f8fafc;
transition: border-color 0.2s, background-color 0.2s; transition: border-color 0.2s, background-color 0.2s;
} }
:deep(.interval-select .el-select__wrapper) {
border-color: #b7c9f2;
background: #fff;
box-shadow: 0 0 0 1px rgba(41, 90, 188, 0.04);
}
:deep(.interval-select .el-select__placeholder),
:deep(.interval-select .el-select__selected-item) {
color: #20314f;
font-weight: 600;
}
:deep(.review-table .el-table__body tr:hover .inline-field .el-input__wrapper), :deep(.review-table .el-table__body tr:hover .inline-field .el-input__wrapper),
:deep(.review-table .el-table__body tr:hover .inline-field .el-select__wrapper), :deep(.review-table .el-table__body tr:hover .inline-field .el-select__wrapper) {
:deep(.review-table .el-table__body tr:hover .inline-time .el-input__wrapper) {
border-color: #d8e0ec; border-color: #d8e0ec;
background-color: #fff; background-color: #fff;
} }
:deep(.inline-field .el-input__wrapper.is-focus), :deep(.inline-field .el-input__wrapper.is-focus),
:deep(.inline-field .el-select__wrapper.is-focused), :deep(.inline-field .el-select__wrapper.is-focused),
:deep(.inline-time .el-input__wrapper.is-focus) { :deep(.interval-select .el-select__wrapper.is-focused) {
border-color: #295abc; border-color: #295abc;
background-color: #fff; background-color: #fff;
} }

View File

@ -0,0 +1,29 @@
import dayjs from 'dayjs'
import type { ReviewProjectItemVO } from '@/api/review/meeting'
import { resolveMeetingStartTime } from './projectSchedule'
const resolveMeetingStartDate = (meetingTimeRange?: Array<string | number | undefined>) => {
const meetingStart = meetingTimeRange?.[0]
if (meetingStart === undefined || meetingStart === null || meetingStart === '') {
return undefined
}
const parsed = resolveMeetingStartTime(meetingStart)
if (!parsed.isValid()) {
return undefined
}
return parsed.format('YYYY-MM-DD')
}
export const applyDefaultReviewDate = (
projects: ReviewProjectItemVO[] = [],
meetingTimeRange?: Array<string | number | undefined>
): ReviewProjectItemVO[] => {
const defaultReviewDate = resolveMeetingStartDate(meetingTimeRange)
if (!defaultReviewDate) {
return projects
}
return projects.map((item) => ({
...item,
reviewDate: item.reviewDate || defaultReviewDate
}))
}

View File

@ -0,0 +1,55 @@
import dayjs from 'dayjs'
import type { ReviewProjectItemVO } from '@/api/review/meeting'
import type { ReviewMeetingProjectRespVO } from '@/api/review/project'
export const REVIEW_MEETING_INTERVAL_OPTIONS = [10, 15, 20, 30] as const
export type ReviewMeetingIntervalMinutes = (typeof REVIEW_MEETING_INTERVAL_OPTIONS)[number]
export const DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES: ReviewMeetingIntervalMinutes = 15
type SchedulableProject = {
startTime?: string
endTime?: string
}
const parseMeetingStartTime = (meetingStartTime?: string | number) => {
if (!meetingStartTime) return null
const normalized = typeof meetingStartTime === 'string'
? (/^\d+$/.test(meetingStartTime) ? Number(meetingStartTime) : meetingStartTime.replace(' ', 'T'))
: meetingStartTime
const parsed = dayjs(normalized)
if (!parsed.isValid()) return null
return parsed
}
export const resolveMeetingStartTime = parseMeetingStartTime
const buildScheduledItems = <T extends SchedulableProject>(
projects: T[],
meetingStartTime: string | number | undefined,
intervalMinutes: ReviewMeetingIntervalMinutes
) => {
const meetingStart = parseMeetingStartTime(meetingStartTime)
if (!meetingStart) return null
return projects.map((project, index) => {
const start = meetingStart.add(index * intervalMinutes, 'minute')
const end = start.add(intervalMinutes, 'minute')
return {
...project,
startTime: start.format('HH:mm'),
endTime: end.format('HH:mm')
}
})
}
export const buildScheduledProjects = (
projects: ReviewMeetingProjectRespVO[],
meetingStartTime: string | undefined,
intervalMinutes: ReviewMeetingIntervalMinutes
) => buildScheduledItems(projects, meetingStartTime, intervalMinutes)
export const buildScheduledProjectItems = (
projects: ReviewProjectItemVO[],
meetingStartTime: string | number | undefined,
intervalMinutes: ReviewMeetingIntervalMinutes = DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
) => buildScheduledItems(projects, meetingStartTime, intervalMinutes)