785 lines
23 KiB
Vue
785 lines
23 KiB
Vue
<template>
|
||
<ContentWrap>
|
||
<!-- 页面标题 -->
|
||
<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>
|
||
|
||
<!-- 搜索栏 -->
|
||
<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-select v-model="queryParams.agendaCategory" placeholder="议程分类" clearable class="search-input">
|
||
<el-option v-for="item in REVIEW_AGENDA_CATEGORY_OPTIONS" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
<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="handleDelete()">
|
||
批量删除
|
||
</button>
|
||
</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="46" 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="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)"
|
||
/>
|
||
<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)"
|
||
/>
|
||
</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="评审结果" 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>
|
||
|
||
<!-- 编辑/新增项目弹窗 -->
|
||
<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="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-select v-model="formData.agendaCategory" placeholder="请选择议程分类" style="width: 100%">
|
||
<el-option v-for="item in REVIEW_AGENDA_CATEGORY_OPTIONS" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
</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="reviewDate">
|
||
<el-date-picker v-model="formData.reviewDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择评审日期" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="评审结果" prop="reviewResult">
|
||
<el-select v-model="formData.reviewResult" placeholder="请选择评审结果" clearable style="width: 100%">
|
||
<el-option label="通过" value="PASS" />
|
||
<el-option label="不通过" value="REJECT" />
|
||
</el-select>
|
||
</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, 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,
|
||
updateReviewProjectSeqBatch,
|
||
createReviewProject,
|
||
deleteReviewProject,
|
||
type ReviewMeetingProjectRespVO
|
||
} from '@/api/review/project'
|
||
import { formatDate } from '@/utils/formatTime'
|
||
|
||
defineOptions({ name: 'ReviewMeetingProject' })
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const reviewMeetingId = Number(route.params.meetingId)
|
||
|
||
const loading = ref(false)
|
||
const sortLoading = ref(false)
|
||
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
||
const total = ref(0)
|
||
const meetingInfo = ref<any>({})
|
||
const tableRef = ref()
|
||
let sortableInstance: Sortable | null = null
|
||
|
||
const 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
|
||
startTime?: string
|
||
endTime?: string
|
||
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,
|
||
projectTitle: undefined as string | undefined,
|
||
agendaCategory: undefined as string | undefined,
|
||
reporter: undefined as string | undefined
|
||
})
|
||
|
||
const getList = async () => {
|
||
loading.value = true
|
||
try {
|
||
const data = await getReviewProjectPage(queryParams)
|
||
list.value = data.list
|
||
total.value = data.total
|
||
syncInlineSnapshots(data.list || [])
|
||
} finally {
|
||
loading.value = false
|
||
await nextTick()
|
||
initSortable()
|
||
}
|
||
}
|
||
|
||
const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({
|
||
seqNo: row.seqNo,
|
||
startTime: row.startTime,
|
||
endTime: row.endTime,
|
||
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.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)
|
||
|| normalize(current.reporterUnit) !== normalize(snapshot.reporterUnit)
|
||
|| normalize(current.reviewDate) !== normalize(snapshot.reviewDate)
|
||
|| normalize(current.reviewResult) !== normalize(snapshot.reviewResult)
|
||
}
|
||
|
||
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) => {
|
||
if (queryParams.projectTitle || queryParams.agendaCategory || queryParams.reporter) {
|
||
ElMessage.warning('请先清空筛选条件后再拖拽排序')
|
||
await getList()
|
||
return
|
||
}
|
||
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 allData = await getReviewProjectPage({
|
||
reviewMeetingId,
|
||
pageNo: 1,
|
||
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
|
||
})
|
||
const fullList = [...(allData.list || [])]
|
||
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
|
||
|
||
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
|
||
await updateReviewProjectSeqBatch(
|
||
changedRows.map((row) => ({
|
||
id: row.id,
|
||
seqNo: row.seqNo
|
||
}))
|
||
)
|
||
ElMessage.success('排序已更新')
|
||
} catch {
|
||
ElMessage.error('排序保存失败,已恢复原列表')
|
||
await getList()
|
||
} finally {
|
||
sortLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
||
const resetQuery = () => {
|
||
queryParams.projectTitle = undefined
|
||
queryParams.agendaCategory = undefined
|
||
queryParams.reporter = undefined
|
||
handleQuery()
|
||
}
|
||
|
||
const selectedIds = ref<number[]>([])
|
||
const handleSelectionChange = (val: ReviewMeetingProjectRespVO[]) => {
|
||
selectedIds.value = val.map(v => v.id)
|
||
}
|
||
|
||
const handleDelete = async (id?: number) => {
|
||
const ids = id ? [id] : selectedIds.value
|
||
if (ids.length === 0) return
|
||
await ElMessageBox.confirm(`确认删除选中的 ${ids.length} 个项目吗?`, '警告', { type: 'warning' })
|
||
await deleteReviewProject(ids)
|
||
ElMessage.success('删除成功')
|
||
getList()
|
||
}
|
||
|
||
const formVisible = ref(false)
|
||
const formType = ref<'create' | 'update'>('create')
|
||
const formLoading = ref(false)
|
||
const formRef = ref()
|
||
const formData = reactive<Partial<ReviewMeetingProjectRespVO>>({})
|
||
const formRules = {
|
||
agendaCategory: [{ 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])
|
||
formData.reviewMeetingId = reviewMeetingId
|
||
} 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) => {
|
||
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-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; }
|
||
|
||
.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;
|
||
}
|
||
|
||
/* ── 搜索栏 ── */
|
||
.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;
|
||
}
|
||
|
||
.inline-time-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.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) {
|
||
border-radius: 6px;
|
||
border: 1px solid transparent;
|
||
box-shadow: none;
|
||
background-color: #f8fafc;
|
||
transition: border-color 0.2s, background-color 0.2s;
|
||
}
|
||
: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) {
|
||
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) {
|
||
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);
|
||
}
|
||
</style>
|