yanghao 4 dni temu
rodzic
commit
be2abb264b

+ 2 - 2
src/views/expert/team/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <el-form
       ref="queryFormRef"
       :inline="true"
@@ -46,7 +46,7 @@
     </el-form>
   </ContentWrap>
 
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <zm-table :loading="loading" :data="list" row-key="id" @row-click="handleRowClick">
       <zm-table-column label="团队所属公司" align="center" min-width="180">
         <template #default="{ row }">

+ 0 - 0
src/views/researchProject/budget/index.vue


+ 132 - 18
src/views/researchProject/list/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <el-form ref="queryFormRef" :model="queryParams" inline class="project-filter">
       <el-form-item label="项目名称" prop="projectName">
         <el-input
@@ -32,7 +32,7 @@
     </el-form>
   </ContentWrap>
 
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <div class="project-table-heading">
       <div><h2>科研项目</h2><span>维护项目申报信息、预算及阶段性计划</span></div>
       <el-tag type="info" effect="plain">共 {{ filteredList.length }} 个项目</el-tag>
@@ -188,21 +188,69 @@
           </el-table>
         </div>
       </el-form-item>
-      <el-divider content-position="left">费用预算(元)</el-divider>
-      <el-row :gutter="18" class="budget-grid"
-        ><el-col v-for="item in budgetItems" :key="item.key" :span="6"
-          ><el-form-item :label="item.label"
-            ><el-input-number
-              v-model="formData.budget[item.key]"
-              :min="0"
-              :precision="2"
-              controls-position="right"
-              class="w-full" /></el-form-item></el-col
-      ></el-row>
-      <div class="budget-total"
-        ><span>预算合计</span><strong>{{ budgetTotal.toFixed(2) }}</strong
-        ><span>元</span></div
-      >
+      <el-divider content-position="left">费用预算明细(元)</el-divider>
+      <div class="budget-editor">
+        <div class="budget-toolbar">
+          <span>请按财务科目录入项目预算,金额需填写到元。</span>
+          <el-button type="primary" plain size="small" @click="addBudgetDetail">
+            <Icon icon="ep:plus" class="mr-5px" />新增预算科目
+          </el-button>
+        </div>
+        <el-table :data="formData.budgetDetails" border class="budget-table">
+          <el-table-column type="index" label="#" width="56" align="center" />
+          <el-table-column label="财务科目" min-width="220">
+            <template #default="{ row }">
+              <el-select
+                v-model="row.subject"
+                filterable
+                allow-create
+                default-first-option
+                class="w-full"
+                placeholder="请选择或输入财务科目"
+              >
+                <el-option
+                  v-for="item in budgetItems"
+                  :key="item.key"
+                  :label="item.label"
+                  :value="item.label"
+                />
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="预算金额(元)" width="220" align="right">
+            <template #default="{ row }">
+              <el-input-number
+                v-model="row.amount"
+                :min="0"
+                :precision="2"
+                controls-position="right"
+                class="w-full"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column label="预算说明" min-width="260">
+            <template #default="{ row }">
+              <el-input v-model="row.remark" maxlength="100" placeholder="请输入该科目的预算用途" />
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="90" align="center">
+            <template #default="{ $index }">
+              <el-button
+                link
+                type="danger"
+                :disabled="formData.budgetDetails.length === 1"
+                @click="removeBudgetDetail($index)"
+              >
+                <Icon icon="ep:delete" />
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="budget-total">
+          <span>预算合计</span><strong>{{ budgetTotal.toFixed(2) }}</strong
+          ><span>元</span>
+        </div>
+      </div>
       <el-divider content-position="left">附件上传</el-divider>
       <el-form-item label="立项申请表" required
         ><UploadFile
@@ -256,6 +304,7 @@ import { ElMessage, ElMessageBox, type FormRules } from 'element-plus'
 
 defineOptions({ name: 'ResearchProjectList' })
 type ScheduleItem = { workPlan: string; deadline: string }
+type BudgetDetail = { subject: string; amount: number; remark: string }
 type Project = {
   id: number
   projectLevel: string
@@ -268,6 +317,7 @@ type Project = {
   researchContent: string
   innovation: string
   schedule: ScheduleItem[]
+  budgetDetails: BudgetDetail[]
   budget: Record<string, number>
   attachments: { application: any[]; report: any[]; others: any[] }
   status: string
@@ -297,6 +347,12 @@ const budgetItems = [
   { key: 'other', label: '其他' }
 ]
 const createSchedule = (): ScheduleItem => ({ workPlan: '', deadline: '' })
+const createBudgetDetail = (data: Partial<BudgetDetail> = {}): BudgetDetail => ({
+  subject: '',
+  amount: 0,
+  remark: '',
+  ...data
+})
 const createBudget = () =>
   Object.fromEntries(budgetItems.map(({ key }) => [key, 0])) as Record<string, number>
 const createForm = () => ({
@@ -310,6 +366,7 @@ const createForm = () => ({
   researchContent: '',
   innovation: '',
   schedule: [createSchedule(), createSchedule(), createSchedule()],
+  budgetDetails: [createBudgetDetail()],
   budget: createBudget(),
   attachments: { application: [], report: [], others: [] }
 })
@@ -329,6 +386,11 @@ const list = ref<Project[]>([
       { workPlan: '完成方案设计', deadline: '2026-06-30' },
       { workPlan: '完成样机试制', deadline: '2026-10-31' }
     ],
+    budgetDetails: [
+      createBudgetDetail({ subject: '直接材料投入', amount: 180000, remark: '样机材料及加工费用' }),
+      createBudgetDetail({ subject: '检验费', amount: 65000, remark: '高温高压环境测试' }),
+      createBudgetDetail({ subject: '差旅费', amount: 30000, remark: '现场调研与试验支持' })
+    ],
     budget: createBudget(),
     attachments: { application: [], report: [], others: [] },
     status: '进行中'
@@ -362,7 +424,7 @@ const formRules = reactive<FormRules>({
   innovation: [{ required: true, message: '请输入创新点', trigger: 'blur' }]
 })
 const budgetTotal = computed(() =>
-  Object.values(formData.value.budget).reduce((sum, value) => sum + Number(value || 0), 0)
+  formData.value.budgetDetails.reduce((sum, item) => sum + Number(item.amount || 0), 0)
 )
 const handleQuery = () => {
   queryParams.pageNo = 1
@@ -394,6 +456,14 @@ const cloneProjectForm = (project: Project) => ({
   researchContent: project.researchContent,
   innovation: project.innovation,
   schedule: project.schedule.map((item) => ({ ...item })),
+  budgetDetails:
+    project.budgetDetails?.length > 0
+      ? project.budgetDetails.map((item) => ({ ...item }))
+      : budgetItems
+          .filter(({ key }) => Number(project.budget?.[key] || 0) > 0)
+          .map(({ label, key }) =>
+            createBudgetDetail({ subject: label, amount: project.budget[key] })
+          ),
   budget: { ...project.budget },
   attachments: cloneAttachments(project.attachments)
 })
@@ -405,12 +475,24 @@ const openForm = (project?: Project) => {
 }
 const addSchedule = () => formData.value.schedule.push(createSchedule())
 const removeSchedule = (index: number) => formData.value.schedule.splice(index, 1)
+const addBudgetDetail = () => formData.value.budgetDetails.push(createBudgetDetail())
+const removeBudgetDetail = (index: number) => formData.value.budgetDetails.splice(index, 1)
 const submitForm = async () => {
   const valid = await formRef.value?.validate().catch(() => false)
   if (!valid) return
+  if (formData.value.budgetDetails.some((item) => !item.subject.trim())) {
+    ElMessage.warning('请完善预算科目后再保存')
+    return
+  }
   submitLoading.value = true
   const project = {
     ...formData.value,
+    budget: Object.fromEntries(
+      formData.value.budgetDetails.map((item, index) => [
+        item.subject || `subject_${index}`,
+        item.amount
+      ])
+    ),
     id: editingId.value || Date.now(),
     status: '进行中'
   } as Project
@@ -473,10 +555,42 @@ const removeProject = async (id: number) => {
   color: var(--el-text-color-secondary);
   font-size: 13px;
 }
+.budget-editor {
+  width: 100%;
+}
+.budget-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+.budget-table {
+  width: 100%;
+}
+.budget-total {
+  display: flex;
+  justify-content: flex-end;
+  align-items: baseline;
+  gap: 8px;
+  padding: 14px 8px 0;
+  color: var(--el-text-color-secondary);
+}
+.budget-total strong {
+  color: var(--el-color-primary);
+  font-size: 22px;
+}
 @media (max-width: 900px) {
   .budget-grid :deep(.el-col) {
     width: 50%;
     flex: 0 0 50%;
   }
+  .budget-table {
+    min-width: 760px;
+  }
+  .budget-editor {
+    overflow-x: auto;
+  }
 }
 </style>

+ 696 - 0
src/views/researchProject/review/index.vue

@@ -0,0 +1,696 @@
+<template>
+  <div class="review-page">
+    <div class="review-filter" style="border: none">
+      <el-form :model="queryParams" inline>
+        <el-form-item label="项目名称">
+          <el-input
+            v-model="queryParams.projectName"
+            clearable
+            placeholder="请输入项目名称"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="项目类型">
+          <el-select
+            v-model="queryParams.projectType"
+            clearable
+            placeholder="请选择项目类型"
+            class="!w-210px"
+          >
+            <el-option v-for="item in projectTypes" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="提交时间">
+          <el-date-picker
+            v-model="queryParams.submitDate"
+            type="date"
+            value-format="YYYY-MM-DD"
+            clearable
+            placeholder="请选择日期"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery"
+            ><Icon icon="ep:search" class="mr-5px" />查询</el-button
+          >
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="review-layout">
+      <ContentWrap class="project-list-panel" style="border: none">
+        <div class="status-tabs">
+          <button
+            v-for="item in statusTabs"
+            :key="item.value"
+            :class="{ active: activeStatus === item.value }"
+            @click="activeStatus = item.value"
+          >
+            {{ item.label }} <span>{{ statusCount(item.value) }}</span>
+          </button>
+        </div>
+        <div class="project-list">
+          <button
+            v-for="project in filteredProjects"
+            :key="project.id"
+            class="project-item"
+            :class="{ selected: selectedProject?.id === project.id }"
+            @click="selectProject(project)"
+          >
+            <div class="project-item-top">
+              <strong>{{ project.projectName }}</strong>
+              <el-tag :type="statusType(project.status)" size="small">{{ project.status }}</el-tag>
+            </div>
+            <div class="project-meta">{{ project.projectType }} · {{ project.projectLeader }}</div>
+            <div class="project-item-bottom"
+              ><span>提交于 {{ project.submitDate }}</span
+              ><span>{{ project.attachments.length }} 个附件</span></div
+            >
+          </button>
+          <el-empty v-if="!filteredProjects.length" description="暂无待审项目" :image-size="72" />
+        </div>
+      </ContentWrap>
+
+      <ContentWrap v-if="selectedProject" class="review-detail-panel" style="border: none">
+        <!-- <div class="process-strip">
+          <div
+            v-for="(step, index) in selectedProject.process"
+            :key="step.name"
+            class="process-step"
+            :class="step.state"
+          >
+            <div class="step-icon"
+              ><Icon
+                :icon="
+                  step.state === 'done'
+                    ? 'ep:check'
+                    : step.state === 'current'
+                      ? 'ep:clock'
+                      : 'ep:more'
+                "
+            /></div>
+            <div
+              ><strong>{{ step.name }}</strong
+              ><span
+                >{{ step.handler }}<template v-if="step.date"> · {{ step.date }}</template></span
+              ></div
+            >
+            <i v-if="index < selectedProject.process.length - 1"></i>
+          </div>
+        </div> -->
+
+        <el-tabs v-model="activeTab" class="detail-tabs">
+          <el-tab-pane label="项目基本信息" name="info">
+            <el-descriptions :column="2" border class="info-table">
+              <el-descriptions-item label="项目名称" :span="2">{{
+                selectedProject.projectName
+              }}</el-descriptions-item>
+              <el-descriptions-item label="项目级别">{{
+                selectedProject.projectLevel
+              }}</el-descriptions-item>
+              <el-descriptions-item label="项目类型">{{
+                selectedProject.projectType
+              }}</el-descriptions-item>
+              <el-descriptions-item label="执行单位">{{
+                selectedProject.executingUnit
+              }}</el-descriptions-item>
+              <el-descriptions-item label="项目负责人">{{
+                selectedProject.projectLeader
+              }}</el-descriptions-item>
+              <el-descriptions-item label="项目周期">{{
+                selectedProject.period
+              }}</el-descriptions-item>
+              <el-descriptions-item label="申报部门">{{
+                selectedProject.department
+              }}</el-descriptions-item>
+              <el-descriptions-item label="项目目标" :span="2">{{
+                selectedProject.purpose
+              }}</el-descriptions-item>
+              <el-descriptions-item label="主要研究内容" :span="2">{{
+                selectedProject.content
+              }}</el-descriptions-item>
+            </el-descriptions>
+          </el-tab-pane>
+          <el-tab-pane label="预算摘要" name="budget">
+            <div class="budget-summary">
+              <div
+                ><span>预算总额</span
+                ><strong>¥ {{ formatAmount(selectedProject.budgetTotal) }}</strong></div
+              >
+              <div
+                ><span>预算科目</span><strong>{{ selectedProject.budgets.length }} 项</strong></div
+              >
+              <div><span>预算完整性</span><strong class="success-text">已填写</strong></div>
+            </div>
+            <el-table :data="selectedProject.budgets" border>
+              <el-table-column type="index" label="#" width="56" align="center" />
+              <el-table-column prop="subject" label="财务科目" min-width="220" />
+              <el-table-column prop="amount" label="预算金额(元)" width="180" align="right"
+                ><template #default="{ row }">{{
+                  formatAmount(row.amount)
+                }}</template></el-table-column
+              >
+              <el-table-column
+                prop="remark"
+                label="预算说明"
+                min-width="260"
+                show-overflow-tooltip
+              />
+            </el-table>
+          </el-tab-pane>
+          <el-tab-pane label="附件材料" name="attachments">
+            <div class="attachment-tip"
+              ><Icon icon="ep:info-filled" />
+              支持在线查看文件信息,点击“预览”将在新窗口打开文件。</div
+            >
+            <div class="attachment-list">
+              <div
+                v-for="file in selectedProject.attachments"
+                :key="file.name"
+                class="attachment-item"
+              >
+                <div class="file-icon" :class="file.kind"><Icon :icon="fileIcon(file.kind)" /></div>
+                <div class="file-info"
+                  ><strong>{{ file.name }}</strong
+                  ><span>{{ file.size }} · {{ file.uploadedBy }} · {{ file.uploadedAt }}</span></div
+                >
+                <el-button link type="primary" @click="previewFile(file)">预览</el-button>
+                <el-button link type="primary" @click="downloadFile(file)">下载</el-button>
+              </div>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+
+        <div class="review-action">
+          <div
+            ><span>预审意见</span><small>请核对项目目标、研究内容、预算及附件的完整性</small></div
+          >
+          <el-input
+            v-model="reviewComment"
+            type="textarea"
+            :rows="3"
+            maxlength="500"
+            show-word-limit
+            placeholder="请输入审批意见,退回时必须填写"
+          />
+          <div class="action-buttons">
+            <el-button type="danger" plain @click="submitReview('退回修改')"
+              ><Icon icon="ep:back" class="mr-5px" />退回修改</el-button
+            >
+            <el-button type="primary" @click="submitReview('预审通过')"
+              ><Icon icon="ep:circle-check" class="mr-5px" />预审通过</el-button
+            >
+          </div>
+        </div>
+      </ContentWrap>
+      <ContentWrap v-else class="review-detail-panel empty-panel"
+        ><el-empty description="请选择需要预审的项目"
+      /></ContentWrap>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+defineOptions({ name: 'ResearchProjectReview' })
+type Status = '待预审' | '预审通过' | '退回修改'
+type Attachment = {
+  name: string
+  kind: 'pdf' | 'word' | 'excel'
+  size: string
+  uploadedBy: string
+  uploadedAt: string
+  url?: string
+}
+type ProcessStep = {
+  name: string
+  handler: string
+  date: string
+  state: 'done' | 'current' | 'pending'
+}
+type Project = {
+  id: number
+  projectNo: string
+  projectName: string
+  projectType: string
+  projectLevel: string
+  projectLeader: string
+  executingUnit: string
+  department: string
+  period: string
+  purpose: string
+  content: string
+  submitDate: string
+  status: Status
+  budgetTotal: number
+  budgets: { subject: string; amount: number; remark: string }[]
+  attachments: Attachment[]
+  process: ProcessStep[]
+}
+const projectTypes = [
+  '基础研究类',
+  '原理方法类',
+  '产品、工具和设备研发类',
+  '成熟工艺技术配套及推广类',
+  '情报调研类'
+]
+const statusTabs = [
+  { label: '全部', value: 'all' },
+  { label: '待预审', value: '待预审' },
+  { label: '已通过', value: '预审通过' },
+  { label: '已退回', value: '退回修改' }
+]
+const createProject = (data: Partial<Project> = {}): Project => ({
+  id: 1,
+  projectNo: 'XM202603001',
+  projectName: '高温高压井下工具可靠性研究',
+  projectType: '产品、工具和设备研发类',
+  projectLevel: '专业公司级/石油技术公司级',
+  projectLeader: '张明远',
+  executingUnit: '瑞恒兴仪 / 四川瑞都',
+  department: '科技发展部',
+  period: '2026-03 至 2027-03',
+  purpose: '提升复杂工况下工具运行可靠性,形成可推广的井下工具评价方法。',
+  content: '开展材料、结构及工艺适应性研究,完成样机试制、环境测试和现场验证。',
+  submitDate: '2026-09-16',
+  status: '待预审',
+  budgetTotal: 275000,
+  budgets: [
+    { subject: '直接材料投入', amount: 180000, remark: '样机材料及加工费用' },
+    { subject: '检验费', amount: 65000, remark: '高温高压环境测试' },
+    { subject: '差旅费', amount: 30000, remark: '现场调研与试验支持' }
+  ],
+  attachments: [
+    {
+      name: '可行性研究报告.pdf',
+      kind: 'pdf',
+      size: '2.4 MB',
+      uploadedBy: '张明远',
+      uploadedAt: '2026-09-16'
+    },
+    {
+      name: '项目实施方案.docx',
+      kind: 'word',
+      size: '856 KB',
+      uploadedBy: '张明远',
+      uploadedAt: '2026-09-16'
+    },
+    {
+      name: '项目预算明细.xlsx',
+      kind: 'excel',
+      size: '128 KB',
+      uploadedBy: '张明远',
+      uploadedAt: '2026-09-16'
+    }
+  ],
+  process: [
+    { name: '项目负责人提交', handler: '张明远', date: '2026-09-16', state: 'done' },
+    { name: '科技发展部预审', handler: '当前节点', date: '', state: 'current' }
+  ],
+  ...data
+})
+const projects = ref<Project[]>([
+  createProject(),
+  createProject({
+    id: 2,
+    projectNo: 'XM202603002',
+    projectName: '智能钻井参数优化方法研究',
+    projectType: '基础研究类',
+    projectLeader: '赵海宁',
+    projectLevel: '集团公司级',
+    budgetTotal: 420000,
+    status: '待预审',
+    submitDate: '2026-09-15',
+    department: '技术中心'
+  }),
+  createProject({
+    id: 3,
+    projectNo: 'XM202602018',
+    projectName: '油气田节能降碳技术应用研究',
+    projectType: '成熟工艺技术配套及推广类',
+    projectLeader: '陈立',
+    status: '预审通过',
+    submitDate: '2026-09-10'
+  })
+])
+const queryParams = reactive({ projectName: '', projectType: '', submitDate: '' })
+const activeStatus = ref('all')
+const selectedProject = ref<Project>(projects.value[0])
+const activeTab = ref('info')
+const reviewComment = ref('')
+const filteredProjects = computed(() =>
+  projects.value.filter(
+    (item) =>
+      (!queryParams.projectName || item.projectName.includes(queryParams.projectName)) &&
+      (!queryParams.projectType || item.projectType === queryParams.projectType) &&
+      (!queryParams.submitDate || item.submitDate === queryParams.submitDate) &&
+      (activeStatus.value === 'all' || item.status === activeStatus.value)
+  )
+)
+const statusCount = (status: string) =>
+  status === 'all'
+    ? projects.value.length
+    : projects.value.filter((item) => item.status === status).length
+const statusType = (status: Status) =>
+  status === '待预审' ? 'warning' : status === '预审通过' ? 'success' : 'danger'
+const selectProject = (project: Project) => {
+  selectedProject.value = project
+  reviewComment.value = ''
+  activeTab.value = 'info'
+}
+const resetQuery = () => {
+  queryParams.projectName = ''
+  queryParams.projectType = ''
+  queryParams.submitDate = ''
+  activeStatus.value = 'all'
+}
+const handleQuery = () => {
+  if (!filteredProjects.value.includes(selectedProject.value))
+    selectedProject.value = filteredProjects.value[0]
+}
+const formatAmount = (amount: number) =>
+  amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+const fileIcon = (kind: Attachment['kind']) =>
+  kind === 'pdf' ? 'ep:document' : kind === 'word' ? 'ep:reading' : 'ep:files'
+const previewFile = (file: Attachment) => {
+  if (file.url) window.open(file.url, '_blank')
+  else ElMessage.info(`前端演示:${file.name}暂无在线文件地址`)
+}
+const downloadFile = (file: Attachment) => {
+  if (file.url) {
+    const link = document.createElement('a')
+    link.href = file.url
+    link.download = file.name
+    link.click()
+  } else ElMessage.info(`前端演示:${file.name}暂无下载地址`)
+}
+const submitReview = async (result: '预审通过' | '退回修改') => {
+  if (result === '退回修改' && !reviewComment.value.trim()) {
+    ElMessage.warning('退回修改时必须填写预审意见')
+    return
+  }
+  const confirmed = await ElMessageBox.confirm(`确定${result}该项目吗?`, '提交预审结果', {
+    type: result === '预审通过' ? 'success' : 'warning',
+    confirmButtonText: '确定',
+    cancelButtonText: '取消'
+  })
+    .then(() => true)
+    .catch(() => false)
+  if (!confirmed || !selectedProject.value) return
+  selectedProject.value.status = result
+  selectedProject.value.process = selectedProject.value.process.map((step) => ({
+    ...step,
+    state: 'done',
+    date: step.date || '2026-09-18'
+  }))
+  ElMessage.success(`${result}成功`)
+}
+</script>
+
+<style scoped lang="scss">
+.review-page {
+  min-height: 100%;
+  color: var(--el-text-color-primary);
+}
+.review-filter {
+  margin-bottom: 16px;
+  background-color: #fff;
+  display: flex;
+  padding: 20px;
+  padding-bottom: 0px;
+  align-items: center;
+}
+.review-layout {
+  display: grid;
+  grid-template-columns: 360px minmax(0, 1fr);
+  gap: 16px;
+  align-items: start;
+}
+.project-list-panel,
+.review-detail-panel {
+  min-height: 680px;
+}
+.panel-heading,
+.detail-heading,
+.project-item-top,
+.project-item-bottom,
+.action-buttons {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.panel-heading h2,
+.detail-heading h1 {
+  margin: 0;
+}
+.panel-heading p,
+.detail-heading p {
+  margin: 7px 0 0;
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+.status-tabs {
+  display: flex;
+  gap: 4px;
+  margin: 0px -8px 8px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+.status-tabs button {
+  padding: 10px 9px;
+  border: 0;
+  border-bottom: 2px solid transparent;
+  background: transparent;
+  color: var(--el-text-color-secondary);
+  cursor: pointer;
+  font-size: 13px;
+}
+.status-tabs button.active {
+  border-bottom-color: var(--el-color-primary);
+  color: var(--el-color-primary);
+}
+.status-tabs span {
+  margin-left: 3px;
+  font-size: 12px;
+}
+.project-list {
+  display: grid;
+  gap: 8px;
+}
+.project-item {
+  padding: 14px;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+  background: var(--el-bg-color);
+  text-align: left;
+  cursor: pointer;
+  transition:
+    border-color 0.2s,
+    background 0.2s;
+}
+.project-item:hover,
+.project-item.selected {
+  border-color: var(--el-color-primary-light-5);
+  background: var(--el-color-primary-light-9);
+}
+.project-item strong {
+  max-width: 215px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.project-meta,
+.project-item-bottom,
+.file-info span,
+.process-step span {
+  color: var(--el-text-color-secondary);
+  font-size: 12px;
+}
+.project-meta {
+  margin: 10px 0;
+}
+.project-item-bottom {
+  font-size: 12px;
+}
+.eyebrow {
+  margin-bottom: 8px;
+  color: var(--el-color-primary);
+  font-size: 12px;
+}
+.detail-heading h1 {
+  font-size: 22px;
+}
+.process-strip {
+  display: flex;
+  align-items: flex-start;
+  margin: 10px 0 22px;
+  padding: 18px 20px;
+  background: var(--el-fill-color-light);
+}
+.process-step {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  gap: 8px;
+  position: relative;
+  color: var(--el-text-color-secondary);
+}
+.process-step > div:not(.step-icon) {
+  display: grid;
+  gap: 4px;
+}
+.process-step.done,
+.process-step.current {
+  color: var(--el-text-color-primary);
+}
+.step-icon {
+  display: grid;
+  width: 28px;
+  height: 28px;
+  place-items: center;
+  border-radius: 50%;
+  background: var(--el-border-color);
+  color: white;
+}
+.process-step.done .step-icon {
+  background: var(--el-color-success);
+}
+.process-step.current .step-icon {
+  background: var(--el-color-primary);
+}
+.process-step i {
+  flex: 1;
+  height: 1px;
+  margin: 0 14px;
+  background: var(--el-border-color);
+}
+.detail-tabs {
+  min-height: 330px;
+}
+.info-table {
+  margin-top: 8px;
+}
+.budget-summary {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+  margin: 8px 0 18px;
+}
+.budget-summary div {
+  padding: 16px;
+  border: 1px solid var(--el-border-color-lighter);
+}
+.budget-summary span,
+.budget-summary strong {
+  display: block;
+}
+.budget-summary span {
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+.budget-summary strong {
+  margin-top: 8px;
+  font-size: 20px;
+}
+.success-text {
+  color: var(--el-color-success);
+}
+.attachment-tip {
+  padding: 10px 12px;
+  background: var(--el-color-primary-light-9);
+  color: var(--el-color-primary);
+  font-size: 13px;
+}
+.attachment-list {
+  margin-top: 14px;
+}
+.attachment-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 14px 4px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+.file-icon {
+  display: grid;
+  width: 38px;
+  height: 38px;
+  place-items: center;
+  border-radius: 4px;
+  font-size: 20px;
+}
+.file-icon.pdf {
+  background: #fef0f0;
+  color: #f56c6c;
+}
+.file-icon.word {
+  background: #ecf5ff;
+  color: #409eff;
+}
+.file-icon.excel {
+  background: #f0f9eb;
+  color: #67c23a;
+}
+.file-info {
+  display: grid;
+  flex: 1;
+  gap: 5px;
+  min-width: 0;
+}
+.file-info strong {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.review-action {
+  margin-top: 16px;
+  padding-top: 18px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+.review-action > div:first-child {
+  display: grid;
+  gap: 5px;
+  margin-bottom: 10px;
+}
+.review-action small {
+  color: var(--el-text-color-secondary);
+}
+.action-buttons {
+  justify-content: flex-end;
+  margin-top: 12px;
+}
+.empty-panel {
+  display: grid;
+  place-items: center;
+}
+@media (max-width: 1100px) {
+  .review-layout {
+    grid-template-columns: 300px minmax(0, 1fr);
+  }
+  .process-strip {
+    overflow-x: auto;
+    min-width: 620px;
+  }
+}
+@media (max-width: 800px) {
+  .review-layout {
+    grid-template-columns: 1fr;
+  }
+  .project-list-panel,
+  .review-detail-panel {
+    min-height: auto;
+  }
+  .budget-summary {
+    grid-template-columns: 1fr;
+  }
+  .detail-heading {
+    align-items: flex-start;
+    gap: 12px;
+  }
+  .detail-heading h1 {
+    font-size: 18px;
+  }
+}
+</style>