v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成

- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
hankin
2026-03-16 07:31:37 +00:00
commit 423baff73b
2578 changed files with 824643 additions and 0 deletions
+517
View File
@@ -0,0 +1,517 @@
<script setup lang="ts">
/**
* 系统设置页 —— 完整 CRUD 交互
* Tab1: 部门树 + 员工管理(新增/编辑弹窗、重置密码、启停)
* Tab2: 角色管理(新增/编辑弹窗 + 权限分配面板 + JSONB menu_keys
*/
import { ref, reactive, onMounted, nextTick } from 'vue'
import { Plus, Edit, Key, Lock, Setting, User, Operation, Check } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import request from '@/api/request'
const activeTab = ref('users')
const loading = ref(false)
// ==========================================
// Tab 1: 部门与员工管理
// ==========================================
// --- 左侧:部门树 ---
const orgTreeData = ref<any[]>([])
const defaultProps = { children: 'children', label: 'name' }
const currentDeptId = ref<string | null>(null)
const currentDeptName = ref('全部部门')
const fetchDeptTree = async () => {
try {
const data = await request.get('/api/settings/departments/tree')
orgTreeData.value = Array.isArray(data) ? data : []
} catch (e) { console.warn('[Settings] fetchDeptTree error:', e) }
}
const handleNodeClick = (data: any) => {
currentDeptId.value = data.id
currentDeptName.value = data.name
fetchUsers()
}
// --- 扁平化部门列表(供 el-select 使用)---
const flatDepts = ref<any[]>([])
const flattenDepts = (nodes: any[], result: any[] = [], depth = 0) => {
for (const n of nodes) {
result.push({ id: n.id, name: ' '.repeat(depth) + n.name })
if (n.children?.length) flattenDepts(n.children, result, depth + 1)
}
return result
}
// --- 右侧:员工大表 ---
const userSearchInput = ref('')
const userList = ref<any[]>([])
const userTotal = ref(0)
const userPage = ref(1)
const userPageSize = ref(20)
const fetchUsers = async () => {
loading.value = true
try {
const params: Record<string, any> = { page: userPage.value, size: userPageSize.value }
if (currentDeptId.value) params.dept_id = currentDeptId.value
if (userSearchInput.value) params.keyword = userSearchInput.value
const data: any = await request.get('/api/settings/users', { params })
userList.value = (data?.items || []).map((u: any) => ({ ...u, status: u.status === 1 }))
userTotal.value = data?.total || 0
} catch (e) { console.warn('[Settings] fetchUsers error:', e) }
finally { loading.value = false }
}
const handleUserSearch = () => { userPage.value = 1; fetchUsers() }
const toggleUserStatus = async (row: any) => {
try {
const newStatus = row.status ? 1 : 0
await request.put(`/api/settings/users/${row.id}`, { status: newStatus })
ElMessage.success(`账号 [${row.username}] 已${row.status ? '启用' : '停用'}`)
} catch { row.status = !row.status }
}
const resetPassword = (row: any) => {
ElMessageBox.confirm(
`确定要将员工 [${row.real_name || row.username}] 的密码重置为默认密码 (123456) 吗?`, '警告', { type: 'warning' }
).then(async () => {
try {
await request.put(`/api/settings/users/${row.id}/reset-password`, { new_password: '123456' })
ElMessage.success(`员工 [${row.real_name || row.username}] 密码已重置!`)
} catch { /* 已统一 */ }
}).catch(() => {})
}
// --- 员工新增/编辑弹窗 ---
const userDialogVisible = ref(false)
const userDialogTitle = ref('新增员工')
const userFormRef = ref<FormInstance>()
const userSubmitting = ref(false)
const userForm = reactive({
id: '' as string,
username: '',
password: '',
real_name: '',
phone: '',
email: '',
dept_id: null as string | null,
role_id: null as string | null,
})
const isEditUser = ref(false)
const userFormRules = reactive<FormRules>({
username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入初始密码', trigger: 'blur' }, { min: 6, message: '密码至少6位', trigger: 'blur' }],
real_name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
})
const openAddUser = () => {
isEditUser.value = false
userDialogTitle.value = '新增员工'
Object.assign(userForm, { id: '', username: '', password: '', real_name: '', phone: '', email: '', dept_id: null, role_id: null })
userDialogVisible.value = true
nextTick(() => userFormRef.value?.clearValidate())
}
const openEditUser = (row: any) => {
isEditUser.value = true
userDialogTitle.value = '编辑员工'
Object.assign(userForm, {
id: row.id,
username: row.username,
password: '',
real_name: row.real_name || '',
phone: row.phone || '',
email: row.email || '',
dept_id: row.dept_id || null,
role_id: row.role_id || null,
})
userDialogVisible.value = true
nextTick(() => userFormRef.value?.clearValidate())
}
const submitUser = async () => {
const valid = await userFormRef.value?.validate().catch(() => false)
if (!valid) return
userSubmitting.value = true
try {
if (isEditUser.value) {
const { id, password, username, ...payload } = userForm
await request.put(`/api/settings/users/${id}`, payload)
ElMessage.success('员工信息已更新')
} else {
await request.post('/api/settings/users', userForm)
ElMessage.success('员工账号创建成功')
}
userDialogVisible.value = false
fetchUsers()
} catch { /* 已统一 */ }
finally { userSubmitting.value = false }
}
// ==========================================
// Tab 2: 角色与权限控制
// ==========================================
const rolesList = ref<any[]>([])
const currentRole = ref<any>({})
const fetchRoles = async () => {
try {
const data = await request.get('/api/settings/roles')
const arr = Array.isArray(data) ? data : []
rolesList.value = arr.map((r: any) => ({
id: r.id, name: r.role_name, desc: r.description || '',
data_scope: r.data_scope, menu_keys: r.menu_keys || [], status: r.status,
}))
if (rolesList.value.length > 0 && !currentRole.value?.id) {
selectRole(rolesList.value[0])
}
} catch (e) { console.warn('[Settings] fetchRoles error:', e) }
}
const selectRole = (role: any) => {
currentRole.value = role
dataScope.value = role.data_scope || 'self'
// 延迟设置 menu_keys 勾选(组件可能还未渲染,需安全访问)
nextTick(() => {
try { menuTreeRef.value?.setCheckedKeys(role.menu_keys || [], false) } catch { /* tree not ready */ }
})
}
// --- 权限面板 ---
const dataScopeOptions = [
{ label: '全部数据 (最高权限)', value: 'all' },
{ label: '本部门及下属部门数据', value: 'dept_and_sub' },
{ label: '仅本人数据', value: 'self' }
]
const dataScope = ref('all')
const menuPermissionsData = [
{ id: 'dashboard', label: '🏠 工作台 (Dashboard)' },
{
id: 'sales', label: '💼 业务线 (Sales)',
children: [
{ id: 'CustomerList', label: '👥 客户管理' },
{ id: 'OrderList', label: '📄 订单管理' },
{ id: 'ShippingList', label: '🚚 发货记录' },
]
},
{ id: 'supply', label: '📦 供应链 (Supply)', children: [{ id: 'ProductList', label: '📦 产品与库存' }] },
{ id: 'finance', label: '💰 财务管理 (Finance)', children: [{ id: 'FinanceList', label: '🎫 发票与报销' }] },
{ id: 'settings', label: '⚙️ 系统设置 (Settings)' },
]
const menuTreeRef = ref<any>(null)
const savePermissions = async () => {
if (!currentRole.value?.id) return
try {
const checkedKeys = menuTreeRef.value?.getCheckedKeys(false) || []
await request.put(`/api/settings/roles/${currentRole.value.id}`, {
data_scope: dataScope.value,
menu_keys: checkedKeys,
})
currentRole.value.data_scope = dataScope.value
currentRole.value.menu_keys = checkedKeys
ElMessage.success(`角色 [${currentRole.value.name}] 的权限配置保存成功!`)
} catch { /* 已统一 */ }
}
// --- 角色新增/编辑弹窗 ---
const roleDialogVisible = ref(false)
const roleDialogTitle = ref('新增角色')
const roleFormRef = ref<FormInstance>()
const roleSubmitting = ref(false)
const roleForm = reactive({
id: '' as string,
role_name: '',
data_scope: 'self',
description: '',
})
const isEditRole = ref(false)
const roleFormRules = reactive<FormRules>({
role_name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
})
const openAddRole = () => {
isEditRole.value = false
roleDialogTitle.value = '新增角色'
Object.assign(roleForm, { id: '', role_name: '', data_scope: 'self', description: '' })
roleDialogVisible.value = true
nextTick(() => roleFormRef.value?.clearValidate())
}
const openEditRole = () => {
if (!currentRole.value?.id) return
isEditRole.value = true
roleDialogTitle.value = '编辑角色'
Object.assign(roleForm, {
id: currentRole.value.id,
role_name: currentRole.value.name,
data_scope: currentRole.value.data_scope || 'self',
description: currentRole.value.desc || '',
})
roleDialogVisible.value = true
nextTick(() => roleFormRef.value?.clearValidate())
}
const submitRole = async () => {
const valid = await roleFormRef.value?.validate().catch(() => false)
if (!valid) return
roleSubmitting.value = true
try {
if (isEditRole.value) {
const { id, ...payload } = roleForm
await request.put(`/api/settings/roles/${id}`, payload)
ElMessage.success('角色信息已更新')
} else {
const { id, ...payload } = roleForm
await request.post('/api/settings/roles', payload)
ElMessage.success('角色创建成功')
}
roleDialogVisible.value = false
await fetchRoles()
} catch { /* 已统一 */ }
finally { roleSubmitting.value = false }
}
// ==========================================
// 初始化
// ==========================================
onMounted(async () => {
// 独立 await 避免一个接口失败导致全部阻塞
await fetchDeptTree()
flatDepts.value = flattenDepts(orgTreeData.value)
await fetchUsers()
await fetchRoles()
})
</script>
<template>
<div class="settings-wrapper">
<el-card shadow="never" class="main-card">
<el-tabs v-model="activeTab" class="settings-tabs">
<!-- ============================================== -->
<!-- Tab 1: 部门与员工管理 -->
<!-- ============================================== -->
<el-tab-pane name="users">
<template #label>
<span class="custom-tabs-label"><el-icon><User /></el-icon><span>部门与员工管理</span></span>
</template>
<el-row :gutter="20">
<!-- 左侧部门树 -->
<el-col :span="5">
<el-card shadow="never" class="tree-card">
<template #header><div class="card-header-bold">组织架构</div></template>
<el-tree :data="orgTreeData" :props="defaultProps" default-expand-all highlight-current node-key="id" @node-click="handleNodeClick" />
</el-card>
</el-col>
<!-- 右侧员工表 -->
<el-col :span="19">
<el-card shadow="never" class="user-table-card">
<div class="table-toolbar">
<div class="toolbar-left">
<span class="dept-title">{{ currentDeptName }} 员工名单</span>
</div>
<div class="toolbar-right">
<el-input v-model="userSearchInput" placeholder="姓名 / 手机号" prefix-icon="Search" style="width: 200px; margin-right: 15px;" clearable @keyup.enter="handleUserSearch" @clear="handleUserSearch" />
<el-button type="primary" :icon="Plus" @click="openAddUser">新增员工</el-button>
</div>
</div>
<el-table :data="userList" stripe border style="width: 100%" v-loading="loading">
<el-table-column prop="real_name" label="员工姓名" width="120">
<template #default="scope"><b>{{ scope.row.real_name || '-' }}</b></template>
</el-table-column>
<el-table-column prop="username" label="登录账号" width="140" />
<el-table-column prop="dept_name" label="所属部门" width="150" />
<el-table-column label="分配角色" min-width="160">
<template #default="scope">
<el-tag :type="scope.row.role_name?.includes('管理') ? 'danger' : 'primary'" effect="plain" v-if="scope.row.role_name">{{ scope.row.role_name }}</el-tag>
<span v-else style="color: #909399;">未分配</span>
</template>
</el-table-column>
<el-table-column label="数据权限" width="130">
<template #default="scope">
<el-tag :type="scope.row.data_scope === 'all' ? 'danger' : scope.row.data_scope === 'dept_and_sub' ? 'warning' : 'info'" size="small" v-if="scope.row.data_scope">
{{ scope.row.data_scope === 'all' ? '全部' : scope.row.data_scope === 'dept_and_sub' ? '本部门' : '仅本人' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="登录状态" width="100" align="center">
<template #default="scope">
<el-switch v-model="scope.row.status" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" @change="toggleUserStatus(scope.row)" />
</template>
</el-table-column>
<el-table-column label="安全与操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link :icon="Edit" @click="openEditUser(scope.row)">编辑</el-button>
<el-button type="danger" link :icon="Key" @click="resetPassword(scope.row)">重置密码</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-if="userTotal > userPageSize" style="margin-top: 16px; justify-content: flex-end;" background layout="total, prev, pager, next" :total="userTotal" :page-size="userPageSize" v-model:current-page="userPage" @current-change="fetchUsers" />
</el-card>
</el-col>
</el-row>
</el-tab-pane>
<!-- ============================================== -->
<!-- Tab 2: 角色与权限控制 -->
<!-- ============================================== -->
<el-tab-pane name="roles">
<template #label>
<span class="custom-tabs-label"><el-icon><Lock /></el-icon><span>角色与权限控制 (RBAC)</span></span>
</template>
<el-row :gutter="20">
<!-- 左侧角色列表 -->
<el-col :span="6">
<el-card shadow="never" class="roles-card">
<template #header>
<div style="display:flex; justify-content:space-between; align-items:center;">
<span class="card-header-bold">平台运营角色</span>
<el-button type="primary" link :icon="Plus" @click="openAddRole">新增角色</el-button>
</div>
</template>
<div class="role-list">
<div v-for="role in rolesList" :key="role.id" class="role-item" :class="{ 'is-active': currentRole.id === role.id }" @click="selectRole(role)">
<div class="role-name">{{ role.name }}</div>
<div class="role-desc">{{ role.desc }}</div>
</div>
<el-empty v-if="rolesList.length === 0" description="暂无角色" :image-size="60" />
</div>
</el-card>
</el-col>
<!-- 右侧权限分配面板 -->
<el-col :span="18">
<el-card shadow="never" class="perm-card">
<template #header>
<div class="perm-header">
<div class="card-header-bold">
角色配置<span style="color:#409EFF">{{ currentRole.name || '请选择角色' }}</span>
<el-button v-if="currentRole.id" type="primary" link :icon="Edit" style="margin-left: 10px;" @click="openEditRole">编辑基础信息</el-button>
</div>
<el-button type="success" :icon="Check" @click="savePermissions" :disabled="!currentRole.id">保存当前权限设置</el-button>
</div>
</template>
<div class="perm-section">
<div class="section-title"><el-icon><Operation /></el-icon> 数据权限穿透范围 (Data Scope)</div>
<el-form label-position="top">
<el-form-item>
<el-select v-model="dataScope" style="width: 400px">
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<div class="tip-text">控制该角色在查看客户订单发票等数据时的横向与纵向范围限制</div>
</el-form-item>
</el-form>
</div>
<el-divider />
<div class="perm-section">
<div class="section-title"><el-icon><Setting /></el-icon> 侧边栏模块与按钮级访问权限 (Menu Permissions)</div>
<el-tree ref="menuTreeRef" :data="menuPermissionsData" show-checkbox default-expand-all node-key="id" :default-checked-keys="currentRole.menu_keys || []" class="perm-tree" />
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- ============================================== -->
<!-- 弹窗新增/编辑员工 -->
<!-- ============================================== -->
<el-dialog v-model="userDialogVisible" :title="userDialogTitle" width="520px" destroy-on-close>
<el-form ref="userFormRef" :model="userForm" :rules="userFormRules" label-width="80px">
<el-form-item label="登录账号" prop="username">
<el-input v-model="userForm.username" placeholder="用于登录系统" :disabled="isEditUser" />
</el-form-item>
<el-form-item label="初始密码" prop="password" v-if="!isEditUser">
<el-input v-model="userForm.password" type="password" show-password placeholder="至少6位" />
</el-form-item>
<el-form-item label="员工姓名" prop="real_name">
<el-input v-model="userForm.real_name" placeholder="真实姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="userForm.phone" placeholder="手机号码" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="userForm.email" placeholder="邮箱地址" />
</el-form-item>
<el-form-item label="所属部门">
<el-select v-model="userForm.dept_id" placeholder="选择部门" clearable style="width: 100%">
<el-option v-for="d in flatDepts" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
</el-form-item>
<el-form-item label="分配角色">
<el-select v-model="userForm.role_id" placeholder="选择角色" clearable style="width: 100%">
<el-option v-for="r in rolesList" :key="r.id" :label="r.name" :value="r.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="userSubmitting" @click="submitUser">{{ isEditUser ? '保存修改' : '确定创建' }}</el-button>
</template>
</el-dialog>
<!-- ============================================== -->
<!-- 弹窗新增/编辑角色 -->
<!-- ============================================== -->
<el-dialog v-model="roleDialogVisible" :title="roleDialogTitle" width="480px" destroy-on-close>
<el-form ref="roleFormRef" :model="roleForm" :rules="roleFormRules" label-width="80px">
<el-form-item label="角色名称" prop="role_name">
<el-input v-model="roleForm.role_name" placeholder="如:工业油销售总监" />
</el-form-item>
<el-form-item label="数据权限">
<el-select v-model="roleForm.data_scope" style="width: 100%">
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="角色描述">
<el-input v-model="roleForm.description" type="textarea" :rows="2" placeholder="可选描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="roleDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="roleSubmitting" @click="submitRole">{{ isEditRole ? '保存修改' : '确定创建' }}</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.settings-wrapper { height: 100%; }
.main-card { border-radius: 8px; border: none; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); min-height: calc(100vh - 100px); }
.settings-tabs :deep(.el-tabs__item) { font-size: 15px; height: 50px; line-height: 50px; }
.custom-tabs-label { display: flex; align-items: center; gap: 6px; font-weight: bold; }
.card-header-bold { font-weight: bold; font-size: 15px; color: #303133; }
.tree-card, .user-table-card, .roles-card, .perm-card { border-radius: 6px; min-height: 600px; }
.table-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.dept-title { font-size: 16px; font-weight: bold; color: #409EFF; border-left: 4px solid #409EFF; padding-left: 10px; }
.toolbar-right { display: flex; align-items: center; }
.role-list { display: flex; flex-direction: column; gap: 10px; }
.role-item { padding: 15px; border: 1px solid #EBEEF5; border-radius: 4px; cursor: pointer; transition: all 0.3s; }
.role-item:hover { box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); }
.role-item.is-active { border-color: #409EFF; background-color: #ecf5ff; }
.role-name { font-weight: bold; font-size: 14px; margin-bottom: 6px; color: #303133; }
.role-desc { font-size: 12px; color: #909399; }
.perm-header { display: flex; justify-content: space-between; align-items: center; }
.section-title { font-size: 15px; font-weight: bold; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; color: #303133; }
.tip-text { font-size: 12px; color: #909399; margin-top: 8px; }
.perm-tree { margin-top: 10px; background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #ebeef5; }
</style>