feat(review-meeting): 优化项目排程与导入展示
parent
9113f446e6
commit
9251c64067
|
|
@ -10,12 +10,13 @@ import * as FileApi from '@/api/infra/file'
|
|||
export interface ReviewProjectItemVO {
|
||||
sourceProjectId?: number
|
||||
seqNo: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
agendaCategory: string
|
||||
projectTitle: string
|
||||
reporter: string
|
||||
reporterUnit: string
|
||||
reviewDate?: string
|
||||
}
|
||||
|
||||
// 会议保存 VO(新增/编辑)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ export interface ReviewMeetingProjectSeqUpdateReqVO {
|
|||
seqNo: number
|
||||
}
|
||||
|
||||
export interface ReviewMeetingProjectResultBatchUpdateReqVO {
|
||||
ids: number[]
|
||||
reviewResult: 'PASS' | 'REJECT'
|
||||
}
|
||||
|
||||
export interface ReviewMeetingProjectTimeBatchUpdateReqVO {
|
||||
items: Array<{
|
||||
id: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ReviewMeetingProjectPageReqVO {
|
||||
pageNo?: number
|
||||
pageSize?: number
|
||||
|
|
@ -141,6 +154,14 @@ export const updateReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =
|
|||
export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) =>
|
||||
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[]) =>
|
||||
request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } })
|
||||
|
|
|
|||
|
|
@ -157,14 +157,17 @@
|
|||
<button type="button" class="btn-default">导入验收申请 Excel</button>
|
||||
</el-upload>
|
||||
<button type="button" class="btn-default" @click="handleDownloadTemplate">下载导入模板</button>
|
||||
<span class="import-hint">格式:序号、开始时间、结束时间、议程分类、项目标题、汇报人、报告人单位</span>
|
||||
<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="起止时间" width="120" 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="projectTitle" show-overflow-tooltip />
|
||||
<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 download from '@/utils/download'
|
||||
import ExpertSelectTable from './components/ExpertSelectTable.vue'
|
||||
import { applyDefaultReviewDate } from './projectReviewDate'
|
||||
import {
|
||||
buildScheduledProjectItems,
|
||||
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||
} from './projectSchedule'
|
||||
|
||||
defineOptions({ name: 'ReviewMeetingEdit' })
|
||||
|
||||
|
|
@ -288,7 +296,14 @@ const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] =>
|
|||
agendaCategory: item.agendaCategory,
|
||||
projectTitle: item.projectTitle,
|
||||
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) => {
|
||||
|
|
@ -349,7 +364,7 @@ const loadCopySource = async (id: number) => {
|
|||
formData.minutesAttachmentType = undefined
|
||||
formData.minutesAttachmentSize = undefined
|
||||
formData.expertIds = detail.expertIds || []
|
||||
formData.projects = mapProjectItems(projectData?.list ?? [])
|
||||
formData.projects = resetProjectReviewDate(mapProjectItems(projectData?.list ?? []))
|
||||
isProjectsModified.value = false
|
||||
|
||||
if (detail.startTime && detail.endTime) {
|
||||
|
|
@ -394,7 +409,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
|||
formLoading.value = true
|
||||
try {
|
||||
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
|
||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||
} catch {
|
||||
|
|
@ -516,7 +532,15 @@ const submitForm = async () => {
|
|||
}
|
||||
formLoading.value = true
|
||||
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) {
|
||||
delete submitData.projects
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,15 +110,18 @@
|
|||
<el-button type="primary" plain>导入验收申请 Excel</el-button>
|
||||
</el-upload>
|
||||
<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 v-if="formData.projects && formData.projects.length > 0" class="mt-10">
|
||||
<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="startTime" width="80" align="center" />
|
||||
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
|
||||
<el-table-column label="起止时间" width="120" 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="projectTitle" show-overflow-tooltip />
|
||||
<el-table-column label="汇报人" prop="reporter" width="80" />
|
||||
|
|
@ -151,6 +154,11 @@ import {
|
|||
} from '@/api/review/meeting'
|
||||
import { getExpertUserList } from '@/api/system/user'
|
||||
import download from '@/utils/download'
|
||||
import { applyDefaultReviewDate } from './projectReviewDate'
|
||||
import {
|
||||
buildScheduledProjectItems,
|
||||
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||
} from './projectSchedule'
|
||||
|
||||
defineOptions({ name: 'ReviewMeetingForm' })
|
||||
const emit = defineEmits(['success'])
|
||||
|
|
@ -257,7 +265,11 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
|||
formLoading.value = true
|
||||
try {
|
||||
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
|
||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||
} catch (e) {
|
||||
|
|
@ -340,7 +352,15 @@ const submitForm = async () => {
|
|||
}
|
||||
formLoading.value = true
|
||||
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) {
|
||||
delete submitData.projects
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,35 @@
|
|||
</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
|
||||
ref="tableRef"
|
||||
v-loading="loading || sortLoading"
|
||||
|
|
@ -46,7 +75,9 @@
|
|||
size="small"
|
||||
border
|
||||
class="review-table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" align="center" />
|
||||
<el-table-column label="拖拽" width="54" align="center">
|
||||
<template #default>
|
||||
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
||||
|
|
@ -56,26 +87,10 @@
|
|||
<el-table-column label="会中序号" prop="seqNo" width="72" align="center" />
|
||||
<el-table-column label="起止时间" width="236" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="inline-time-range">
|
||||
<el-time-picker
|
||||
v-model="row.startTime"
|
||||
class="inline-time"
|
||||
size="small"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="开始"
|
||||
@change="saveInlineRow(row)"
|
||||
/>
|
||||
<div class="readonly-time-range">
|
||||
<span class="readonly-time">{{ row.startTime || '--:--' }}</span>
|
||||
<span class="inline-time-sep">-</span>
|
||||
<el-time-picker
|
||||
v-model="row.endTime"
|
||||
class="inline-time"
|
||||
size="small"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="结束"
|
||||
@change="saveInlineRow(row)"
|
||||
/>
|
||||
<span class="readonly-time">{{ row.endTime || '--:--' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -197,24 +212,34 @@ import {
|
|||
REVIEW_AGENDA_CATEGORY_OPTIONS,
|
||||
getReviewProjectPage,
|
||||
updateReviewProject,
|
||||
updateReviewProjectResultBatch,
|
||||
updateReviewProjectSeqBatch,
|
||||
updateReviewProjectTimeBatch,
|
||||
deleteReviewProject,
|
||||
type ReviewMeetingProjectRespVO
|
||||
} from '@/api/review/project'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import {
|
||||
buildScheduledProjects,
|
||||
REVIEW_MEETING_INTERVAL_OPTIONS,
|
||||
type ReviewMeetingIntervalMinutes
|
||||
} from './projectSchedule'
|
||||
|
||||
defineOptions({ name: 'ReviewMeetingProject' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const reviewMeetingId = Number(route.params.meetingId)
|
||||
const scheduleInterval = ref<ReviewMeetingIntervalMinutes>(15)
|
||||
|
||||
const loading = ref(false)
|
||||
const sortLoading = ref(false)
|
||||
const batchPassLoading = ref(false)
|
||||
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
||||
const total = ref(0)
|
||||
const meetingInfo = ref<any>({})
|
||||
const tableRef = ref()
|
||||
const selectedRows = ref<ReviewMeetingProjectRespVO[]>([])
|
||||
let sortableInstance: Sortable | null = null
|
||||
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||
|
|
@ -225,8 +250,6 @@ const getMaterialCompleteClass = (complete?: boolean) => {
|
|||
}
|
||||
type InlineEditableFields = {
|
||||
seqNo?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
agendaCategory?: string
|
||||
projectTitle?: string
|
||||
reporter?: string
|
||||
|
|
@ -248,18 +271,18 @@ const getList = async () => {
|
|||
const data = await getReviewProjectPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
selectedRows.value = []
|
||||
syncInlineSnapshots(data.list || [])
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
tableRef.value?.clearSelection?.()
|
||||
initSortable()
|
||||
}
|
||||
}
|
||||
|
||||
const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({
|
||||
seqNo: row.seqNo,
|
||||
startTime: row.startTime,
|
||||
endTime: row.endTime,
|
||||
agendaCategory: row.agendaCategory,
|
||||
projectTitle: row.projectTitle,
|
||||
reporter: row.reporter,
|
||||
|
|
@ -279,8 +302,6 @@ const syncInlineSnapshots = (rows: ReviewMeetingProjectRespVO[]) => {
|
|||
const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => {
|
||||
const normalize = (value: unknown) => (value ?? '') as string | number
|
||||
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.projectTitle) !== normalize(snapshot.projectTitle)
|
||||
|| normalize(current.reporter) !== normalize(snapshot.reporter)
|
||||
|
|
@ -289,6 +310,35 @@ const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditable
|
|||
|| 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 snapshot = inlineSnapshotMap.value[row.id]
|
||||
if (!snapshot) return
|
||||
|
|
@ -346,12 +396,7 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
|||
sortLoading.value = true
|
||||
try {
|
||||
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
||||
const allData = await getReviewProjectPage({
|
||||
reviewMeetingId,
|
||||
pageNo: 1,
|
||||
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
|
||||
})
|
||||
const fullList = [...(allData.list || [])]
|
||||
const fullList = await getFullProjectList()
|
||||
list.value.forEach((item, index) => {
|
||||
fullList[pageStart + index] = item
|
||||
})
|
||||
|
|
@ -366,13 +411,20 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
|||
})
|
||||
if (changedRows.length === 0) return
|
||||
|
||||
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
|
||||
await updateReviewProjectSeqBatch(
|
||||
changedRows.map((row) => ({
|
||||
id: row.id,
|
||||
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('排序已更新')
|
||||
} catch {
|
||||
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) => {
|
||||
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
|
||||
await deleteReviewProject([id])
|
||||
|
|
@ -389,6 +458,30 @@ const handleDelete = async (id: number) => {
|
|||
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) => {
|
||||
router.push({
|
||||
name: 'ReviewProjectDetail',
|
||||
|
|
@ -496,6 +589,41 @@ onBeforeUnmount(() => {
|
|||
gap: 10px 14px;
|
||||
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 {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(41, 90, 188, 0.1);
|
||||
|
|
@ -570,33 +698,50 @@ onBeforeUnmount(() => {
|
|||
align-items: center;
|
||||
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 {
|
||||
color: #999;
|
||||
}
|
||||
.inline-time {
|
||||
width: 92px;
|
||||
}
|
||||
.inline-field {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.inline-field .el-input__wrapper),
|
||||
:deep(.inline-field .el-select__wrapper),
|
||||
:deep(.inline-time .el-input__wrapper) {
|
||||
:deep(.interval-select .el-select__wrapper) {
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
background-color: #f8fafc;
|
||||
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-select__wrapper),
|
||||
:deep(.review-table .el-table__body tr:hover .inline-time .el-input__wrapper) {
|
||||
:deep(.review-table .el-table__body tr:hover .inline-field .el-select__wrapper) {
|
||||
border-color: #d8e0ec;
|
||||
background-color: #fff;
|
||||
}
|
||||
:deep(.inline-field .el-input__wrapper.is-focus),
|
||||
: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;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}))
|
||||
}
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue