yanghao 2 dagen geleden
bovenliggende
commit
dc573f227d
1 gewijzigde bestanden met toevoegingen van 849 en 0 verwijderingen
  1. 849 0
      src/views/researchProject/approval/index.vue

+ 849 - 0
src/views/researchProject/approval/index.vue

@@ -0,0 +1,849 @@
+<template>
+  <div class="approval-page">
+    <div class="approval-filter">
+      <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.status"
+            clearable
+            placeholder="请选择审批状态"
+            class="!w-210px"
+            ><el-option
+              v-for="item in statusTabs.slice(1)"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value" /></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="approval-layout">
+      <ContentWrap class="approval-list" style="border: none">
+        <div class="list-title"
+          ><div><h2>立项审批</h2><p>按流程完成项目立项汇报及审批</p></div
+          ><el-tag type="warning">待处理 {{ pendingCount }}</el-tag></div
+        >
+        <div class="status-tabs"
+          ><button
+            v-for="item in statusTabs"
+            :key="item.value"
+            :class="{ active: activeStatus === item.value }"
+            @click="changeStatus(item.value)"
+            >{{ item.label }} <span>{{ statusCount(item.value) }}</span></button
+          ></div
+        >
+        <div class="project-list">
+          <button
+            v-for="project in pagedProjects"
+            :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.projectLeader }} · {{ project.executingUnit }}</div
+            >
+            <div class="project-item-bottom"
+              ><span>{{ project.projectNo }}</span
+              ><span>提交于 {{ project.submitDate }}</span></div
+            >
+          </button>
+          <el-empty v-if="!filteredProjects.length" description="暂无审批项目" :image-size="72" />
+        </div>
+        <Pagination
+          v-if="filteredProjects.length"
+          v-model:page="pagination.pageNo"
+          v-model:limit="pagination.pageSize"
+          :total="filteredProjects.length"
+          :page-sizes="[5, 10, 20]"
+          layout="total, prev, pager, next"
+          @pagination="handlePagination"
+        />
+      </ContentWrap>
+
+      <ContentWrap v-if="selectedProject" class="approval-detail" style="border: none">
+        <div class="detail-heading"
+          ><div
+            ><div class="eyebrow">立项审批 · {{ selectedProject.projectNo }}</div
+            ><h1>{{ selectedProject.projectName }}</h1
+            ><p
+              >{{ selectedProject.projectType }} · {{ selectedProject.projectLeader }} ·
+              {{ selectedProject.executingUnit }}</p
+            ></div
+          ><el-tag :type="statusType(selectedProject.status)" size="large">{{
+            selectedProject.status
+          }}</el-tag></div
+        >
+        <div class="workflow"
+          ><div
+            v-for="(step, index) in selectedProject.process"
+            :key="step.name"
+            class="workflow-step"
+            :class="step.state"
+            ><div class="node"
+              ><Icon
+                :icon="
+                  step.state === 'done'
+                    ? 'ep:check'
+                    : step.state === 'current'
+                      ? 'ep:clock'
+                      : 'ep:more'
+                " /></div
+            ><div class="node-text"
+              ><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">
+          <el-tab-pane label="立项汇报" name="report"
+            ><el-descriptions :column="2" border
+              ><el-descriptions-item label="汇报主题" :span="2">{{
+                selectedProject.reportTitle
+              }}</el-descriptions-item
+              ><el-descriptions-item label="汇报人">{{
+                selectedProject.projectLeader
+              }}</el-descriptions-item
+              ><el-descriptions-item label="汇报时间">{{
+                selectedProject.reportDate
+              }}</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="info"
+            ><el-descriptions :column="2" border
+              ><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.projectLeader
+              }}</el-descriptions-item
+              ><el-descriptions-item label="执行单位">{{
+                selectedProject.executingUnit
+              }}</el-descriptions-item
+              ><el-descriptions-item label="项目周期">{{
+                selectedProject.period
+              }}</el-descriptions-item
+              ><el-descriptions-item label="申报部门">{{
+                selectedProject.department
+              }}</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 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-tab-pane label="审批记录" name="history"
+            ><el-timeline
+              ><el-timeline-item
+                v-for="record in selectedProject.records"
+                :key="record.id"
+                :timestamp="record.date"
+                :type="record.type"
+                ><strong>{{ record.action }}</strong
+                ><p
+                  >{{ record.operator }}:{{ record.comment || '暂无审批意见' }}</p
+                ></el-timeline-item
+              ></el-timeline
+            ></el-tab-pane
+          >
+        </el-tabs>
+
+        <div class="approval-action" v-if="isCurrentApproval"
+          ><div
+            ><strong>审批意见</strong
+            ><small>请结合立项汇报、项目资料、预算和附件进行审批</small></div
+          ><el-input
+            v-model="approvalComment"
+            type="textarea"
+            :rows="3"
+            maxlength="500"
+            show-word-limit
+            :placeholder="`请输入${selectedProject.currentNode}意见,退回时必须填写`"
+          /><div class="action-buttons"
+            ><el-button type="danger" plain @click="submitApproval('退回修改')"
+              ><Icon icon="ep:back" class="mr-5px" />退回修改</el-button
+            ><el-button type="primary" @click="submitApproval('审批通过')"
+              ><Icon icon="ep:circle-check" class="mr-5px" />{{
+                selectedProject.currentNode
+              }}通过</el-button
+            ></div
+          ></div
+        >
+        <el-alert
+          v-else
+          type="success"
+          :closable="false"
+          title="当前项目已完成本节点审批"
+          class="finished-alert"
+        />
+      </ContentWrap>
+      <ContentWrap v-else class="approval-detail 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: 'ResearchProjectApproval' })
+type Status = '待专业公司审批' | '待科技发展部审批' | '已立项' | '退回修改'
+type FileKind = 'pdf' | 'word' | 'excel'
+type Attachment = {
+  name: string
+  kind: FileKind
+  size: string
+  uploadedBy: string
+  uploadedAt: string
+  url?: string
+}
+type ProcessStep = {
+  name: string
+  handler: string
+  date: string
+  state: 'done' | 'current' | 'pending'
+}
+type RecordItem = {
+  id: number
+  action: string
+  operator: string
+  date: string
+  comment: string
+  type: 'success' | 'warning' | 'danger' | 'primary'
+}
+type Project = {
+  id: number
+  projectNo: string
+  projectName: string
+  projectType: string
+  projectLevel: string
+  projectLeader: string
+  executingUnit: string
+  department: string
+  period: string
+  purpose: string
+  content: string
+  reportTitle: string
+  reportDate: string
+  submitDate: string
+  status: Status
+  currentNode: '专业公司审批' | '科技发展部审批'
+  budgetTotal: number
+  budgets: { subject: string; amount: number; remark: string }[]
+  attachments: Attachment[]
+  process: ProcessStep[]
+  records: RecordItem[]
+}
+const statusTabs = [
+  { label: '全部', value: 'all' },
+  { label: '待专业公司审批', value: '待专业公司审批' },
+  { label: '待科技发展部审批', value: '待科技发展部审批' },
+  { label: '已立项', value: '已立项' },
+  { label: '已退回', value: '退回修改' }
+]
+const baseFiles: Attachment[] = [
+  {
+    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'
+  }
+]
+const createProject = (data: Partial<Project> = {}): Project => ({
+  id: 1,
+  projectNo: 'XM202603001',
+  projectName: '高温高压井下工具可靠性研究',
+  projectType: '产品、工具和设备研发类',
+  projectLevel: '专业公司级/石油技术公司级',
+  projectLeader: '张明远',
+  executingUnit: '瑞恒兴仪 / 四川瑞都',
+  department: '科技发展部',
+  period: '2026-03 至 2027-03',
+  purpose: '提升复杂工况下工具运行可靠性,形成可推广的井下工具评价方法。',
+  content: '开展材料、结构及工艺适应性研究,完成样机试制、环境测试和现场验证。',
+  reportTitle: '高温高压井下工具可靠性研究立项汇报',
+  reportDate: '2026-09-16',
+  submitDate: '2026-09-16',
+  status: '待专业公司审批',
+  currentNode: '专业公司审批',
+  budgetTotal: 275000,
+  budgets: [
+    { subject: '直接材料投入', amount: 180000, remark: '样机材料及加工费用' },
+    { subject: '检验费', amount: 65000, remark: '高温高压环境测试' },
+    { subject: '差旅费', amount: 30000, remark: '现场调研与试验支持' }
+  ],
+  attachments: baseFiles,
+  process: [
+    { name: '立项汇报', handler: '张明远', date: '2026-09-16', state: 'done' },
+    { name: '项目负责人提交', handler: '张明远', date: '2026-09-16', state: 'done' },
+    { name: '专业公司审批', handler: '当前节点', date: '', state: 'current' },
+    { name: '科技发展部审批', handler: '待处理', date: '', state: 'pending' }
+  ],
+  records: [
+    {
+      id: 1,
+      action: '项目负责人提交立项',
+      operator: '张明远',
+      date: '2026-09-16 14:30',
+      comment: '申请立项,请审批。',
+      type: 'primary'
+    }
+  ],
+  ...data
+})
+const projects = ref<Project[]>([
+  createProject(),
+  createProject({
+    id: 2,
+    projectNo: 'XM202603002',
+    projectName: '智能钻井参数优化方法研究',
+    projectType: '基础研究类',
+    projectLevel: '集团公司级',
+    projectLeader: '赵海宁',
+    department: '技术中心',
+    status: '待科技发展部审批',
+    currentNode: '科技发展部审批',
+    submitDate: '2026-09-15',
+    process: [
+      { name: '立项汇报', handler: '赵海宁', date: '2026-09-12', state: 'done' },
+      { name: '项目负责人提交', handler: '赵海宁', date: '2026-09-15', state: 'done' },
+      { name: '专业公司审批', handler: '专业公司', date: '2026-09-16', state: 'done' },
+      { name: '科技发展部审批', handler: '当前节点', date: '', state: 'current' }
+    ]
+  }),
+  createProject({
+    id: 3,
+    projectNo: 'XM202602018',
+    projectName: '油气田节能降碳技术应用研究',
+    projectLeader: '陈立',
+    status: '已立项',
+    currentNode: '科技发展部审批',
+    submitDate: '2026-09-10',
+    process: [
+      { name: '立项汇报', handler: '陈立', date: '2026-09-01', state: 'done' },
+      { name: '项目负责人提交', handler: '陈立', date: '2026-09-05', state: 'done' },
+      { name: '专业公司审批', handler: '专业公司', date: '2026-09-07', state: 'done' },
+      { name: '科技发展部审批', handler: '科技发展部', date: '2026-09-10', state: 'done' }
+    ]
+  })
+])
+const queryParams = reactive({ projectName: '', status: '', submitDate: '' })
+const pagination = reactive({ pageNo: 1, pageSize: 5 })
+const activeStatus = ref('all')
+const selectedProject = ref<Project>(projects.value[0])
+const activeTab = ref('report')
+const approvalComment = ref('')
+const filteredProjects = computed(() =>
+  projects.value.filter(
+    (item) =>
+      (!queryParams.projectName || item.projectName.includes(queryParams.projectName)) &&
+      (!queryParams.status || item.status === queryParams.status) &&
+      (!queryParams.submitDate || item.submitDate === queryParams.submitDate) &&
+      (activeStatus.value === 'all' || item.status === activeStatus.value)
+  )
+)
+const pagedProjects = computed(() =>
+  filteredProjects.value.slice(
+    (pagination.pageNo - 1) * pagination.pageSize,
+    pagination.pageNo * pagination.pageSize
+  )
+)
+const pendingCount = computed(
+  () =>
+    projects.value.filter(
+      (item) => item.status === '待专业公司审批' || item.status === '待科技发展部审批'
+    ).length
+)
+const isCurrentApproval = computed(
+  () =>
+    selectedProject.value &&
+    (selectedProject.value.status === '待专业公司审批' ||
+      selectedProject.value.status === '待科技发展部审批')
+)
+const statusCount = (status: string) =>
+  status === 'all'
+    ? projects.value.length
+    : projects.value.filter((item) => item.status === status).length
+const statusType = (status: Status) =>
+  status === '已立项' ? 'success' : status === '退回修改' ? 'danger' : 'warning'
+const changeStatus = (status: string) => {
+  activeStatus.value = status
+  pagination.pageNo = 1
+  syncSelected()
+}
+const selectProject = (project: Project) => {
+  selectedProject.value = project
+  activeTab.value = 'report'
+  approvalComment.value = ''
+}
+const syncSelected = () => {
+  if (!filteredProjects.value.includes(selectedProject.value))
+    selectedProject.value = pagedProjects.value[0]
+}
+const handleQuery = () => {
+  pagination.pageNo = 1
+  syncSelected()
+}
+const resetQuery = () => {
+  queryParams.projectName = ''
+  queryParams.status = ''
+  queryParams.submitDate = ''
+  activeStatus.value = 'all'
+  pagination.pageNo = 1
+  syncSelected()
+}
+const handlePagination = () => {
+  const maxPage = Math.max(1, Math.ceil(filteredProjects.value.length / pagination.pageSize))
+  if (pagination.pageNo > maxPage) pagination.pageNo = maxPage
+  syncSelected()
+}
+const formatAmount = (amount: number) =>
+  amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+const fileIcon = (kind: FileKind) =>
+  kind === 'pdf' ? 'ep:document' : kind === 'word' ? 'ep:reading' : 'ep:files'
+const previewFile = (file: Attachment) =>
+  file.url
+    ? window.open(file.url, '_blank')
+    : ElMessage.info(`前端演示:${file.name}暂无在线文件地址`)
+const downloadFile = (file: Attachment) => {
+  if (!file.url) {
+    ElMessage.info(`前端演示:${file.name}暂无下载地址`)
+    return
+  }
+  const link = document.createElement('a')
+  link.href = file.url
+  link.download = file.name
+  link.click()
+}
+const submitApproval = async (result: '审批通过' | '退回修改') => {
+  if (result === '退回修改' && !approvalComment.value.trim()) {
+    ElMessage.warning('退回修改时必须填写审批意见')
+    return
+  }
+  const confirmed = await ElMessageBox.confirm(`确定${result}该项目吗?`, '提交审批结果', {
+    type: result === '审批通过' ? 'success' : 'warning',
+    confirmButtonText: '确定',
+    cancelButtonText: '取消'
+  })
+    .then(() => true)
+    .catch(() => false)
+  if (!confirmed) return
+  const project = selectedProject.value
+  if (result === '退回修改') {
+    project.status = result
+    project.records.unshift({
+      id: Date.now(),
+      action: `${project.currentNode}退回修改`,
+      operator: project.currentNode,
+      date: '2026-09-18',
+      comment: approvalComment.value,
+      type: 'danger'
+    })
+  } else if (project.status === '待专业公司审批') {
+    project.status = '待科技发展部审批'
+    project.currentNode = '科技发展部审批'
+    project.process = project.process.map((step) =>
+      step.name === '专业公司审批'
+        ? { ...step, state: 'done', handler: '专业公司', date: '2026-09-18' }
+        : step.name === '科技发展部审批'
+          ? { ...step, state: 'current', handler: '当前节点' }
+          : step
+    )
+    project.records.unshift({
+      id: Date.now(),
+      action: '专业公司审批通过',
+      operator: '专业公司',
+      date: '2026-09-18',
+      comment: approvalComment.value,
+      type: 'success'
+    })
+  } else {
+    project.status = '已立项'
+    project.process = project.process.map((step) => ({
+      ...step,
+      state: 'done',
+      handler: step.name === '科技发展部审批' ? '科技发展部' : step.handler,
+      date: step.date || '2026-09-18'
+    }))
+    project.records.unshift({
+      id: Date.now(),
+      action: '科技发展部审批通过,项目已立项',
+      operator: '科技发展部',
+      date: '2026-09-18',
+      comment: approvalComment.value,
+      type: 'success'
+    })
+  }
+  approvalComment.value = ''
+  ElMessage.success(`${result}成功`)
+}
+</script>
+
+<style scoped lang="scss">
+.approval-page {
+  min-height: 100%;
+  color: var(--el-text-color-primary);
+}
+.approval-filter {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 20px 20px 0;
+  background: #fff;
+}
+.approval-layout {
+  display: grid;
+  grid-template-columns: 360px minmax(0, 1fr);
+  gap: 16px;
+  align-items: start;
+}
+.approval-list,
+.approval-detail {
+  min-height: 720px;
+}
+.list-title,
+.detail-heading,
+.project-item-top,
+.project-item-bottom,
+.action-buttons {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.list-title h2,
+.detail-heading h1 {
+  margin: 0;
+}
+.list-title p,
+.detail-heading p {
+  margin: 7px 0 0;
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+.status-tabs {
+  display: flex;
+  gap: 3px;
+  margin: 22px -8px 8px;
+  overflow-x: auto;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+.status-tabs button {
+  flex: 0 0 auto;
+  padding: 10px 8px;
+  border: 0;
+  border-bottom: 2px solid transparent;
+  background: transparent;
+  color: var(--el-text-color-secondary);
+  cursor: pointer;
+  font-size: 12px;
+}
+.status-tabs button.active {
+  border-bottom-color: var(--el-color-primary);
+  color: var(--el-color-primary);
+}
+.status-tabs span {
+  margin-left: 3px;
+}
+.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;
+}
+.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,
+.workflow-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;
+}
+.workflow {
+  display: flex;
+  align-items: flex-start;
+  margin: 28px 0 22px;
+  padding: 18px 20px;
+  background: var(--el-fill-color-light);
+}
+.workflow-step {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  gap: 8px;
+  color: var(--el-text-color-secondary);
+}
+.workflow-step.current,
+.workflow-step.done {
+  color: var(--el-text-color-primary);
+}
+.node {
+  display: grid;
+  width: 28px;
+  height: 28px;
+  flex: 0 0 28px;
+  place-items: center;
+  border-radius: 50%;
+  background: var(--el-border-color);
+  color: #fff;
+}
+.workflow-step.done .node {
+  background: var(--el-color-success);
+}
+.workflow-step.current .node {
+  background: var(--el-color-primary);
+}
+.node-text {
+  display: grid;
+  gap: 4px;
+}
+.workflow-step i {
+  flex: 1;
+  height: 1px;
+  margin: 0 10px;
+  background: var(--el-border-color);
+}
+.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;
+}
+.approval-action {
+  margin-top: 18px;
+  padding-top: 18px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+.approval-action > div:first-child {
+  display: grid;
+  gap: 5px;
+  margin-bottom: 10px;
+}
+.approval-action small {
+  color: var(--el-text-color-secondary);
+}
+.action-buttons {
+  justify-content: flex-end;
+  margin-top: 12px;
+}
+.finished-alert {
+  margin-top: 18px;
+}
+.empty-panel {
+  display: grid;
+  place-items: center;
+}
+@media (max-width: 1100px) {
+  .approval-layout {
+    grid-template-columns: 300px minmax(0, 1fr);
+  }
+  .workflow {
+    min-width: 720px;
+    overflow-x: auto;
+  }
+}
+@media (max-width: 800px) {
+  .approval-layout {
+    grid-template-columns: 1fr;
+  }
+  .approval-list,
+  .approval-detail {
+    min-height: auto;
+  }
+  .budget-summary {
+    grid-template-columns: 1fr;
+  }
+  .detail-heading h1 {
+    font-size: 18px;
+  }
+}
+</style>