Jelajahi Sumber

feat: #11 前端框架搭建与基础配置

新建绩效管理业务模块基础框架,含API层+视图层+路由配置

- 新增5个API模块:project/output/review/calculate/message
- 新增8个业务页面:项目管理/产值填报/审核评定/绩效核算/消息通知
- 配置performanceRoutes动态路由,更新permission store
- 更新.env.development系统标题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shenzx 1 bulan lalu
induk
melakukan
5562b230f4

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 信息工程院日常绩效分配管理系统
 
 # 开发环境配置
 VITE_APP_ENV = 'development'

+ 54 - 0
src/api/performance/calculate.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询绩效核算列表
+export function listCalculate(query) {
+  return request({
+    url: '/performance/calculate/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询部门绩效核算详细
+export function getCalculate(deptId, month) {
+  return request({
+    url: '/performance/calculate/' + deptId + '/' + month,
+    method: 'get'
+  })
+}
+
+// 查询绩效汇总
+export function getCalculateSummary(query) {
+  return request({
+    url: '/performance/calculate/summary',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改绩效核算比例
+export function updateCalculateRatio(ratio) {
+  return request({
+    url: '/performance/calculate/ratio',
+    method: 'put',
+    data: { ratio }
+  })
+}
+
+// 导出绩效数据
+export function exportCalculate(query) {
+  return request({
+    url: '/performance/calculate/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询绩效统计图表数据
+export function getCalculateChart(query) {
+  return request({
+    url: '/performance/calculate/chart',
+    method: 'get',
+    params: query
+  })
+}

+ 59 - 0
src/api/performance/message.js

@@ -0,0 +1,59 @@
+import request from '@/utils/request'
+
+// 查询消息列表
+export function listMessage(query) {
+  return request({
+    url: '/performance/message/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 标记消息已读
+export function readMessage(messageId) {
+  return request({
+    url: '/performance/message/read/' + messageId,
+    method: 'put'
+  })
+}
+
+// 全部标记已读
+export function readAllMessage() {
+  return request({
+    url: '/performance/message/readAll',
+    method: 'put'
+  })
+}
+
+// 删除消息
+export function delMessage(messageId) {
+  return request({
+    url: '/performance/message/' + messageId,
+    method: 'delete'
+  })
+}
+
+// 查询未读消息数量
+export function getUnreadCount() {
+  return request({
+    url: '/performance/message/unreadCount',
+    method: 'get'
+  })
+}
+
+// 查询消息配置
+export function getMessageConfig() {
+  return request({
+    url: '/performance/message/config',
+    method: 'get'
+  })
+}
+
+// 更新消息配置
+export function updateMessageConfig(data) {
+  return request({
+    url: '/performance/message/config',
+    method: 'put',
+    data: data
+  })
+}

+ 88 - 0
src/api/performance/output.js

@@ -0,0 +1,88 @@
+import request from '@/utils/request'
+
+// 查询产值填报列表
+export function listOutput(query) {
+  return request({
+    url: '/performance/output/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询产值填报详细
+export function getOutput(outputId) {
+  return request({
+    url: '/performance/output/' + outputId,
+    method: 'get'
+  })
+}
+
+// 新增产值填报
+export function addOutput(data) {
+  return request({
+    url: '/performance/output',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改产值填报
+export function updateOutput(data) {
+  return request({
+    url: '/performance/output',
+    method: 'put',
+    data: data
+  })
+}
+
+// 提交产值填报
+export function submitOutput(outputId) {
+  return request({
+    url: '/performance/output/submit/' + outputId,
+    method: 'put'
+  })
+}
+
+// 批量填报
+export function batchOutput(data) {
+  return request({
+    url: '/performance/output/batch',
+    method: 'post',
+    data: data
+  })
+}
+
+// 查询协作申请列表
+export function listCooperation(query) {
+  return request({
+    url: '/performance/output/cooperation/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 发起协作申请
+export function addCooperation(data) {
+  return request({
+    url: '/performance/output/cooperation',
+    method: 'post',
+    data: data
+  })
+}
+
+// 确认协作申请
+export function confirmCooperation(cooperationId) {
+  return request({
+    url: '/performance/output/cooperation/confirm/' + cooperationId,
+    method: 'put'
+  })
+}
+
+// 导出产值数据
+export function exportOutput(query) {
+  return request({
+    url: '/performance/output/export',
+    method: 'get',
+    params: query
+  })
+}

+ 88 - 0
src/api/performance/project.js

@@ -0,0 +1,88 @@
+import request from '@/utils/request'
+
+// 查询项目列表
+export function listProject(query) {
+  return request({
+    url: '/performance/project/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询项目详细
+export function getProject(projectId) {
+  return request({
+    url: '/performance/project/' + projectId,
+    method: 'get'
+  })
+}
+
+// 新增项目
+export function addProject(data) {
+  return request({
+    url: '/performance/project',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改项目
+export function updateProject(data) {
+  return request({
+    url: '/performance/project',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除项目
+export function delProject(projectId) {
+  return request({
+    url: '/performance/project/' + projectId,
+    method: 'delete'
+  })
+}
+
+// 项目状态变更
+export function changeProjectStatus(projectId, status) {
+  return request({
+    url: '/performance/project/changeStatus',
+    method: 'put',
+    data: { projectId, status }
+  })
+}
+
+// 批量导入项目
+export function importProject(data) {
+  return request({
+    url: '/performance/project/import',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出项目
+export function exportProject(query) {
+  return request({
+    url: '/performance/project/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询项目统计
+export function getProjectStats(query) {
+  return request({
+    url: '/performance/project/stats',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询部门树
+export function deptTreeSelect() {
+  return request({
+    url: '/performance/project/deptTree',
+    method: 'get'
+  })
+}

+ 63 - 0
src/api/performance/review.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 查询审核列表
+export function listReview(query) {
+  return request({
+    url: '/performance/review/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询审核详细
+export function getReview(reviewId) {
+  return request({
+    url: '/performance/review/' + reviewId,
+    method: 'get'
+  })
+}
+
+// 审核通过
+export function approveReview(data) {
+  return request({
+    url: '/performance/review/approve',
+    method: 'put',
+    data: data
+  })
+}
+
+// 审核不通过
+export function rejectReview(data) {
+  return request({
+    url: '/performance/review/reject',
+    method: 'put',
+    data: data
+  })
+}
+
+// 批量审核
+export function batchReview(data) {
+  return request({
+    url: '/performance/review/batch',
+    method: 'put',
+    data: data
+  })
+}
+
+// 查询审核日志
+export function listReviewLog(query) {
+  return request({
+    url: '/performance/review/log/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出审核记录
+export function exportReview(query) {
+  return request({
+    url: '/performance/review/export',
+    method: 'get',
+    params: query
+  })
+}

+ 60 - 0
src/router/index.js

@@ -166,6 +166,66 @@ export const dynamicRoutes = [
   }
 ]
 
+// 绩效分配管理 - 业务子页面动态路由
+export const performanceRoutes = [
+  {
+    path: '/performance/project-detail',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:project:list'],
+    children: [
+      {
+        path: 'index/:projectId(\\d+)',
+        component: () => import('@/views/performance/project/detail'),
+        name: 'ProjectDetail',
+        meta: { title: '项目详情', activeMenu: '/performance/project' }
+      }
+    ]
+  },
+  {
+    path: '/performance/project-edit',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:project:edit'],
+    children: [
+      {
+        path: 'index/:projectId(\\d+)?',
+        component: () => import('@/views/performance/project/index'),
+        name: 'ProjectEdit',
+        meta: { title: '项目编辑', activeMenu: '/performance/project' }
+      }
+    ]
+  },
+  {
+    path: '/performance/output-cooperation',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:output:list'],
+    children: [
+      {
+        path: 'index/:outputId(\\d+)',
+        component: () => import('@/views/performance/output/cooperation'),
+        name: 'OutputCooperation',
+        meta: { title: '协作申请确认', activeMenu: '/performance/output' }
+      }
+    ]
+  },
+  {
+    path: '/performance/calculate-detail',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:calculate:list'],
+    children: [
+      {
+        path: 'index/:deptId(\\d+)/:month',
+        component: () => import('@/views/performance/calculate/detail'),
+        name: 'CalculateDetail',
+        meta: { title: '绩效核算详情', activeMenu: '/performance/calculate' }
+      }
+    ]
+  }
+]
+
 const router = createRouter({
   history: createWebHistory(),
   routes: constantRoutes,

+ 3 - 1
src/store/modules/permission.js

@@ -1,5 +1,5 @@
 import auth from '@/plugins/auth'
-import router, { constantRoutes, dynamicRoutes } from '@/router'
+import router, { constantRoutes, dynamicRoutes, performanceRoutes } from '@/router'
 import { getRouters } from '@/api/menu'
 import Layout from '@/layout/index'
 import ParentView from '@/components/ParentView'
@@ -43,7 +43,9 @@ const usePermissionStore = defineStore(
             const rewriteRoutes = filterAsyncRouter(rdata, false, true)
             const defaultRoutes = filterAsyncRouter(defaultData)
             const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
+            const asyncPerfRoutes = filterDynamicRoutes(performanceRoutes)
             asyncRoutes.forEach(route => { router.addRoute(route) })
+            asyncPerfRoutes.forEach(route => { router.addRoute(route) })
             this.setRoutes(rewriteRoutes)
             this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
             this.setDefaultRoutes(sidebarRoutes)

+ 46 - 0
src/views/performance/calculate/detail.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="app-container">
+    <el-descriptions title="绩效核算详情" :column="2" border>
+      <el-descriptions-item label="部门">{{ detail.deptName }}</el-descriptions-item>
+      <el-descriptions-item label="核算月份">{{ detail.month }}</el-descriptions-item>
+      <el-descriptions-item label="当月产值总和(万元)">{{ detail.totalOutput }}</el-descriptions-item>
+      <el-descriptions-item label="核算比例(%)">{{ detail.ratio }}</el-descriptions-item>
+      <el-descriptions-item label="绩效总额(万元)">
+        <span style="font-weight: bold; color: #409EFF; font-size: 18px">{{ detail.performanceAmount }}</span>
+      </el-descriptions-item>
+      <el-descriptions-item label="核算时间">{{ parseTime(detail.createTime) }}</el-descriptions-item>
+    </el-descriptions>
+
+    <el-divider content-position="left">项目产值明细</el-divider>
+
+    <el-table :data="detail.projects" border>
+      <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+      <el-table-column label="当月进度(%)" align="center" prop="currentProgress" />
+      <el-table-column label="当月产值(万元)" align="center" prop="currentOutput" />
+      <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" />
+      <el-table-column label="审核结果" align="center" prop="reviewResult">
+        <template #default="scope">
+          <el-tag :type="scope.row.reviewResult === 'APPROVED' ? 'success' : 'danger'">
+            {{ scope.row.reviewResult === 'APPROVED' ? '通过' : '不通过' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup name="CalculateDetail">
+import { getCalculate } from '@/api/performance/calculate'
+
+const route = useRoute()
+const deptId = route.params.deptId
+const month = route.params.month
+
+const detail = ref({})
+
+onMounted(() => {
+  getCalculate(deptId, month).then(res => {
+    detail.value = res.data
+  })
+})
+</script>

+ 197 - 0
src/views/performance/calculate/index.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="部门" prop="deptId">
+        <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" clearable check-strictly style="width: 200px" />
+      </el-form-item>
+      <el-form-item label="核算月份" prop="month">
+        <el-date-picker v-model="queryParams.month" type="month" value-format="YYYY-MM" placeholder="请选择月份" clearable style="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="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:calculate:export']">导出</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 绩效汇总卡片 -->
+    <el-row :gutter="20" class="mb8">
+      <el-col :span="6" v-for="card in summaryCards" :key="card.key">
+        <el-card shadow="hover">
+          <div class="summary-card">
+            <div class="summary-card__label">{{ card.label }}</div>
+            <div class="summary-card__value">{{ card.value }}</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 绩效核算图表 -->
+    <el-row :gutter="20" class="mb8">
+      <el-col :span="12">
+        <el-card shadow="hover">
+          <template #header><span>各部门当月绩效对比</span></template>
+          <div ref="barChartRef" style="height: 350px"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card shadow="hover">
+          <template #header><span>绩效月度趋势</span></template>
+          <div ref="lineChartRef" style="height: 350px"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-table v-loading="loading" :data="calculateList" border>
+      <el-table-column label="部门" align="center" prop="deptName" />
+      <el-table-column label="核算月份" align="center" prop="month" />
+      <el-table-column label="当月产值总和(万元)" align="center" prop="totalOutput" />
+      <el-table-column label="核算比例(%)" align="center" prop="ratio" />
+      <el-table-column label="绩效总额(万元)" align="center" prop="performanceAmount">
+        <template #default="scope">
+          <span style="font-weight: bold; color: #409EFF">{{ scope.row.performanceAmount }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="100">
+        <template #default="scope">
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+  </div>
+</template>
+
+<script setup name="PerformanceCalculate">
+import * as echarts from 'echarts'
+import { listCalculate, getCalculateSummary } from '@/api/performance/calculate'
+
+const { proxy } = getCurrentInstance()
+
+const calculateList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const total = ref(0)
+const deptOptions = ref([])
+const barChartRef = ref(null)
+const lineChartRef = ref(null)
+let barChart = null
+let lineChart = null
+
+const summaryCards = ref([
+  { key: 'totalOutput', label: '全院当月产值总和(万元)', value: '--' },
+  { key: 'totalPerformance', label: '全院当月绩效总额(万元)', value: '--' },
+  { key: 'deptCount', label: '已核算部门数', value: '--' },
+  { key: 'projectCount', label: '已核算项目数', value: '--' }
+])
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    deptId: undefined,
+    month: undefined
+  }
+})
+
+const { queryParams } = toRefs(data)
+
+function getList() {
+  loading.value = true
+  listCalculate(queryParams.value).then(res => {
+    calculateList.value = res.rows
+    total.value = res.total
+    loading.value = false
+  })
+}
+
+function getSummary() {
+  getCalculateSummary(queryParams.value).then(res => {
+    const data = res.data
+    summaryCards.value = [
+      { key: 'totalOutput', label: '全院当月产值总和(万元)', value: data.totalOutput || '--' },
+      { key: 'totalPerformance', label: '全院当月绩效总额(万元)', value: data.totalPerformance || '--' },
+      { key: 'deptCount', label: '已核算部门数', value: data.deptCount || '--' },
+      { key: 'projectCount', label: '已核算项目数', value: data.projectCount || '--' }
+    ]
+  })
+}
+
+function initBarChart() {
+  if (!barChartRef.value) return
+  barChart = echarts.init(barChartRef.value)
+  barChart.setOption({
+    tooltip: { trigger: 'axis' },
+    xAxis: { type: 'category', data: [] },
+    yAxis: { type: 'value', name: '绩效总额(万元)' },
+    series: [{ type: 'bar', data: [], itemStyle: { color: '#409EFF' } }]
+  })
+}
+
+function initLineChart() {
+  if (!lineChartRef.value) return
+  lineChart = echarts.init(lineChartRef.value)
+  lineChart.setOption({
+    tooltip: { trigger: 'axis' },
+    xAxis: { type: 'category', data: [] },
+    yAxis: { type: 'value', name: '绩效总额(万元)' },
+    series: [{ type: 'line', data: [], smooth: true, itemStyle: { color: '#67C23A' } }]
+  })
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1
+  getList()
+  getSummary()
+}
+
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  handleQuery()
+}
+
+function handleDetail(row) {
+  // TODO: 查看部门绩效详情
+}
+
+function handleExport() {
+  proxy.download('/performance/calculate/export', { ...queryParams.value }, `calculate_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList()
+  getSummary()
+  nextTick(() => {
+    initBarChart()
+    initLineChart()
+  })
+})
+
+onUnmounted(() => {
+  barChart?.dispose()
+  lineChart?.dispose()
+})
+</script>
+
+<style lang="scss" scoped>
+.summary-card {
+  text-align: center;
+  &__label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+  &__value {
+    font-size: 24px;
+    font-weight: bold;
+    color: #303133;
+  }
+}
+</style>

+ 168 - 0
src/views/performance/message/index.vue

@@ -0,0 +1,168 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Check" @click="handleReadAll" :disabled="unreadCount === 0">全部标为已读</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">批量删除</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane label="全部消息" name="all" />
+      <el-tab-pane :label="'未读消息 (' + unreadCount + ')'" name="unread" />
+    </el-tabs>
+
+    <el-table v-loading="loading" :data="messageList" @selection-change="handleSelectionChange" row-key="messageId">
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column label="消息类型" align="center" prop="messageType" width="100">
+        <template #default="scope">
+          <el-tag :type="getMessageTagType(scope.row.messageType)">{{ getMessageTypeLabel(scope.row.messageType) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="消息内容" align="center" prop="content" :show-overflow-tooltip="true">
+        <template #default="scope">
+          <span :style="{ fontWeight: scope.row.status === 'UNREAD' ? 'bold' : 'normal', cursor: 'pointer' }" @click="handleRead(scope.row)">
+            {{ scope.row.content }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="接收时间" align="center" prop="createTime" width="160">
+        <template #default="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="80">
+        <template #default="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="80">
+        <template #default="scope">
+          <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+  </div>
+</template>
+
+<script setup name="PerformanceMessage">
+import { listMessage, readMessage, readAllMessage, delMessage, getUnreadCount } from '@/api/performance/message'
+
+const { proxy } = getCurrentInstance()
+
+const statusOptions = ref([
+  { label: '未读', value: 'UNREAD' },
+  { label: '已读', value: 'READ' }
+])
+
+const messageTypeMap = {
+  'FILL_REMINDER': { label: '填报提醒', tagType: 'warning' },
+  'COOPERATION_REMINDER': { label: '协作提醒', tagType: 'info' },
+  'REVIEW_REMINDER': { label: '审核提醒', tagType: 'danger' },
+  'OVERDUE_REMINDER': { label: '逾期提醒', tagType: 'danger' },
+  'REVIEW_RESULT': { label: '审核结果', tagType: 'success' }
+}
+
+const messageList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const activeTab = ref('all')
+const unreadCount = ref(0)
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    status: undefined
+  }
+})
+
+const { queryParams } = toRefs(data)
+
+function getList() {
+  loading.value = true
+  listMessage(queryParams.value).then(res => {
+    messageList.value = res.rows
+    total.value = res.total
+    loading.value = false
+  })
+}
+
+function getUnreadCountApi() {
+  getUnreadCount().then(res => {
+    unreadCount.value = res.data || 0
+  })
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1
+  getList()
+}
+
+function handleTabChange(tab) {
+  queryParams.value.status = tab === 'unread' ? 'UNREAD' : undefined
+  handleQuery()
+}
+
+function handleSelectionChange(selection) {
+  ids.value = selection.map(item => item.messageId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+function handleRead(row) {
+  if (row.status === 'UNREAD') {
+    readMessage(row.messageId).then(() => {
+      row.status = 'READ'
+      getUnreadCountApi()
+    })
+  }
+}
+
+function handleReadAll() {
+  proxy.$modal.confirm('确认将所有消息标为已读?').then(() => {
+    return readAllMessage()
+  }).then(() => {
+    proxy.$modal.msgSuccess('操作成功')
+    getList()
+    getUnreadCountApi()
+  }).catch(() => {})
+}
+
+function handleDelete(row) {
+  const messageIds = row.messageId || ids.value.join(',')
+  proxy.$modal.confirm('确认删除选中的消息?').then(() => {
+    return delMessage(messageIds)
+  }).then(() => {
+    proxy.$modal.msgSuccess('删除成功')
+    getList()
+    getUnreadCountApi()
+  }).catch(() => {})
+}
+
+function handleBatchDelete() {
+  handleDelete(null)
+}
+
+function getMessageTypeLabel(type) {
+  return (messageTypeMap[type] || {}).label || type
+}
+
+function getMessageTagType(type) {
+  return (messageTypeMap[type] || {}).tagType || 'info'
+}
+
+onMounted(() => {
+  getList()
+  getUnreadCountApi()
+})
+</script>

+ 123 - 0
src/views/performance/output/cooperation.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="app-container">
+    <el-alert title="协作申请确认" type="info" :closable="false" show-icon style="margin-bottom: 20px">
+      <template #default>
+        <p>协作部门管理员可在此发起协同申请,填报协作部分的当月产值,由牵头部门管理员确认。</p>
+      </template>
+    </el-alert>
+
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="申请部门" prop="deptId">
+        <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" clearable check-strictly style="width: 200px" />
+      </el-form-item>
+      <el-form-item label="确认状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="确认状态" clearable style="width: 200px">
+          <el-option label="待确认" value="PENDING" />
+          <el-option label="已确认" value="CONFIRMED" />
+          <el-option label="已拒绝" value="REJECTED" />
+        </el-select>
+      </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:output:cooperation']">发起协作申请</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="cooperationList">
+      <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+      <el-table-column label="协作部门" align="center" prop="coopDeptName" />
+      <el-table-column label="填报月份" align="center" prop="month" />
+      <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" />
+      <el-table-column label="确认状态" align="center" prop="status">
+        <template #default="scope">
+          <el-tag :type="scope.row.status === 'CONFIRMED' ? 'success' : scope.row.status === 'REJECTED' ? 'danger' : 'warning'">
+            {{ scope.row.status === 'CONFIRMED' ? '已确认' : scope.row.status === 'REJECTED' ? '已拒绝' : '待确认' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="180">
+        <template #default="scope">
+          <el-button link type="success" icon="CircleCheck" @click="handleConfirm(scope.row)" v-if="scope.row.status === 'PENDING'" v-hasPermi="['performance:output:cooperation']">确认</el-button>
+          <el-button link type="danger" icon="CircleClose" @click="handleReject(scope.row)" v-if="scope.row.status === 'PENDING'" v-hasPermi="['performance:output:cooperation']">拒绝</el-button>
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+  </div>
+</template>
+
+<script setup name="OutputCooperation">
+import { listCooperation, confirmCooperation } from '@/api/performance/output'
+
+const { proxy } = getCurrentInstance()
+
+const cooperationList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const total = ref(0)
+const deptOptions = ref([])
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    deptId: undefined,
+    status: undefined
+  }
+})
+
+const { queryParams } = toRefs(data)
+
+function getList() {
+  loading.value = true
+  listCooperation(queryParams.value).then(res => {
+    cooperationList.value = res.rows
+    total.value = res.total
+    loading.value = false
+  })
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1
+  getList()
+}
+
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  handleQuery()
+}
+
+function handleAdd() {
+  // TODO: 发起协作申请对话框
+}
+
+function handleConfirm(row) {
+  proxy.$modal.confirm('确认该协作申请?').then(() => {
+    return confirmCooperation(row.cooperationId)
+  }).then(() => {
+    proxy.$modal.msgSuccess('确认成功')
+    getList()
+  }).catch(() => {})
+}
+
+function handleReject(row) {
+  // TODO: 拒绝协作申请
+}
+
+function handleDetail(row) {
+  // TODO: 查看协作申请详情
+}
+
+onMounted(() => {
+  getList()
+})
+</script>

+ 244 - 0
src/views/performance/output/index.vue

@@ -0,0 +1,244 @@
+<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="month">
+        <el-date-picker v-model="queryParams.month" type="month" value-format="YYYY-MM" placeholder="请选择月份" clearable style="width: 200px" />
+      </el-form-item>
+      <el-form-item label="审核状态" prop="reviewStatus">
+        <el-select v-model="queryParams.reviewStatus" placeholder="审核状态" clearable style="width: 200px">
+          <el-option label="待提交" value="DRAFT" />
+          <el-option label="已提交待审核" value="SUBMITTED" />
+          <el-option label="审核通过" value="APPROVED" />
+          <el-option label="审核不通过" value="REJECTED" />
+        </el-select>
+      </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="Edit" @click="handleAdd" v-hasPermi="['performance:output:add']">填报产值</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="Upload" :disabled="multiple" @click="handleBatchSubmit" v-hasPermi="['performance:output:submit']">批量提交</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:output:export']">导出</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="outputList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+      <el-table-column label="填报月份" align="center" prop="month" />
+      <el-table-column label="当月进度(%)" align="center" prop="currentProgress" />
+      <el-table-column label="当月产值(万元)" align="center" prop="currentOutput" />
+      <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" />
+      <el-table-column label="审核状态" align="center" prop="reviewStatus">
+        <template #default="scope">
+          <dict-tag :options="reviewStatusOptions" :value="scope.row.reviewStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-if="scope.row.reviewStatus === 'DRAFT'" v-hasPermi="['performance:output:edit']">修改</el-button>
+          <el-button link type="primary" icon="Upload" @click="handleSubmit(scope.row)" v-if="scope.row.reviewStatus === 'DRAFT'" v-hasPermi="['performance:output:submit']">提交</el-button>
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <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="650px" append-to-body>
+      <el-form :model="form" :rules="rules" ref="outputRef" label-width="140px">
+        <el-form-item label="项目" prop="projectId">
+          <el-select v-model="form.projectId" placeholder="请选择项目" :disabled="form.outputId !== undefined" style="width: 100%">
+            <el-option v-for="item in projectOptions" :key="item.projectId" :label="item.projectName" :value="item.projectId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="填报月份" prop="month">
+          <el-date-picker v-model="form.month" type="month" value-format="YYYY-MM" placeholder="请选择月份" :disabled="form.outputId !== undefined" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="当月实际进度(%)" prop="currentProgress">
+          <el-input-number v-model="form.currentProgress" :precision="1" :min="0" :max="100" placeholder="请输入当月实际进度" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="当月实际产值(万元)" prop="currentOutput">
+          <el-input-number v-model="form.currentOutput" :precision="4" :min="0" placeholder="请输入当月实际产值" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="进度说明" prop="description">
+          <el-input v-model="form.description" type="textarea" placeholder="请输入进度说明" maxlength="500" />
+        </el-form-item>
+      </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>
+  </div>
+</template>
+
+<script setup name="PerformanceOutput">
+import { listOutput, getOutput, addOutput, updateOutput, submitOutput } from '@/api/performance/output'
+
+const { proxy } = getCurrentInstance()
+
+const reviewStatusOptions = ref([
+  { label: '待提交', value: 'DRAFT' },
+  { label: '已提交待审核', value: 'SUBMITTED' },
+  { label: '审核通过', value: 'APPROVED' },
+  { label: '审核不通过', value: 'REJECTED' }
+])
+
+const outputList = 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 projectOptions = ref([])
+
+const data = reactive({
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    projectName: undefined,
+    month: undefined,
+    reviewStatus: undefined
+  },
+  rules: {
+    projectId: [{ required: true, message: '项目不能为空', trigger: 'change' }],
+    month: [{ required: true, message: '填报月份不能为空', trigger: 'change' }],
+    currentProgress: [{ required: true, message: '当月实际进度不能为空', trigger: 'blur' }],
+    currentOutput: [{ required: true, message: '当月实际产值不能为空', trigger: 'blur' }]
+  }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+function getList() {
+  loading.value = true
+  listOutput(queryParams.value).then(res => {
+    outputList.value = res.rows
+    total.value = res.total
+    loading.value = false
+  })
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1
+  getList()
+}
+
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  handleQuery()
+}
+
+function handleSelectionChange(selection) {
+  ids.value = selection.map(item => item.outputId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+function reset() {
+  form.value = {
+    outputId: undefined,
+    projectId: undefined,
+    month: undefined,
+    currentProgress: undefined,
+    currentOutput: undefined,
+    description: undefined
+  }
+  proxy.resetForm('outputRef')
+}
+
+function cancel() {
+  open.value = false
+  reset()
+}
+
+function handleAdd() {
+  reset()
+  open.value = true
+  title.value = '产值填报'
+}
+
+function handleUpdate(row) {
+  reset()
+  getOutput(row.outputId).then(res => {
+    form.value = res.data
+    open.value = true
+    title.value = '修改产值填报'
+  })
+}
+
+function handleDetail(row) {
+  // TODO: 查看填报详情
+}
+
+function handleSubmit(row) {
+  proxy.$modal.confirm('确认提交该条产值填报?提交后将无法修改。').then(() => {
+    return submitOutput(row.outputId)
+  }).then(() => {
+    proxy.$modal.msgSuccess('提交成功')
+    getList()
+  }).catch(() => {})
+}
+
+function handleBatchSubmit() {
+  if (ids.value.length === 0) {
+    proxy.$modal.msgWarning('请选择要提交的填报记录')
+    return
+  }
+  proxy.$modal.confirm('确认批量提交选中的产值填报?').then(() => {
+    const tasks = ids.value.map(id => submitOutput(id))
+    return Promise.all(tasks)
+  }).then(() => {
+    proxy.$modal.msgSuccess('批量提交成功')
+    getList()
+  }).catch(() => {})
+}
+
+function handleExport() {
+  proxy.download('/performance/output/export', { ...queryParams.value }, `output_${new Date().getTime()}.xlsx`)
+}
+
+function submitForm() {
+  proxy.$refs.outputRef.validate(valid => {
+    if (valid) {
+      if (form.value.outputId) {
+        updateOutput(form.value).then(() => {
+          proxy.$modal.msgSuccess('修改成功')
+          open.value = false
+          getList()
+        })
+      } else {
+        addOutput(form.value).then(() => {
+          proxy.$modal.msgSuccess('填报成功')
+          open.value = false
+          getList()
+        })
+      }
+    }
+  })
+}
+
+onMounted(() => {
+  getList()
+})
+</script>

+ 42 - 0
src/views/performance/project/detail.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="app-container">
+    <el-descriptions title="项目详情" :column="2" border>
+      <el-descriptions-item label="项目名称">{{ project.projectName }}</el-descriptions-item>
+      <el-descriptions-item label="项目类型">{{ project.projectType }}</el-descriptions-item>
+      <el-descriptions-item label="牵头部门">{{ project.deptName }}</el-descriptions-item>
+      <el-descriptions-item label="协作部门">{{ project.coopDeptName || '无' }}</el-descriptions-item>
+      <el-descriptions-item label="合同额(万元)">{{ project.contractAmount }}</el-descriptions-item>
+      <el-descriptions-item label="外协成本(万元)">{{ project.outsourceCost }}</el-descriptions-item>
+      <el-descriptions-item label="预估产值(万元)">{{ project.estimatedOutput }}</el-descriptions-item>
+      <el-descriptions-item label="实际产值(万元)">{{ project.actualOutput }}</el-descriptions-item>
+      <el-descriptions-item label="项目状态">
+        <dict-tag :options="projectStatusOptions" :value="project.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label="创建时间">{{ parseTime(project.createTime) }}</el-descriptions-item>
+      <el-descriptions-item label="备注" :span="2">{{ project.remark || '无' }}</el-descriptions-item>
+    </el-descriptions>
+  </div>
+</template>
+
+<script setup name="ProjectDetail">
+import { getProject } from '@/api/performance/project'
+
+const route = useRoute()
+const projectId = route.params.projectId
+
+const projectStatusOptions = ref([
+  { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS' },
+  { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED' },
+  { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS' },
+  { label: '完工', value: 'COMPLETED' },
+  { label: '作废', value: 'CANCELLED' }
+])
+
+const project = ref({})
+
+onMounted(() => {
+  getProject(projectId).then(res => {
+    project.value = res.data
+  })
+})
+</script>

+ 678 - 0
src/views/performance/project/index.vue

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

+ 196 - 0
src/views/performance/review/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="部门" prop="deptId">
+        <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" clearable check-strictly style="width: 200px" />
+      </el-form-item>
+      <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="result">
+        <el-select v-model="queryParams.result" placeholder="审核结果" clearable style="width: 200px">
+          <el-option label="待审核" value="PENDING" />
+          <el-option label="审核通过" value="APPROVED" />
+          <el-option label="审核不通过" value="REJECTED" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="填报月份" prop="month">
+        <el-date-picker v-model="queryParams.month" type="month" value-format="YYYY-MM" placeholder="请选择月份" clearable style="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="success" plain icon="CircleCheck" :disabled="multiple" @click="handleBatchApprove" v-hasPermi="['performance:review:approve']">批量审核通过</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:review:export']">导出</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="reviewList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+      <el-table-column label="填报部门" align="center" prop="deptName" />
+      <el-table-column label="填报月份" align="center" prop="month" />
+      <el-table-column label="当月进度(%)" align="center" prop="currentProgress" />
+      <el-table-column label="当月产值(万元)" align="center" prop="currentOutput" />
+      <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" />
+      <el-table-column label="审核结果" align="center" prop="result">
+        <template #default="scope">
+          <dict-tag :options="resultOptions" :value="scope.row.result" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button link type="success" icon="CircleCheck" @click="handleApprove(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">通过</el-button>
+          <el-button link type="danger" icon="CircleClose" @click="handleReject(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">不通过</el-button>
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 审核不通过对话框 -->
+    <el-dialog title="审核不通过" v-model="rejectOpen" width="500px" append-to-body>
+      <el-form :model="rejectForm" :rules="rejectRules" ref="rejectRef" label-width="100px">
+        <el-form-item label="不通过原因" prop="reason">
+          <el-input v-model="rejectForm.reason" type="textarea" placeholder="请填写审核不通过的原因" maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="danger" @click="submitReject">确 定</el-button>
+          <el-button @click="rejectOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="PerformanceReview">
+import { listReview, approveReview, rejectReview } from '@/api/performance/review'
+
+const { proxy } = getCurrentInstance()
+
+const resultOptions = ref([
+  { label: '待审核', value: 'PENDING' },
+  { label: '审核通过', value: 'APPROVED' },
+  { label: '审核不通过', value: 'REJECTED' }
+])
+
+const reviewList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const rejectOpen = ref(false)
+const currentRow = ref(null)
+const deptOptions = ref([])
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    deptId: undefined,
+    projectName: undefined,
+    result: undefined,
+    month: undefined
+  },
+  rejectForm: {
+    reviewId: undefined,
+    reason: undefined
+  },
+  rejectRules: {
+    reason: [{ required: true, message: '不通过原因不能为空', trigger: 'blur' }]
+  }
+})
+
+const { queryParams, rejectForm, rejectRules } = toRefs(data)
+
+function getList() {
+  loading.value = true
+  listReview(queryParams.value).then(res => {
+    reviewList.value = res.rows
+    total.value = res.total
+    loading.value = false
+  })
+}
+
+function handleQuery() {
+  queryParams.value.pageNum = 1
+  getList()
+}
+
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  handleQuery()
+}
+
+function handleSelectionChange(selection) {
+  ids.value = selection.map(item => item.reviewId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+function handleApprove(row) {
+  proxy.$modal.confirm('确认审核通过该条产值填报?').then(() => {
+    return approveReview({ reviewId: row.reviewId })
+  }).then(() => {
+    proxy.$modal.msgSuccess('审核通过')
+    getList()
+  }).catch(() => {})
+}
+
+function handleReject(row) {
+  rejectForm.value.reviewId = row.reviewId
+  rejectForm.value.reason = undefined
+  currentRow.value = row
+  rejectOpen.value = true
+}
+
+function submitReject() {
+  proxy.$refs.rejectRef.validate(valid => {
+    if (valid) {
+      rejectReview(rejectForm.value).then(() => {
+        proxy.$modal.msgSuccess('已提交审核不通过意见')
+        rejectOpen.value = false
+        getList()
+      })
+    }
+  })
+}
+
+function handleBatchApprove() {
+  if (ids.value.length === 0) {
+    proxy.$modal.msgWarning('请选择要审核的记录')
+    return
+  }
+  proxy.$modal.confirm('确认批量审核通过选中的记录?').then(() => {
+    return approveReview({ reviewIds: ids.value })
+  }).then(() => {
+    proxy.$modal.msgSuccess('批量审核通过')
+    getList()
+  }).catch(() => {})
+}
+
+function handleDetail(row) {
+  // TODO: 查看审核详情
+}
+
+function handleExport() {
+  proxy.download('/performance/review/export', { ...queryParams.value }, `review_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList()
+})
+</script>