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