|
@@ -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>
|