763 lines
20 KiB
Vue
763 lines
20 KiB
Vue
<template>
|
|
<ContentWrap>
|
|
<div class="page-title">
|
|
<span class="back-btn" @click="handleBack">‹</span>
|
|
项目资料
|
|
</div>
|
|
|
|
<div class="info-panel">
|
|
<div class="info-panel-title">{{ projectInfo.projectTitle || '-' }}</div>
|
|
<div class="info-panel-meta">
|
|
<div class="meta-left">
|
|
<span v-if="projectInfo.startTime || projectInfo.endTime" class="meta-tag">
|
|
{{ projectInfo.startTime || '' }}{{ projectInfo.endTime ? ` - ${projectInfo.endTime}` : '' }}
|
|
</span>
|
|
<span class="meta-item">会中序号:{{ projectInfo.seqNo ?? '-' }}</span>
|
|
</div>
|
|
<div class="meta-right">所属会议:{{ projectInfo.meetingName || '-' }}</div>
|
|
</div>
|
|
<div class="info-panel-footer">
|
|
<div class="info-footer-item">
|
|
<span class="footer-label">议程分类</span>
|
|
<span class="footer-value">{{ materialSummary.agendaType || projectInfo.agendaCategory || '-' }}</span>
|
|
</div>
|
|
<div class="info-footer-item">
|
|
<span class="footer-label">报告人</span>
|
|
<span class="footer-value">
|
|
{{ projectInfo.reporter || '-' }}
|
|
<em v-if="projectInfo.reporterUnit">{{ projectInfo.reporterUnit }}</em>
|
|
</span>
|
|
</div>
|
|
<div class="info-footer-item">
|
|
<span class="footer-label">主持人</span>
|
|
<span class="footer-value">{{ projectInfo.meetingHost || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ai-status-card" v-loading="aiLoading">
|
|
<div class="ai-status-header">
|
|
<div>
|
|
<div class="ai-status-title">AI 构建状态</div>
|
|
<div class="ai-status-sub">{{ aiSummary?.blockReason || 'AI 能力仅影响摘要与助手,不影响主流程' }}</div>
|
|
</div>
|
|
<div class="ai-status-actions">
|
|
<span :class="`ai-pill ai-pill-${aiSummary?.status ?? 0}`">{{ aiSummary?.statusName || '待构建' }}</span>
|
|
<el-button
|
|
v-hasPermi="['review:meeting:update']"
|
|
type="primary"
|
|
:loading="aiActionLoading"
|
|
@click="handleBuildAllAi"
|
|
>
|
|
构建全部待构建文件
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
<div class="ai-status-metrics">
|
|
<div class="metric-item">
|
|
<span class="metric-label">待构建</span>
|
|
<span class="metric-value">{{ aiSummary?.pendingFileCount ?? 0 }}</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-label">失败</span>
|
|
<span class="metric-value metric-danger">{{ aiSummary?.failedFileCount ?? 0 }}</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-label">无需构建</span>
|
|
<span class="metric-value">{{ aiSummary?.skippedFileCount ?? 0 }}</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-label">最近更新时间</span>
|
|
<span class="metric-value metric-time">{{ aiSummary?.updatedTime || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="material-card" v-loading="loading">
|
|
<div class="group-title">会前资料</div>
|
|
<el-table :data="beforeMeetingMaterials" border class="material-table" empty-text="暂无会前资料">
|
|
<el-table-column label="资料类型" min-width="220">
|
|
<template #default="{ row }">
|
|
<span>
|
|
{{ row.materialName }}
|
|
<span v-if="row.required" class="required-flag">*</span>
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="资料文件" min-width="300">
|
|
<template #default="{ row }">
|
|
<div v-if="row.latestFile" class="file-block">
|
|
<span class="file-name">{{ row.latestFile.fileName }}</span>
|
|
<span :class="`file-ai-pill file-ai-pill-${row.latestFile.aiBuildStatus ?? 0}`">
|
|
{{ row.latestFile.aiBuildStatusName || '待构建' }}
|
|
</span>
|
|
<span v-if="row.latestFile.aiBuildErrorMessage" class="file-ai-error">
|
|
{{ row.latestFile.aiBuildErrorMessage }}
|
|
</span>
|
|
</div>
|
|
<span v-else class="file-empty">未上传</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="340" align="center">
|
|
<template #default="{ row }">
|
|
<a class="op-link" @click="triggerUpload(row)">{{ row.latestFile ? '替换' : '上传' }}</a>
|
|
<a class="op-link" :class="{ disabled: !row.latestFile }" @click="row.latestFile && handleDownload(row.latestFile)">下载</a>
|
|
<a class="op-link" @click="openHistory(row)">历史版本</a>
|
|
<a
|
|
v-if="row.latestFile && row.latestFile.aiBuildStatus !== 4"
|
|
class="op-link"
|
|
:class="{ disabled: rebuildingFileId === row.latestFile.id }"
|
|
@click="row.latestFile && handleRebuildFileAi(row.latestFile)"
|
|
>
|
|
{{ rebuildingFileId === row.latestFile.id ? '提交中' : '重建AI' }}
|
|
</a>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div class="group-title group-after">会后资料</div>
|
|
<el-table :data="afterMeetingMaterials" border class="material-table" empty-text="暂无会后资料">
|
|
<el-table-column label="资料类型" min-width="220">
|
|
<template #default="{ row }">
|
|
<span>
|
|
{{ row.materialName }}
|
|
<span v-if="row.required" class="required-flag">*</span>
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="资料文件" min-width="300">
|
|
<template #default="{ row }">
|
|
<div v-if="row.latestFile" class="file-block">
|
|
<span class="file-name">{{ row.latestFile.fileName }}</span>
|
|
<span :class="`file-ai-pill file-ai-pill-${row.latestFile.aiBuildStatus ?? 0}`">
|
|
{{ row.latestFile.aiBuildStatusName || '待构建' }}
|
|
</span>
|
|
<span v-if="row.latestFile.aiBuildErrorMessage" class="file-ai-error">
|
|
{{ row.latestFile.aiBuildErrorMessage }}
|
|
</span>
|
|
</div>
|
|
<span v-else class="file-empty">未上传</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="340" align="center">
|
|
<template #default="{ row }">
|
|
<a class="op-link" @click="triggerUpload(row)">{{ row.latestFile ? '替换' : '上传' }}</a>
|
|
<a class="op-link" :class="{ disabled: !row.latestFile }" @click="row.latestFile && handleDownload(row.latestFile)">下载</a>
|
|
<a class="op-link" @click="openHistory(row)">历史版本</a>
|
|
<a
|
|
v-if="row.latestFile && row.latestFile.aiBuildStatus !== 4"
|
|
class="op-link"
|
|
:class="{ disabled: rebuildingFileId === row.latestFile.id }"
|
|
@click="row.latestFile && handleRebuildFileAi(row.latestFile)"
|
|
>
|
|
{{ rebuildingFileId === row.latestFile.id ? '提交中' : '重建AI' }}
|
|
</a>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
|
|
<input
|
|
ref="uploadInputRef"
|
|
type="file"
|
|
class="hidden-upload-input"
|
|
@change="handleFileSelected"
|
|
/>
|
|
|
|
<el-dialog v-model="historyVisible" title="历史版本" width="760px">
|
|
<el-table :data="historyList" border>
|
|
<el-table-column label="版本" width="80" align="center">
|
|
<template #default="{ row }">v{{ row.file.version || 1 }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="文件名" prop="file.fileName" min-width="280" show-overflow-tooltip />
|
|
<el-table-column label="上传人" prop="file.creator" width="120" align="center" />
|
|
<el-table-column label="上传时间" prop="file.createTime" width="180" align="center" />
|
|
<el-table-column label="操作" width="90" align="center">
|
|
<template #default="{ row }">
|
|
<a class="op-link" @click="handleDownload(row.file)">下载</a>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-dialog>
|
|
|
|
<div class="form-footer">
|
|
<button class="btn-default" @click="handleBack">返回</button>
|
|
</div>
|
|
</ContentWrap>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { getReviewMeeting } from '@/api/review/meeting'
|
|
import {
|
|
getProjectAiSummary,
|
|
rebuildProjectAiSummary,
|
|
rebuildReviewFileAi,
|
|
type ReviewAiSummaryVO
|
|
} from '@/api/review/ai'
|
|
import {
|
|
getProjectMaterialHistory,
|
|
getProjectMaterialSummary,
|
|
uploadProjectMaterial,
|
|
type ReviewMeetingFileRespVO,
|
|
type ReviewMeetingMaterialHistoryRespVO,
|
|
type ReviewMeetingMaterialItemRespVO,
|
|
type ReviewMeetingMaterialSummaryRespVO
|
|
} from '@/api/review/project'
|
|
|
|
defineOptions({ name: 'ReviewProjectDetail' })
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const reviewMeetingId = Number(route.params.meetingId)
|
|
const reviewMeetingProjectId = Number(route.params.projectId)
|
|
|
|
const state = window.history.state || {}
|
|
const projectInfo = ref({
|
|
projectTitle: state.projectTitle as string | undefined,
|
|
seqNo: state.seqNo as number | undefined,
|
|
startTime: state.startTime as string | undefined,
|
|
endTime: state.endTime as string | undefined,
|
|
agendaCategory: state.agendaCategory as string | undefined,
|
|
reporter: state.reporter as string | undefined,
|
|
reporterUnit: state.reporterUnit as string | undefined,
|
|
meetingHost: state.meetingHost as string | undefined,
|
|
meetingName: state.meetingName as string | undefined
|
|
})
|
|
|
|
const loading = ref(false)
|
|
const aiLoading = ref(false)
|
|
const aiActionLoading = ref(false)
|
|
const rebuildingFileId = ref<number>()
|
|
let aiPollingTimer: ReturnType<typeof setTimeout> | null = null
|
|
const materialSummary = ref<ReviewMeetingMaterialSummaryRespVO>({
|
|
agendaType: '',
|
|
templateAvailable: false,
|
|
materials: []
|
|
})
|
|
const aiSummary = ref<ReviewAiSummaryVO | null>(null)
|
|
|
|
const uploadInputRef = ref<HTMLInputElement>()
|
|
const uploadTarget = ref<ReviewMeetingMaterialItemRespVO>()
|
|
|
|
const historyVisible = ref(false)
|
|
const historyList = ref<ReviewMeetingMaterialHistoryRespVO[]>([])
|
|
|
|
const PHASE_MAPPING: Record<string, Record<string, 'before' | 'after'>> = {
|
|
项目申报论证: {
|
|
PROJECT_ARGUMENT_DOC: 'before',
|
|
PROJECT_DEFENSE_PPT: 'before',
|
|
PROJECT_DRAFT_EXPERT_OPINION: 'before',
|
|
PROJECT_EXPERT_OPINION_SIGNED: 'after',
|
|
PROJECT_RECTIFICATION_NOTICE: 'after'
|
|
},
|
|
项目立项: {
|
|
PROJECT_ARGUMENT_DOC: 'before',
|
|
PROJECT_DEFENSE_PPT: 'before',
|
|
PROJECT_DRAFT_EXPERT_OPINION: 'before',
|
|
PROJECT_EXPERT_OPINION_SIGNED: 'after',
|
|
PROJECT_RECTIFICATION_NOTICE: 'after'
|
|
},
|
|
预验收: {
|
|
PRE_PROJECT_CONTRACT: 'before',
|
|
PRE_REQUIREMENT_SPEC: 'before',
|
|
PRE_ACCEPTANCE_REPORT: 'before',
|
|
PRE_USER_FUNCTION_TEST: 'before',
|
|
PRE_SERVICE_CHECKLIST: 'before',
|
|
PRE_DRAFT_EXPERT_OPINION: 'before',
|
|
PRE_RECTIFICATION_NOTICE: 'after'
|
|
},
|
|
项目终验: {
|
|
FINAL_ACCEPTANCE_DOC: 'before',
|
|
FINAL_DEFENSE_PPT: 'before',
|
|
FINAL_DRAFT_EXPERT_OPINION: 'before',
|
|
FINAL_SIGNED_EXPERT_OPINION: 'after',
|
|
FINAL_RECTIFICATION_OPINION: 'after',
|
|
FINAL_RECTIFICATION_REPLY: 'after'
|
|
}
|
|
}
|
|
|
|
const beforeMeetingMaterials = computed(() => {
|
|
const agendaType = materialSummary.value.agendaType
|
|
const agendaMap = PHASE_MAPPING[agendaType] || {}
|
|
return materialSummary.value.materials.filter((item) => agendaMap[item.materialCode] !== 'after')
|
|
})
|
|
|
|
const afterMeetingMaterials = computed(() => {
|
|
const agendaType = materialSummary.value.agendaType
|
|
const agendaMap = PHASE_MAPPING[agendaType] || {}
|
|
return materialSummary.value.materials.filter((item) => agendaMap[item.materialCode] === 'after')
|
|
})
|
|
|
|
const loadSummary = async () => {
|
|
loading.value = true
|
|
try {
|
|
materialSummary.value = await getProjectMaterialSummary(reviewMeetingProjectId)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const loadAiSummary = async () => {
|
|
aiLoading.value = true
|
|
try {
|
|
aiSummary.value = await getProjectAiSummary(reviewMeetingProjectId)
|
|
} finally {
|
|
aiLoading.value = false
|
|
}
|
|
}
|
|
|
|
const clearAiPolling = () => {
|
|
if (aiPollingTimer) {
|
|
clearTimeout(aiPollingTimer)
|
|
aiPollingTimer = null
|
|
}
|
|
}
|
|
|
|
const scheduleAiPolling = () => {
|
|
clearAiPolling()
|
|
if (aiSummary.value?.status !== 1) return
|
|
aiPollingTimer = setTimeout(async () => {
|
|
await Promise.all([loadAiSummary(), loadSummary()])
|
|
scheduleAiPolling()
|
|
}, 1000)
|
|
}
|
|
|
|
const triggerUpload = (row: ReviewMeetingMaterialItemRespVO) => {
|
|
uploadTarget.value = row
|
|
if (!uploadInputRef.value) return
|
|
uploadInputRef.value.value = ''
|
|
uploadInputRef.value.accept = toAcceptAttr(row.acceptExts)
|
|
uploadInputRef.value.click()
|
|
}
|
|
|
|
const handleFileSelected = async (event: Event) => {
|
|
const input = event.target as HTMLInputElement
|
|
const file = input.files?.[0]
|
|
const target = uploadTarget.value
|
|
if (!file || !target) return
|
|
if (file.size > 500 * 1024 * 1024) {
|
|
ElMessage.error('文件大小不能超过 500MB')
|
|
return
|
|
}
|
|
if (!checkFileExt(file.name, target.acceptExts)) {
|
|
ElMessage.error(`文件类型不合法,仅支持:${target.acceptExts}`)
|
|
return
|
|
}
|
|
loading.value = true
|
|
try {
|
|
await uploadProjectMaterial(reviewMeetingId, reviewMeetingProjectId, target.materialCode, file)
|
|
ElMessage.success('上传成功')
|
|
await Promise.all([loadSummary(), loadAiSummary()])
|
|
} catch {
|
|
ElMessage.error('上传失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleBuildAllAi = async () => {
|
|
await ElMessageBox.confirm('确认构建当前项目全部待构建资料的 AI 吗?', 'AI 构建确认', {
|
|
type: 'warning',
|
|
confirmButtonText: '确认构建'
|
|
})
|
|
aiActionLoading.value = true
|
|
try {
|
|
await rebuildProjectAiSummary(reviewMeetingProjectId)
|
|
ElMessage.success('AI 构建任务已提交')
|
|
await Promise.all([loadAiSummary(), loadSummary()])
|
|
scheduleAiPolling()
|
|
} finally {
|
|
aiActionLoading.value = false
|
|
}
|
|
}
|
|
|
|
const handleRebuildFileAi = async (file: ReviewMeetingFileRespVO) => {
|
|
await ElMessageBox.confirm(`确认重建文件「${file.fileName}」的 AI 资产吗?`, '文件 AI 重建确认', {
|
|
type: 'warning',
|
|
confirmButtonText: '确认重建'
|
|
})
|
|
rebuildingFileId.value = file.id
|
|
try {
|
|
await rebuildReviewFileAi(file.id)
|
|
ElMessage.success('文件 AI 重建任务已提交')
|
|
await Promise.all([loadAiSummary(), loadSummary()])
|
|
scheduleAiPolling()
|
|
} finally {
|
|
rebuildingFileId.value = undefined
|
|
}
|
|
}
|
|
|
|
const openHistory = async (row: ReviewMeetingMaterialItemRespVO) => {
|
|
historyVisible.value = true
|
|
historyList.value = []
|
|
historyList.value = await getProjectMaterialHistory(reviewMeetingProjectId, row.materialCode)
|
|
}
|
|
|
|
const handleDownload = (file: ReviewMeetingFileRespVO) => {
|
|
window.open(file.fileUrl, '_blank')
|
|
}
|
|
|
|
const toAcceptAttr = (exts: string) =>
|
|
exts
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
.map((item) => `.${item}`)
|
|
.join(',')
|
|
|
|
const checkFileExt = (name: string, exts: string) => {
|
|
if (!name.includes('.')) return false
|
|
const ext = name.split('.').pop()?.toLowerCase() || ''
|
|
return exts
|
|
.split(',')
|
|
.map((item) => item.trim().toLowerCase())
|
|
.includes(ext)
|
|
}
|
|
|
|
const handleBack = () => {
|
|
router.back()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadSummary(), loadAiSummary()])
|
|
scheduleAiPolling()
|
|
getReviewMeeting(reviewMeetingId)
|
|
.then((meeting) => {
|
|
if (!projectInfo.value.meetingName) {
|
|
projectInfo.value.meetingName = meeting.name
|
|
}
|
|
projectInfo.value.meetingHost = meeting.host
|
|
})
|
|
.catch(() => {})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
clearAiPolling()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid #e1e7f0;
|
|
}
|
|
|
|
.back-btn {
|
|
font-size: 28px;
|
|
color: #295abc;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
margin-right: 2px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.info-panel {
|
|
background: linear-gradient(135deg, rgba(41, 90, 188, 0.06) 0%, rgba(41, 90, 188, 0.03) 100%);
|
|
border: 1px solid rgba(41, 90, 188, 0.15);
|
|
border-radius: 8px;
|
|
padding: 18px 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.info-panel-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #295abc;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.info-panel-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.meta-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.meta-tag {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
background-color: rgba(41, 90, 188, 0.1);
|
|
color: #295abc;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.meta-item,
|
|
.meta-right {
|
|
font-size: 13px;
|
|
color: #666;
|
|
}
|
|
|
|
.info-panel-footer {
|
|
display: flex;
|
|
gap: 32px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid rgba(41, 90, 188, 0.12);
|
|
}
|
|
|
|
.info-footer-item {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
}
|
|
|
|
.footer-label {
|
|
font-size: 13px;
|
|
color: #999;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.footer-value {
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
.footer-value em {
|
|
color: #666;
|
|
font-style: normal;
|
|
margin-left: 6px;
|
|
}
|
|
|
|
.material-card {
|
|
background-color: #fff;
|
|
border: 1px solid #e6eaf2;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.ai-status-card {
|
|
background: #fff9ec;
|
|
border: 1px solid #f2d6a2;
|
|
border-radius: 8px;
|
|
padding: 16px 18px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.ai-status-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.ai-status-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #6d4d12;
|
|
}
|
|
|
|
.ai-status-sub {
|
|
margin-top: 4px;
|
|
font-size: 13px;
|
|
color: #8b6a2b;
|
|
}
|
|
|
|
.ai-status-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.ai-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 74px;
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ai-pill-0 {
|
|
color: #8a6214;
|
|
background: #fff3cd;
|
|
}
|
|
|
|
.ai-pill-1 {
|
|
color: #0f766e;
|
|
background: #ccfbf1;
|
|
}
|
|
|
|
.ai-pill-2 {
|
|
color: #166534;
|
|
background: #dcfce7;
|
|
}
|
|
|
|
.ai-pill-3 {
|
|
color: #b91c1c;
|
|
background: #fee2e2;
|
|
}
|
|
|
|
.ai-status-metrics {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.metric-item {
|
|
background: rgba(255, 255, 255, 0.72);
|
|
border: 1px solid rgba(242, 214, 162, 0.72);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.metric-label {
|
|
display: block;
|
|
color: #8b6a2b;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.metric-value {
|
|
display: block;
|
|
color: #5d4211;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.metric-danger {
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.metric-time {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.group-title {
|
|
margin: 8px 0 10px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #606266;
|
|
}
|
|
|
|
.group-after {
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.required-flag {
|
|
color: #fc4f54;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.file-name {
|
|
color: #333;
|
|
}
|
|
|
|
.file-block {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.file-ai-pill {
|
|
display: inline-flex;
|
|
width: fit-content;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.file-ai-pill-0 {
|
|
color: #8a6214;
|
|
background: #fff3cd;
|
|
}
|
|
|
|
.file-ai-pill-1 {
|
|
color: #0f766e;
|
|
background: #ccfbf1;
|
|
}
|
|
|
|
.file-ai-pill-2 {
|
|
color: #166534;
|
|
background: #dcfce7;
|
|
}
|
|
|
|
.file-ai-pill-3 {
|
|
color: #b91c1c;
|
|
background: #fee2e2;
|
|
}
|
|
|
|
.file-ai-pill-4 {
|
|
color: #475569;
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
.file-ai-error {
|
|
color: #b91c1c;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.file-empty {
|
|
color: #909399;
|
|
}
|
|
|
|
.op-link {
|
|
display: inline-block;
|
|
color: #295abc;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
margin: 0 8px;
|
|
}
|
|
|
|
.op-link.disabled {
|
|
color: #c0c4cc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.hidden-upload-input {
|
|
display: none;
|
|
}
|
|
|
|
.form-footer {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.btn-default {
|
|
min-width: 90px;
|
|
height: 36px;
|
|
border: 1px solid #dcdfe6;
|
|
background-color: #fff;
|
|
border-radius: 4px;
|
|
color: #606266;
|
|
cursor: pointer;
|
|
}
|
|
|
|
:deep(.material-table .el-table__header-wrapper th) {
|
|
background-color: #f5f7fa;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.ai-status-header,
|
|
.info-panel-meta,
|
|
.info-panel-footer {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.ai-status-metrics {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
</style>
|