809 lines
22 KiB
Vue
809 lines
22 KiB
Vue
<template>
|
|
<ContentWrap>
|
|
<!-- 页面标题 -->
|
|
<div class="page-title">项目列表</div>
|
|
|
|
<!-- 会议摘要信息 -->
|
|
<div class="meeting-info-card">
|
|
<div class="meeting-info-head">
|
|
<div class="meeting-title-wrap">
|
|
<div class="meeting-caption">所属会议</div>
|
|
<div class="meeting-name">{{ meetingInfo.name || '-' }}</div>
|
|
<div class="meeting-time">
|
|
{{ meetingInfo.startTime ? formatDate(meetingInfo.startTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
|
<span class="meeting-time-sep">~</span>
|
|
{{ meetingInfo.endTime ? formatDate(meetingInfo.endTime, 'YYYY-MM-DD HH:mm') : '-' }}
|
|
</div>
|
|
</div>
|
|
<span :class="`status-pill status-pill-${meetingInfo.status}`">{{ STATUS_LABEL[meetingInfo.status] || '-' }}</span>
|
|
</div>
|
|
<div class="meeting-meta-grid">
|
|
<div class="meta-item">
|
|
<span class="meta-label">会议地点</span>
|
|
<span class="meta-value">{{ meetingInfo.location || '-' }}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">组织单位</span>
|
|
<span class="meta-value">{{ meetingInfo.organizationUnit || '-' }}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">会议主持人</span>
|
|
<span class="meta-value">{{ meetingInfo.host || '-' }}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">参会专家数</span>
|
|
<span class="meta-value">{{ meetingInfo.expertCount ?? 0 }}</span>
|
|
</div>
|
|
</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
|
|
ref="tableRef"
|
|
v-loading="loading || sortLoading"
|
|
:data="list"
|
|
row-key="id"
|
|
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>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="立项编号" prop="id" width="72" align="center" />
|
|
<el-table-column label="会中序号" prop="seqNo" width="72" align="center" />
|
|
<el-table-column label="起止时间" width="236" align="center">
|
|
<template #default="{ row }">
|
|
<div class="readonly-time-range">
|
|
<span class="readonly-time">{{ row.startTime || '--:--' }}</span>
|
|
<span class="inline-time-sep">-</span>
|
|
<span class="readonly-time">{{ row.endTime || '--:--' }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="议程分类" width="130">
|
|
<template #default="{ row }">
|
|
<el-select
|
|
v-model="row.agendaCategory"
|
|
class="inline-field"
|
|
size="small"
|
|
placeholder="议程分类"
|
|
@change="saveInlineRow(row, { reload: true })"
|
|
>
|
|
<el-option v-for="item in REVIEW_AGENDA_CATEGORY_OPTIONS" :key="item" :label="item" :value="item" />
|
|
</el-select>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="项目名称" prop="projectTitle" min-width="220">
|
|
<template #default="{ row }">
|
|
<el-input
|
|
v-model="row.projectTitle"
|
|
class="inline-field"
|
|
size="small"
|
|
placeholder="项目名称"
|
|
@blur="saveInlineRow(row)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="报告人" width="120">
|
|
<template #default="{ row }">
|
|
<el-input
|
|
v-model="row.reporter"
|
|
class="inline-field"
|
|
size="small"
|
|
placeholder="报告人"
|
|
@blur="saveInlineRow(row)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="报告单位" min-width="180">
|
|
<template #default="{ row }">
|
|
<el-input
|
|
v-model="row.reporterUnit"
|
|
class="inline-field"
|
|
size="small"
|
|
placeholder="报告单位"
|
|
@blur="saveInlineRow(row)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="评审日期" width="142" align="center">
|
|
<template #default="{ row }">
|
|
<el-date-picker
|
|
v-model="row.reviewDate"
|
|
class="inline-field"
|
|
size="small"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
placeholder="评审日期"
|
|
@change="saveInlineRow(row)"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="会前资料齐全" width="110" align="center">
|
|
<template #default="{ row }">
|
|
<span :class="getMaterialCompleteClass(row.preMeetingMaterialsComplete)">
|
|
{{ row.preMeetingMaterialsComplete ? '✓' : '✗' }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="会后资料齐全" width="110" align="center">
|
|
<template #default="{ row }">
|
|
<span :class="getMaterialCompleteClass(row.postMeetingMaterialsComplete)">
|
|
{{ row.postMeetingMaterialsComplete ? '✓' : '✗' }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="AI状态" width="110" align="center">
|
|
<template #default="{ row }">
|
|
<span :class="`ai-status ai-status-${row.aiSummaryStatus ?? 0}`">
|
|
{{ AI_STATUS_LABEL[row.aiSummaryStatus ?? 0] }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="评审结果" width="120" align="center">
|
|
<template #default="{ row }">
|
|
<el-select
|
|
v-model="row.reviewResult"
|
|
class="inline-field"
|
|
size="small"
|
|
placeholder="评审结果"
|
|
clearable
|
|
@change="saveInlineRow(row)"
|
|
>
|
|
<el-option label="通过" value="PASS" />
|
|
<el-option label="不通过" value="REJECT" />
|
|
</el-select>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="154" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<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>
|
|
|
|
<!-- 分页 -->
|
|
<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, onBeforeUnmount, nextTick } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import Sortable from 'sortablejs'
|
|
import { getReviewMeeting } from '@/api/review/meeting'
|
|
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: '已取消' }
|
|
const AI_STATUS_LABEL: Record<number, string> = { 0: '待构建', 1: '构建中', 2: '已完成', 3: '失败' }
|
|
const getMaterialCompleteClass = (complete?: boolean) => {
|
|
if (complete) return 'material-complete'
|
|
return 'material-incomplete'
|
|
}
|
|
type InlineEditableFields = {
|
|
seqNo?: number
|
|
agendaCategory?: string
|
|
projectTitle?: string
|
|
reporter?: string
|
|
reporterUnit?: string
|
|
reviewDate?: string
|
|
reviewResult?: 'PASS' | 'REJECT'
|
|
}
|
|
const inlineSnapshotMap = ref<Record<number, InlineEditableFields>>({})
|
|
|
|
const queryParams = reactive({
|
|
pageNo: 1,
|
|
pageSize: 10,
|
|
reviewMeetingId
|
|
})
|
|
|
|
const getList = async () => {
|
|
loading.value = true
|
|
try {
|
|
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,
|
|
agendaCategory: row.agendaCategory,
|
|
projectTitle: row.projectTitle,
|
|
reporter: row.reporter,
|
|
reporterUnit: row.reporterUnit,
|
|
reviewDate: row.reviewDate,
|
|
reviewResult: row.reviewResult
|
|
})
|
|
|
|
const syncInlineSnapshots = (rows: ReviewMeetingProjectRespVO[]) => {
|
|
const snapshot: Record<number, InlineEditableFields> = {}
|
|
rows.forEach((row) => {
|
|
snapshot[row.id] = buildInlineFields(row)
|
|
})
|
|
inlineSnapshotMap.value = snapshot
|
|
}
|
|
|
|
const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => {
|
|
const normalize = (value: unknown) => (value ?? '') as string | number
|
|
return normalize(current.seqNo) !== normalize(snapshot.seqNo)
|
|
|| normalize(current.agendaCategory) !== normalize(snapshot.agendaCategory)
|
|
|| normalize(current.projectTitle) !== normalize(snapshot.projectTitle)
|
|
|| normalize(current.reporter) !== normalize(snapshot.reporter)
|
|
|| normalize(current.reporterUnit) !== normalize(snapshot.reporterUnit)
|
|
|| normalize(current.reviewDate) !== normalize(snapshot.reviewDate)
|
|
|| 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
|
|
row.projectTitle = row.projectTitle?.trim()
|
|
if (!row.agendaCategory) {
|
|
ElMessage.warning('议程分类不能为空')
|
|
Object.assign(row, snapshot)
|
|
return
|
|
}
|
|
if (!row.projectTitle) {
|
|
ElMessage.warning('项目名称不能为空')
|
|
Object.assign(row, snapshot)
|
|
return
|
|
}
|
|
const current = buildInlineFields(row)
|
|
if (!isInlineChanged(current, snapshot)) return
|
|
try {
|
|
await updateReviewProject({
|
|
id: row.id,
|
|
...current
|
|
})
|
|
inlineSnapshotMap.value[row.id] = buildInlineFields(row)
|
|
if (options.reload) {
|
|
await getList()
|
|
}
|
|
} catch {
|
|
Object.assign(row, snapshot)
|
|
ElMessage.error('保存失败,已恢复')
|
|
}
|
|
}
|
|
|
|
const initSortable = () => {
|
|
if (!tableRef.value?.$el) return
|
|
const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody')
|
|
if (!tbody) return
|
|
sortableInstance?.destroy()
|
|
sortableInstance = Sortable.create(tbody, {
|
|
animation: 150,
|
|
handle: '.drag-handle',
|
|
ghostClass: 'drag-ghost',
|
|
chosenClass: 'drag-chosen',
|
|
onEnd: ({ oldIndex, newIndex }) => {
|
|
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) return
|
|
handleSortEnd(oldIndex, newIndex)
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
|
const moved = list.value.splice(oldIndex, 1)[0]
|
|
if (!moved) return
|
|
list.value.splice(newIndex, 0, moved)
|
|
list.value = [...list.value]
|
|
|
|
sortLoading.value = true
|
|
try {
|
|
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
|
const fullList = await getFullProjectList()
|
|
list.value.forEach((item, index) => {
|
|
fullList[pageStart + index] = item
|
|
})
|
|
|
|
const changedRows: ReviewMeetingProjectRespVO[] = []
|
|
fullList.forEach((item, index) => {
|
|
const nextSeq = index + 1
|
|
if (item.seqNo !== nextSeq) {
|
|
item.seqNo = nextSeq
|
|
changedRows.push(item)
|
|
}
|
|
})
|
|
if (changedRows.length === 0) return
|
|
|
|
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('排序保存失败,已恢复原列表')
|
|
await getList()
|
|
} finally {
|
|
sortLoading.value = false
|
|
}
|
|
}
|
|
|
|
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])
|
|
ElMessage.success('删除成功')
|
|
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',
|
|
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,
|
|
meetingHost: meetingInfo.value?.host,
|
|
meetingName: meetingInfo.value?.name
|
|
}
|
|
})
|
|
}
|
|
|
|
onMounted(async () => {
|
|
meetingInfo.value = await getReviewMeeting(reviewMeetingId)
|
|
await getList()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
sortableInstance?.destroy()
|
|
sortableInstance = null
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ── 页面标题 ── */
|
|
.page-title {
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid #e1e7f0;
|
|
}
|
|
|
|
/* ── 会议信息卡 ── */
|
|
.meeting-info-card {
|
|
padding: 16px;
|
|
background: linear-gradient(180deg, rgba(41, 90, 188, 0.1) 0%, rgba(41, 90, 188, 0.04) 100%);
|
|
border: 1px solid rgba(41, 90, 188, 0.16);
|
|
border-radius: 10px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.meeting-info-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.meeting-title-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.meeting-caption {
|
|
color: #4f6d9f;
|
|
font-size: 12px;
|
|
margin: 0;
|
|
}
|
|
.meeting-name {
|
|
color: #295abc;
|
|
font-size: 20px;
|
|
line-height: 1.3;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
.meeting-time {
|
|
margin: 0;
|
|
color: #2f3f5e;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
.meeting-time-sep {
|
|
margin: 0 6px;
|
|
color: #7d91b4;
|
|
}
|
|
.status-pill {
|
|
align-self: flex-start;
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.status-pill-0 {
|
|
color: #b57616;
|
|
background: rgba(236, 174, 75, 0.2);
|
|
}
|
|
.status-pill-1 {
|
|
color: #267d1e;
|
|
background: rgba(115, 192, 71, 0.2);
|
|
}
|
|
.status-pill-2,
|
|
.status-pill-3 {
|
|
color: #596b89;
|
|
background: rgba(125, 145, 180, 0.16);
|
|
}
|
|
.meeting-meta-grid {
|
|
display: grid;
|
|
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);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
}
|
|
.meta-label {
|
|
display: block;
|
|
color: #6a7f9f;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.meta-value {
|
|
display: block;
|
|
color: #20314f;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── 状态文字 ── */
|
|
.review-result-pass {
|
|
color: #67c23a;
|
|
font-weight: 500;
|
|
}
|
|
.review-result-reject {
|
|
color: #f56c6c;
|
|
font-weight: 500;
|
|
}
|
|
.material-complete {
|
|
color: #67c23a !important;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
.material-incomplete {
|
|
color: #f56c6c !important;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.ai-status {
|
|
display: inline-flex;
|
|
min-width: 64px;
|
|
justify-content: center;
|
|
padding: 3px 8px;
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ai-status-0 {
|
|
color: #8a6214;
|
|
background: #fff3cd;
|
|
}
|
|
|
|
.ai-status-1 {
|
|
color: #0f766e;
|
|
background: #ccfbf1;
|
|
}
|
|
|
|
.ai-status-2 {
|
|
color: #166534;
|
|
background: #dcfce7;
|
|
}
|
|
|
|
.ai-status-3 {
|
|
color: #b91c1c;
|
|
background: #fee2e2;
|
|
}
|
|
|
|
.inline-time-range {
|
|
display: flex;
|
|
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-field {
|
|
width: 100%;
|
|
}
|
|
:deep(.inline-field .el-input__wrapper),
|
|
:deep(.inline-field .el-select__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) {
|
|
border-color: #d8e0ec;
|
|
background-color: #fff;
|
|
}
|
|
:deep(.inline-field .el-input__wrapper.is-focus),
|
|
:deep(.inline-field .el-select__wrapper.is-focused),
|
|
:deep(.interval-select .el-select__wrapper.is-focused) {
|
|
border-color: #295abc;
|
|
background-color: #fff;
|
|
}
|
|
|
|
/* ── 操作链接 ── */
|
|
.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;
|
|
padding-top: 10px;
|
|
padding-bottom: 10px;
|
|
}
|
|
:deep(.review-table .el-table__body tr:hover > td) {
|
|
background-color: rgba(41, 90, 188, 0.04);
|
|
}
|
|
|
|
.drag-handle {
|
|
display: inline-block;
|
|
color: #7a869a;
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
:deep(.drag-ghost > td) {
|
|
background-color: rgba(41, 90, 188, 0.1) !important;
|
|
}
|
|
:deep(.drag-chosen > td) {
|
|
background-color: rgba(41, 90, 188, 0.06);
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.meeting-meta-grid {
|
|
grid-template-columns: repeat(2, minmax(140px, 1fr));
|
|
}
|
|
}
|
|
@media (max-width: 768px) {
|
|
.meeting-meta-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|