Forráskód Böngészése

feat: #15 系统管理页面 - 全项目收官

管理员后台总控页面,快捷导航+数据备份+系统配置

- 4张快捷导航卡片: 用户管理/部门管理/角色权限管理/操作日志
  (链接至RuoYi现有/system/user|dept|role + /monitor/operlog页面)
- 数据备份Tab: 手动备份按钮+备份列表(文件名/大小/时间)+恢复/下载/删除
  + 空状态引导 + 覆盖恢复二次确认
- 系统配置Tab: 核算比例(1-100%,2位小数)/填报时间窗(1-31日)
  /审核时间窗(1-31日)/安全锁定时间(1-1440分钟)/备份周期(1-90天)
  + 批量保存+重置
- 权限守卫: 非admin角色自动重定向至首页
- 卡片hover动效 + 彩色顶部边条

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shenzx 1 hónapja
szülő
commit
3206d8ae47
3 módosított fájl, 412 hozzáadás és 0 törlés
  1. 66 0
      src/api/performance/system.js
  2. 14 0
      src/router/index.js
  3. 332 0
      src/views/performance/system/index.vue

+ 66 - 0
src/api/performance/system.js

@@ -0,0 +1,66 @@
+import request from '@/utils/request'
+
+// ===== 数据备份 =====
+
+// 执行数据备份
+export function executeBackup() {
+  return request({
+    url: '/system/backup/execute',
+    method: 'post'
+  })
+}
+
+// 查询备份文件列表
+export function listBackups() {
+  return request({
+    url: '/system/backup/list',
+    method: 'get'
+  })
+}
+
+// 删除备份文件
+export function deleteBackup(fileName) {
+  return request({
+    url: '/system/backup/' + encodeURIComponent(fileName),
+    method: 'delete'
+  })
+}
+
+// 恢复备份
+export function restoreBackup(fileName) {
+  return request({
+    url: '/system/backup/restore/' + encodeURIComponent(fileName),
+    method: 'post'
+  })
+}
+
+// 下载备份文件
+export function downloadBackup(fileName) {
+  return request({
+    url: '/system/backup/download/' + encodeURIComponent(fileName),
+    method: 'get',
+    responseType: 'blob'
+  })
+}
+
+// ===== 系统配置 =====
+
+// 查询绩效相关配置(批量获取)
+export function getPerfConfigs() {
+  return request({
+    url: '/system/config/configKeys',
+    method: 'get',
+    params: {
+      keys: 'perf.calculation_ratio,perf.fill_start_day,perf.fill_end_day,perf.review_start_day,perf.review_end_day,sys.account.lockTime,perf.backup_cycle'
+    }
+  })
+}
+
+// 更新系统配置
+export function updatePerfConfig(key, value) {
+  return request({
+    url: '/system/config/updateByKey',
+    method: 'put',
+    data: { configKey: key, configValue: value }
+  })
+}

+ 14 - 0
src/router/index.js

@@ -237,6 +237,20 @@ export const performanceRoutes = [
         meta: { title: '项目查询统计', activeMenu: '/performance/project' }
       }
     ]
+  },
+  {
+    path: '/performance/system',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:system:manage'],
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/performance/system/index'),
+        name: 'SystemManagement',
+        meta: { title: '系统管理', activeMenu: '/performance/system' }
+      }
+    ]
   }
 ]
 

+ 332 - 0
src/views/performance/system/index.vue

@@ -0,0 +1,332 @@
+<template>
+  <div class="app-container">
+    <!-- 快捷导航卡片 -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="6" v-for="(item, idx) in navCards" :key="item.key">
+        <el-card shadow="hover" class="nav-card" :class="`nav-card--color${idx}`" @click="navigateTo(item.path)">
+          <div class="nav-card__icon">{{ item.icon }}</div>
+          <div class="nav-card__title">{{ item.title }}</div>
+          <div class="nav-card__desc">{{ item.desc }}</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 功能区域 Tabs -->
+    <el-card shadow="never">
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- Tab 1: 数据备份 -->
+        <el-tab-pane label="数据备份" name="backup">
+          <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+              <el-button type="primary" icon="Upload" @click="handleManualBackup" :loading="backupLoading" v-hasPermi="['system:backup:execute']">
+                手动备份
+              </el-button>
+            </el-col>
+            <el-col :span="1.5">
+              <el-button icon="Refresh" @click="getBackupList">刷新列表</el-button>
+            </el-col>
+          </el-row>
+
+          <el-alert title="备份说明" type="info" :closable="false" show-icon class="mb16">
+            <template #default>
+              <p style="margin: 0; font-size: 12px; color: #909399">
+                手动备份将导出当前数据库全部数据为 SQL 文件,存储在服务器 backup 目录。定期备份可防止数据丢失。
+                恢复备份将覆盖当前数据库,请谨慎操作。
+              </p>
+            </template>
+          </el-alert>
+
+          <el-table v-loading="backupListLoading" :data="backupList" border stripe>
+            <el-table-column label="文件名" align="center" prop="name" :show-overflow-tooltip="true" />
+            <el-table-column label="文件大小" align="center" width="140">
+              <template #default="scope">{{ formatFileSize(scope.row.length) }}</template>
+            </el-table-column>
+            <el-table-column label="修改时间" align="center" width="180">
+              <template #default="scope">{{ formatTime(scope.row.lastModified) }}</template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="260">
+              <template #default="scope">
+                <el-button link type="primary" icon="Download" @click="handleDownload(scope.row)">下载</el-button>
+                <el-popconfirm title="确定恢复此备份?当前数据将被覆盖且不可撤销!" @confirm="handleRestore(scope.row)">
+                  <template #reference>
+                    <el-button link type="warning" icon="RefreshRight">恢复</el-button>
+                  </template>
+                </el-popconfirm>
+                <el-popconfirm title="确定删除此备份文件?" @confirm="handleDeleteBackup(scope.row)">
+                  <template #reference>
+                    <el-button link type="danger" icon="Delete">删除</el-button>
+                  </template>
+                </el-popconfirm>
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <div class="mt16" v-if="backupList.length === 0 && !backupListLoading">
+            <el-empty description="暂无备份文件,请点击手动备份创建">
+              <el-button type="primary" @click="handleManualBackup" :loading="backupLoading">立即备份</el-button>
+            </el-empty>
+          </div>
+        </el-tab-pane>
+
+        <!-- Tab 2: 系统配置 -->
+        <el-tab-pane label="系统配置" name="config">
+          <el-form :model="configForm" ref="configRef" label-width="180px" class="config-form">
+            <el-divider content-position="left">绩效核算配置</el-divider>
+            <el-form-item label="绩效核算比例(%)" prop="calculationRatio">
+              <el-input-number v-model="configForm.calculationRatio" :min="1" :max="100" :precision="2" style="width: 200px" />
+              <span class="config-tip">部门当月绩效总额 = 当月审核通过产值总和 × 该比例</span>
+            </el-form-item>
+
+            <el-divider content-position="left">月度填报时间窗</el-divider>
+            <el-form-item label="填报开始日" prop="fillStartDay">
+              <el-input-number v-model="configForm.fillStartDay" :min="1" :max="31" style="width: 200px" />
+              <span class="config-tip">每月填报开始日期</span>
+            </el-form-item>
+            <el-form-item label="填报截止日" prop="fillEndDay">
+              <el-input-number v-model="configForm.fillEndDay" :min="1" :max="31" style="width: 200px" />
+              <span class="config-tip">每月填报截止日期,逾期未填报视为当月无产值</span>
+            </el-form-item>
+
+            <el-divider content-position="left">月度审核时间窗</el-divider>
+            <el-form-item label="审核开始日" prop="reviewStartDay">
+              <el-input-number v-model="configForm.reviewStartDay" :min="1" :max="31" style="width: 200px" />
+              <span class="config-tip">每月审核开始日期</span>
+            </el-form-item>
+            <el-form-item label="审核截止日" prop="reviewEndDay">
+              <el-input-number v-model="configForm.reviewEndDay" :min="1" :max="31" style="width: 200px" />
+              <span class="config-tip">每月审核截止日期,逾期未审核系统自动提醒</span>
+            </el-form-item>
+
+            <el-divider content-position="left">安全配置</el-divider>
+            <el-form-item label="账号锁定时间(分钟)" prop="lockTime">
+              <el-input-number v-model="configForm.lockTime" :min="1" :max="1440" style="width: 200px" />
+              <span class="config-tip">多次登录失败后的账号锁定时长</span>
+            </el-form-item>
+
+            <el-divider content-position="left">备份配置</el-divider>
+            <el-form-item label="自动备份周期(天)" prop="backupCycle">
+              <el-input-number v-model="configForm.backupCycle" :min="1" :max="90" style="width: 200px" />
+              <span class="config-tip">系统自动备份数据的周期,建议设为 7 天</span>
+            </el-form-item>
+
+            <el-form-item>
+              <el-button type="primary" icon="Check" @click="handleSaveConfig" :loading="configSaveLoading" v-hasPermi="['system:config:edit']">保 存</el-button>
+              <el-button icon="Refresh" @click="getConfigs">重 置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup name="SystemManagement">
+import { listBackups as fetchBackups, executeBackup, restoreBackup, deleteBackup } from '@/api/performance/system'
+import { listConfig, getConfigKey, updateConfig } from '@/api/system/config'
+import useUserStore from '@/store/modules/user'
+
+const { proxy } = getCurrentInstance()
+const router = useRouter()
+const userStore = useUserStore()
+
+// 权限校验
+const isAdmin = computed(() => userStore.roles.includes('admin') || userStore.roles.includes('ROLE_ADMIN'))
+
+// 快捷导航卡片
+const navCards = [
+  { key: 'user', icon: '👤', title: '用户管理', desc: '管理系统用户、分配部门与角色', path: '/system/user' },
+  { key: 'dept', icon: '🏢', title: '部门管理', desc: '维护6个核心部门信息', path: '/system/dept' },
+  { key: 'role', icon: '🔑', title: '角色权限管理', desc: '配置4种角色权限', path: '/system/role' },
+  { key: 'operlog', icon: '📋', title: '操作日志', desc: '查看业务操作记录与审计追溯', path: '/monitor/operlog' }
+]
+
+// 状态
+const activeTab = ref('backup')
+const backupList = ref([])
+const backupLoading = ref(false)
+const backupListLoading = ref(false)
+const configSaveLoading = ref(false)
+
+const configForm = reactive({
+  calculationRatio: 20,
+  fillStartDay: 1,
+  fillEndDay: 12,
+  reviewStartDay: 13,
+  reviewEndDay: 16,
+  lockTime: 30,
+  backupCycle: 7
+})
+
+// ===== 备份管理 =====
+function getBackupList() {
+  backupListLoading.value = true
+  fetchBackups().then(res => {
+    const data = res.data
+    backupList.value = Array.isArray(data) ? data : []
+    backupListLoading.value = false
+  }).catch(() => {
+    backupListLoading.value = false
+  })
+}
+
+function handleManualBackup() {
+  backupLoading.value = true
+  executeBackup().then(res => {
+    proxy.$modal.msgSuccess(res.msg || '数据备份成功')
+    getBackupList()
+    backupLoading.value = false
+  }).catch(() => {
+    backupLoading.value = false
+  })
+}
+
+function handleDownload(row) {
+  proxy.download('/system/backup/download/' + encodeURIComponent(row.name), {}, row.name)
+}
+
+function handleRestore(row) {
+  restoreBackup(row.name).then(res => {
+    proxy.$modal.msgSuccess('数据恢复成功')
+  })
+}
+
+function handleDeleteBackup(row) {
+  deleteBackup(row.name).then(res => {
+    proxy.$modal.msgSuccess('备份文件删除成功')
+    getBackupList()
+  })
+}
+
+function formatFileSize(bytes) {
+  if (!bytes || bytes === 0) return '0 B'
+  const k = 1024
+  const sizes = ['B', 'KB', 'MB', 'GB']
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+function formatTime(timestamp) {
+  if (!timestamp) return '--'
+  return proxy.parseTime(new Date(timestamp))
+}
+
+// ===== 系统配置 =====
+function getConfigs() {
+  const keys = [
+    { field: 'calculationRatio', key: 'perf.calculation_ratio', defaultVal: 20 },
+    { field: 'fillStartDay', key: 'perf.fill_start_day', defaultVal: 1 },
+    { field: 'fillEndDay', key: 'perf.fill_end_day', defaultVal: 12 },
+    { field: 'reviewStartDay', key: 'perf.review_start_day', defaultVal: 13 },
+    { field: 'reviewEndDay', key: 'perf.review_end_day', defaultVal: 16 },
+    { field: 'lockTime', key: 'sys.account.lockTime', defaultVal: 30 },
+    { field: 'backupCycle', key: 'perf.backup_cycle', defaultVal: 7 }
+  ]
+  keys.forEach(item => {
+    getConfigKey(item.key).then(res => {
+      const val = res.data
+      configForm[item.field] = val !== null && val !== undefined ? Number(val) : item.defaultVal
+    }).catch(() => {
+      // 配置项不存在时使用默认值
+    })
+  })
+}
+
+function handleSaveConfig() {
+  configSaveLoading.value = true
+
+  const configs = [
+    { key: 'perf.calculation_ratio', value: String(configForm.calculationRatio) },
+    { key: 'perf.fill_start_day', value: String(configForm.fillStartDay) },
+    { key: 'perf.fill_end_day', value: String(configForm.fillEndDay) },
+    { key: 'perf.review_start_day', value: String(configForm.reviewStartDay) },
+    { key: 'perf.review_end_day', value: String(configForm.reviewEndDay) },
+    { key: 'sys.account.lockTime', value: String(configForm.lockTime) },
+    { key: 'perf.backup_cycle', value: String(configForm.backupCycle) }
+  ]
+
+  // 先查询所有配置ID,再逐个更新
+  listConfig({}).then(listRes => {
+    const allConfigs = listRes.rows || []
+    const updatePromises = configs.map(cfg => {
+      const existing = allConfigs.find(c => c.configKey === cfg.key)
+      if (existing) {
+        return updateConfig({
+          configId: existing.configId,
+          configKey: cfg.key,
+          configValue: cfg.value,
+          configName: existing.configName
+        })
+      }
+      return Promise.resolve()
+    })
+
+    Promise.all(updatePromises).then(() => {
+      proxy.$modal.msgSuccess('系统配置保存成功')
+      configSaveLoading.value = false
+    }).catch(() => {
+      proxy.$modal.msgError('部分配置保存失败')
+      configSaveLoading.value = false
+    })
+  }).catch(() => {
+    proxy.$modal.msgError('获取配置列表失败')
+    configSaveLoading.value = false
+  })
+}
+
+// 快捷导航
+function navigateTo(path) {
+  // 在新标签页中打开Ruoyi现有页面
+  router.push(path)
+}
+
+onMounted(() => {
+  if (!isAdmin.value) {
+    proxy.$modal.msgWarning('仅系统管理员可访问此页面')
+    router.push('/index')
+    return
+  }
+  getBackupList()
+  getConfigs()
+})
+</script>
+
+<style lang="scss" scoped>
+.mb16 { margin-bottom: 16px; }
+.mb8 { margin-bottom: 8px; }
+.mt16 { margin-top: 16px; }
+
+.nav-card {
+  cursor: pointer;
+  text-align: center;
+  border-top: 3px solid #DCDFE6;
+  transition: all 0.3s;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  }
+
+  &--color0 { border-top-color: #409EFF; }
+  &--color1 { border-top-color: #67C23A; }
+  &--color2 { border-top-color: #E6A23C; }
+  &--color3 { border-top-color: #909399; }
+
+  &__icon { font-size: 28px; margin-bottom: 8px; }
+  &__title { font-size: 15px; font-weight: bold; color: #303133; margin-bottom: 4px; }
+  &__desc { font-size: 12px; color: #909399; }
+}
+
+.config-form {
+  max-width: 800px;
+
+  :deep(.el-divider) {
+    margin: 16px 0;
+  }
+}
+
+.config-tip {
+  margin-left: 12px;
+  font-size: 12px;
+  color: #909399;
+}
+</style>