|
|
@@ -0,0 +1,678 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <!-- 搜索区域 -->
|
|
|
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
|
|
|
+ <el-form-item label="项目名称" prop="projectName">
|
|
|
+ <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="项目类型" prop="projectType">
|
|
|
+ <el-select v-model="queryParams.projectType" placeholder="项目类型" clearable style="width: 200px">
|
|
|
+ <el-option v-for="dict in projectTypeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="项目状态" prop="status">
|
|
|
+ <el-select v-model="queryParams.status" placeholder="项目状态" clearable style="width: 200px">
|
|
|
+ <el-option v-for="dict in projectStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="牵头部门" prop="deptId">
|
|
|
+ <DeptSelector v-model="queryParams.deptId" :useTree="true" :treeData="deptOptions" placeholder="请选择部门" width="200px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
+ <el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['performance:project:add']">新增</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate(null)" v-hasPermi="['performance:project:edit']">修改</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete(null)" v-hasPermi="['performance:project:remove']">删除</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['performance:project:import']">导入</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:project:export']">导出</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="default" plain icon="DataLine" @click="handleStats" v-hasPermi="['performance:project:stats']">统计</el-button>
|
|
|
+ </el-col>
|
|
|
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns" @toggleView="handleToggleView">
|
|
|
+ <template #viewToggle>
|
|
|
+ <el-tooltip :content="viewMode === 'table' ? '切换卡片视图' : '切换表格视图'" placement="top">
|
|
|
+ <el-button link @click="handleToggleView">
|
|
|
+ <svg-icon :icon-class="viewMode === 'table' ? 'component' : 'list'" />
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </right-toolbar>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="16" class="mb8" v-if="showStats">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="hover" class="stats-card">
|
|
|
+ <div class="stats-card__value">{{ stats.totalProjects }}</div>
|
|
|
+ <div class="stats-card__label">项目总数</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="hover" class="stats-card stats-card--blue">
|
|
|
+ <div class="stats-card__value">{{ stats.totalEstimatedOutput }}</div>
|
|
|
+ <div class="stats-card__label">总预估产值(万元)</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="hover" class="stats-card stats-card--green">
|
|
|
+ <div class="stats-card__value">{{ stats.totalActualOutput }}</div>
|
|
|
+ <div class="stats-card__label">总实际产值(万元)</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="hover" class="stats-card stats-card--orange">
|
|
|
+ <div class="stats-card__value">{{ stats.completedProjects }}</div>
|
|
|
+ <div class="stats-card__label">完工项目数</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 表格视图 -->
|
|
|
+ <el-table v-if="viewMode === 'table'" v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
|
|
|
+ <el-table-column type="selection" width="50" align="center" />
|
|
|
+ <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ <a class="link-type" style="cursor: pointer" @click="handleDetail(scope.row)">{{ scope.row.projectName }}</a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="项目类型" align="center" prop="projectType" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ typeMap[scope.row.projectType] || scope.row.projectType }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="牵头部门" align="center" prop="deptName" width="140" />
|
|
|
+ <el-table-column label="协作部门" align="center" prop="coopDeptName" width="140">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.coopDeptName || '无' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="合同额(万元)" align="center" prop="contractAmount" width="120" />
|
|
|
+ <el-table-column label="预估产值(万元)" align="center" prop="estimatedOutput" width="120" />
|
|
|
+ <el-table-column label="实际产值(万元)" align="center" prop="actualOutput" width="120" />
|
|
|
+ <el-table-column label="项目状态" align="center" width="160">
|
|
|
+ <template #default="scope">
|
|
|
+ <ReviewStatusTag :status="scope.row.status" :options="statusTagOptions" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" width="240" class-name="small-padding fixed-width">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
|
|
|
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-if="scope.row.status !== 'COMPLETED'" v-hasPermi="['performance:project:edit']">编辑</el-button>
|
|
|
+ <el-dropdown @command="(cmd) => handleStatusChange(scope.row, cmd)" v-if="getAvailableTransitions(scope.row.status).length > 0">
|
|
|
+ <el-button link type="warning" icon="Switch">
|
|
|
+ 状态<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item
|
|
|
+ v-for="t in getAvailableTransitions(scope.row.status)"
|
|
|
+ :key="t.value"
|
|
|
+ :command="t.value"
|
|
|
+ >{{ t.label }}</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-if="scope.row.status === 'PLANNING_NOT_STARTED'" v-hasPermi="['performance:project:remove']">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 卡片视图 -->
|
|
|
+ <div v-if="viewMode === 'card'" v-loading="loading" class="project-card-grid">
|
|
|
+ <ProjectCard
|
|
|
+ v-for="project in projectList"
|
|
|
+ :key="project.projectId"
|
|
|
+ :project="project"
|
|
|
+ :clickable="true"
|
|
|
+ :statusOptions="statusTagOptions"
|
|
|
+ :typeMap="typeMap"
|
|
|
+ @click="handleDetail"
|
|
|
+ >
|
|
|
+ <template #actions>
|
|
|
+ <el-button type="primary" size="small" @click.stop="handleUpdate(project)" v-hasPermi="['performance:project:edit']">编辑</el-button>
|
|
|
+ <el-dropdown @command="(cmd) => handleStatusChange(project, cmd)" v-if="getAvailableTransitions(project.status).length > 0">
|
|
|
+ <el-button type="warning" size="small" @click.stop>状态变更</el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item v-for="t in getAvailableTransitions(project.status)" :key="t.value" :command="t.value">{{ t.label }}</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </template>
|
|
|
+ </ProjectCard>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
|
|
+
|
|
|
+ <!-- 新增/编辑项目对话框 -->
|
|
|
+ <el-dialog :title="title" v-model="open" width="750px" append-to-body @close="cancel">
|
|
|
+ <el-form :model="form" :rules="rules" ref="projectRef" label-width="130px">
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="项目名称" prop="projectName">
|
|
|
+ <el-input v-model="form.projectName" placeholder="请输入项目名称" maxlength="100" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="项目类型" prop="projectType">
|
|
|
+ <el-select v-model="form.projectType" placeholder="请选择项目类型" style="width: 100%">
|
|
|
+ <el-option v-for="dict in projectTypeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="牵头部门" prop="leadDeptId">
|
|
|
+ <DeptSelector v-model="form.leadDeptId" :useTree="true" :treeData="deptOptions" placeholder="请选择牵头部门" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="协作部门" prop="coopDeptId">
|
|
|
+ <DeptSelector v-model="form.coopDeptId" :useTree="true" :treeData="deptOptions" placeholder="请选择协作部门(可选)" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <!-- 合同模式 -->
|
|
|
+ <el-divider content-position="left">产值信息</el-divider>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="合同额(万元)" prop="contractAmount">
|
|
|
+ <el-input-number v-model="form.contractAmount" :precision="4" :min="0" placeholder="已签合同项目请填写" style="width: 100%" @change="onContractChange" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="外协成本(万元)" prop="outsourceCost">
|
|
|
+ <el-input-number v-model="form.outsourceCost" :precision="4" :min="0" placeholder="请输入外协成本" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row v-if="hasContract">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="实际产值(万元)">
|
|
|
+ <span class="computed-output">{{ computedActualOutput }} 万元</span>
|
|
|
+ <div class="form-item__hint">实际产值 = 合同额 - 外协成本</div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <!-- 预估模式 -->
|
|
|
+ <el-row v-if="!hasContract">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="预估产值(万元)" prop="estimatedOutput">
|
|
|
+ <el-input-number v-model="form.estimatedOutput" :precision="4" :min="0" placeholder="未签合同项目请填写" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="风险等级" prop="riskLevel">
|
|
|
+ <el-select v-model="form.riskLevel" placeholder="请选择风险等级" style="width: 100%">
|
|
|
+ <el-option label="低风险" value="LOW" />
|
|
|
+ <el-option label="中风险" value="MEDIUM" />
|
|
|
+ <el-option label="高风险" value="HIGH" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row v-if="!hasContract">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="立项成功率(%)" prop="successRate">
|
|
|
+ <el-input-number v-model="form.successRate" :precision="1" :min="0" :max="100" placeholder="请输入立项成功率" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="风险预估产值(万元)">
|
|
|
+ <span class="computed-output">{{ computedRiskOutput }} 万元</span>
|
|
|
+ <div class="form-item__hint">风险预估产值 = 预估产值 * 成功率%</div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="项目状态" prop="status">
|
|
|
+ <el-select v-model="form.status" placeholder="请选择项目状态" style="width: 100%">
|
|
|
+ <el-option v-for="dict in editableStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-form-item label="备注" prop="remark">
|
|
|
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注(立项风险说明、前期成本说明等)" maxlength="500" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitForm">确 定</el-button>
|
|
|
+ <el-button @click="cancel">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 导入对话框 -->
|
|
|
+ <ExcelImportDialog ref="importRef" title="项目导入" :action="importAction" :templateAction="importTemplateAction" templateFileName="project_template" @success="getList" />
|
|
|
+
|
|
|
+ <!-- 统计对话框 -->
|
|
|
+ <el-dialog title="项目统计" v-model="statsOpen" width="900px" append-to-body>
|
|
|
+ <el-row :gutter="16" class="mb8">
|
|
|
+ <el-col :span="6" v-for="card in statsCards" :key="card.key">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="stats-dialog-card">
|
|
|
+ <div class="stats-dialog-card__value">{{ card.value }}</div>
|
|
|
+ <div class="stats-dialog-card__label">{{ card.label }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <PerformanceChart type="bar" :xData="statsChartData.deptNames" :seriesData="statsChartData.series" title="各部门项目数量与产值对比" yName="数量/万元" height="300" />
|
|
|
+ <PerformanceChart type="pie" :xData="statsChartData.statusNames" :seriesData="statsChartData.statusSeries" title="项目状态分布" height="300" />
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="PerformanceProject">
|
|
|
+import { listProject, getProject, addProject, updateProject, delProject, deptTreeSelect, changeProjectStatus, getProjectStats } from '@/api/performance/project'
|
|
|
+import { ProjectCard, ReviewStatusTag, PerformanceChart, DeptSelector } from '@/components/performance'
|
|
|
+import ExcelImportDialog from '@/components/ExcelImportDialog'
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance()
|
|
|
+const router = useRouter()
|
|
|
+
|
|
|
+// 状态选项(用于 ReviewStatusTag)
|
|
|
+const statusTagOptions = [
|
|
|
+ { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS', tagType: 'warning' },
|
|
|
+ { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED', tagType: 'info' },
|
|
|
+ { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS', tagType: '' },
|
|
|
+ { label: '完工', value: 'COMPLETED', tagType: 'success' },
|
|
|
+ { label: '作废', value: 'CANCELLED', tagType: 'danger' }
|
|
|
+]
|
|
|
+
|
|
|
+// 项目状态选项(用于下拉)
|
|
|
+const projectStatusOptions = [
|
|
|
+ { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS' },
|
|
|
+ { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED' },
|
|
|
+ { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS' },
|
|
|
+ { label: '完工', value: 'COMPLETED' },
|
|
|
+ { label: '作废', value: 'CANCELLED' }
|
|
|
+]
|
|
|
+
|
|
|
+// 可编辑状态(新增时可选)
|
|
|
+const editableStatusOptions = [
|
|
|
+ { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS' },
|
|
|
+ { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED' },
|
|
|
+ { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS' }
|
|
|
+]
|
|
|
+
|
|
|
+const projectTypeOptions = [
|
|
|
+ { label: '工程项目', value: 'ENGINEERING' },
|
|
|
+ { label: '技术服务', value: 'TECHNICAL_SERVICE' },
|
|
|
+ { label: '咨询服务', value: 'CONSULTING' },
|
|
|
+ { label: '其他', value: 'OTHER' }
|
|
|
+]
|
|
|
+
|
|
|
+const typeMap = {
|
|
|
+ 'ENGINEERING': '工程项目',
|
|
|
+ 'TECHNICAL_SERVICE': '技术服务',
|
|
|
+ 'CONSULTING': '咨询服务',
|
|
|
+ 'OTHER': '其他'
|
|
|
+}
|
|
|
+
|
|
|
+// 状态流转规则
|
|
|
+const statusTransitions = {
|
|
|
+ 'PLANNING_NOT_STARTED': [
|
|
|
+ { label: '转为同步开工', value: 'PLANNING_IN_PROGRESS' },
|
|
|
+ { label: '转为招投标在建', value: 'BIDDING_IN_PROGRESS' },
|
|
|
+ { label: '标记作废', value: 'CANCELLED' }
|
|
|
+ ],
|
|
|
+ 'PLANNING_IN_PROGRESS': [
|
|
|
+ { label: '转为招投标在建', value: 'BIDDING_IN_PROGRESS' },
|
|
|
+ { label: '标记作废', value: 'CANCELLED' }
|
|
|
+ ],
|
|
|
+ 'BIDDING_IN_PROGRESS': [
|
|
|
+ { label: '标记完工', value: 'COMPLETED' },
|
|
|
+ { label: '标记作废', value: 'CANCELLED' }
|
|
|
+ ],
|
|
|
+ 'COMPLETED': [],
|
|
|
+ 'CANCELLED': []
|
|
|
+}
|
|
|
+
|
|
|
+function getAvailableTransitions(status) {
|
|
|
+ return statusTransitions[status] || []
|
|
|
+}
|
|
|
+
|
|
|
+// 状态变量
|
|
|
+const projectList = ref([])
|
|
|
+const open = ref(false)
|
|
|
+const loading = ref(true)
|
|
|
+const showSearch = ref(true)
|
|
|
+const ids = ref([])
|
|
|
+const single = ref(true)
|
|
|
+const multiple = ref(true)
|
|
|
+const total = ref(0)
|
|
|
+const title = ref('')
|
|
|
+const deptOptions = ref([])
|
|
|
+const viewMode = ref('table')
|
|
|
+const showStats = ref(false)
|
|
|
+const statsOpen = ref(false)
|
|
|
+
|
|
|
+const columns = ref({
|
|
|
+ projectName: { label: '项目名称', visible: true },
|
|
|
+ projectType: { label: '项目类型', visible: true },
|
|
|
+ deptName: { label: '牵头部门', visible: true },
|
|
|
+ coopDeptName: { label: '协作部门', visible: true },
|
|
|
+ contractAmount: { label: '合同额', visible: true },
|
|
|
+ estimatedOutput: { label: '预估产值', visible: true },
|
|
|
+ actualOutput: { label: '实际产值', visible: true },
|
|
|
+ status: { label: '项目状态', visible: true }
|
|
|
+})
|
|
|
+
|
|
|
+const stats = reactive({
|
|
|
+ totalProjects: 0,
|
|
|
+ totalEstimatedOutput: '--',
|
|
|
+ totalActualOutput: '--',
|
|
|
+ completedProjects: 0
|
|
|
+})
|
|
|
+
|
|
|
+const statsChartData = reactive({
|
|
|
+ deptNames: [],
|
|
|
+ series: [{ name: '项目数', data: [] }, { name: '产值(万元)', data: [] }],
|
|
|
+ statusNames: [],
|
|
|
+ statusSeries: [{ name: '项目数', data: [] }]
|
|
|
+})
|
|
|
+
|
|
|
+const data = reactive({
|
|
|
+ form: {
|
|
|
+ projectId: undefined,
|
|
|
+ projectName: undefined,
|
|
|
+ projectType: undefined,
|
|
|
+ leadDeptId: undefined,
|
|
|
+ coopDeptId: undefined,
|
|
|
+ contractAmount: undefined,
|
|
|
+ outsourceCost: undefined,
|
|
|
+ estimatedOutput: undefined,
|
|
|
+ riskLevel: undefined,
|
|
|
+ successRate: undefined,
|
|
|
+ status: undefined,
|
|
|
+ remark: undefined
|
|
|
+ },
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ projectName: undefined,
|
|
|
+ projectType: undefined,
|
|
|
+ status: undefined,
|
|
|
+ deptId: undefined
|
|
|
+ },
|
|
|
+ rules: {
|
|
|
+ projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
|
|
|
+ leadDeptId: [{ required: true, message: '牵头部门不能为空', trigger: 'change' }],
|
|
|
+ projectType: [{ required: true, message: '项目类型不能为空', trigger: 'change' }],
|
|
|
+ status: [{ required: true, message: '项目状态不能为空', trigger: 'change' }]
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const { queryParams, form, rules } = toRefs(data)
|
|
|
+
|
|
|
+const hasContract = computed(() => (form.value.contractAmount || 0) > 0)
|
|
|
+const computedActualOutput = computed(() => {
|
|
|
+ const contract = form.value.contractAmount || 0
|
|
|
+ const cost = form.value.outsourceCost || 0
|
|
|
+ return Math.max(0, contract - cost).toFixed(4)
|
|
|
+})
|
|
|
+const computedRiskOutput = computed(() => {
|
|
|
+ const estimated = form.value.estimatedOutput || 0
|
|
|
+ const rate = (form.value.successRate || 0) / 100
|
|
|
+ return (estimated * rate).toFixed(4)
|
|
|
+})
|
|
|
+
|
|
|
+const importAction = '/performance/project/importData'
|
|
|
+const importTemplateAction = '/performance/project/importTemplate'
|
|
|
+
|
|
|
+const statsCards = computed(() => [
|
|
|
+ { key: 'total', label: '项目总数', value: stats.totalProjects },
|
|
|
+ { key: 'estimated', label: '总预估产值(万元)', value: stats.totalEstimatedOutput },
|
|
|
+ { key: 'actual', label: '总实际产值(万元)', value: stats.totalActualOutput },
|
|
|
+ { key: 'completed', label: '完工项目数', value: stats.completedProjects }
|
|
|
+])
|
|
|
+
|
|
|
+// 数据加载
|
|
|
+function getList() {
|
|
|
+ loading.value = true
|
|
|
+ listProject(queryParams.value).then(res => {
|
|
|
+ projectList.value = res.rows
|
|
|
+ total.value = res.total
|
|
|
+ loading.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function getDeptTree() {
|
|
|
+ deptTreeSelect().then(res => {
|
|
|
+ deptOptions.value = res.data
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function getStats() {
|
|
|
+ getProjectStats(queryParams.value).then(res => {
|
|
|
+ const d = res.data || {}
|
|
|
+ stats.totalProjects = d.totalProjects || 0
|
|
|
+ stats.totalEstimatedOutput = d.totalEstimatedOutput || '--'
|
|
|
+ stats.totalActualOutput = d.totalActualOutput || '--'
|
|
|
+ stats.completedProjects = d.completedProjects || 0
|
|
|
+ statsChartData.deptNames = d.deptNames || []
|
|
|
+ statsChartData.series = [
|
|
|
+ { name: '项目数', data: d.deptProjectCounts || [], color: '#409EFF' },
|
|
|
+ { name: '产值(万元)', data: d.deptOutputValues || [], color: '#67C23A' }
|
|
|
+ ]
|
|
|
+ statsChartData.statusNames = d.statusNames || []
|
|
|
+ statsChartData.statusSeries = [{ name: '项目数', data: d.statusCounts || [] }]
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索与重置
|
|
|
+function handleQuery() {
|
|
|
+ queryParams.value.pageNum = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function resetQuery() {
|
|
|
+ proxy.resetForm('queryRef')
|
|
|
+ handleQuery()
|
|
|
+}
|
|
|
+
|
|
|
+function handleToggleView() {
|
|
|
+ viewMode.value = viewMode.value === 'table' ? 'card' : 'table'
|
|
|
+}
|
|
|
+
|
|
|
+// 选择
|
|
|
+function handleSelectionChange(selection) {
|
|
|
+ ids.value = selection.map(item => item.projectId)
|
|
|
+ single.value = selection.length !== 1
|
|
|
+ multiple.value = !selection.length
|
|
|
+}
|
|
|
+
|
|
|
+// 表单操作
|
|
|
+function reset() {
|
|
|
+ form.value = {
|
|
|
+ projectId: undefined, projectName: undefined, projectType: undefined,
|
|
|
+ leadDeptId: undefined, coopDeptId: undefined,
|
|
|
+ contractAmount: undefined, outsourceCost: undefined,
|
|
|
+ estimatedOutput: undefined, riskLevel: undefined, successRate: undefined,
|
|
|
+ status: undefined, remark: undefined
|
|
|
+ }
|
|
|
+ proxy.resetForm('projectRef')
|
|
|
+}
|
|
|
+
|
|
|
+function cancel() {
|
|
|
+ open.value = false
|
|
|
+ reset()
|
|
|
+}
|
|
|
+
|
|
|
+function onContractChange(val) {
|
|
|
+ if (val > 0) {
|
|
|
+ form.value.estimatedOutput = undefined
|
|
|
+ form.value.riskLevel = undefined
|
|
|
+ form.value.successRate = undefined
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleAdd() {
|
|
|
+ reset()
|
|
|
+ open.value = true
|
|
|
+ title.value = '添加项目'
|
|
|
+}
|
|
|
+
|
|
|
+function handleUpdate(row) {
|
|
|
+ reset()
|
|
|
+ const projectId = row ? row.projectId : ids.value[0]
|
|
|
+ if (!projectId) {
|
|
|
+ proxy.$modal.msgWarning('请选择一个项目')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ getProject(projectId).then(res => {
|
|
|
+ form.value = res.data
|
|
|
+ open.value = true
|
|
|
+ title.value = '修改项目'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function handleDetail(row) {
|
|
|
+ router.push(`/performance/project-detail/index/${row.projectId}`)
|
|
|
+}
|
|
|
+
|
|
|
+function handleDelete(row) {
|
|
|
+ const projectIds = row ? row.projectId : ids.value.join(',')
|
|
|
+ if (!projectIds) {
|
|
|
+ proxy.$modal.msgWarning('请选择要删除的项目')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ proxy.$modal.confirm('是否确认删除该项目?已审核通过的项目不可删除。').then(() => {
|
|
|
+ return delProject(projectIds)
|
|
|
+ }).then(() => {
|
|
|
+ getList()
|
|
|
+ proxy.$modal.msgSuccess('删除成功')
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+function handleStatusChange(row, newStatus) {
|
|
|
+ const transition = getAvailableTransitions(row.status).find(t => t.value === newStatus)
|
|
|
+ const label = transition ? transition.label : newStatus
|
|
|
+ proxy.$modal.confirm(`确认将项目「${row.projectName}」的状态${label}?`).then(() => {
|
|
|
+ return changeProjectStatus(row.projectId, newStatus)
|
|
|
+ }).then(() => {
|
|
|
+ proxy.$modal.msgSuccess('状态变更成功')
|
|
|
+ getList()
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+function handleImport() {
|
|
|
+ proxy.$refs.importRef.open()
|
|
|
+}
|
|
|
+
|
|
|
+function handleExport() {
|
|
|
+ proxy.download('/performance/project/export', { ...queryParams.value }, `project_${new Date().getTime()}.xlsx`)
|
|
|
+}
|
|
|
+
|
|
|
+function handleStats() {
|
|
|
+ showStats.value = !showStats.value
|
|
|
+ if (showStats.value) {
|
|
|
+ getStats()
|
|
|
+ statsOpen.value = true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function submitForm() {
|
|
|
+ proxy.$refs.projectRef.validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ // 自动计算实际产值
|
|
|
+ if (hasContract.value) {
|
|
|
+ form.value.actualOutput = parseFloat(computedActualOutput.value)
|
|
|
+ }
|
|
|
+ if (form.value.projectId) {
|
|
|
+ updateProject(form.value).then(() => {
|
|
|
+ proxy.$modal.msgSuccess('修改成功')
|
|
|
+ open.value = false
|
|
|
+ getList()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ addProject(form.value).then(() => {
|
|
|
+ proxy.$modal.msgSuccess('新增成功')
|
|
|
+ open.value = false
|
|
|
+ getList()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getDeptTree()
|
|
|
+ getList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.project-card-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card {
|
|
|
+ text-align: center;
|
|
|
+ &__value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+ &__label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ &--blue .stats-card__value { color: #409EFF; }
|
|
|
+ &--green .stats-card__value { color: #67C23A; }
|
|
|
+ &--orange .stats-card__value { color: #E6A23C; }
|
|
|
+}
|
|
|
+
|
|
|
+.stats-dialog-card {
|
|
|
+ text-align: center;
|
|
|
+ &__value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409EFF;
|
|
|
+ }
|
|
|
+ &__label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.computed-output {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409EFF;
|
|
|
+}
|
|
|
+
|
|
|
+.form-item__hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+</style>
|