refactor: 重构系统权限与设置管理,新增多类型配置能力

1.  重构权限码规范,将系统权限前缀从perm:system改为system:
2.  扩展系统设置模型,支持string/image/select/boolean四种配置类型
3.  新增设置项选项配置、图片上传功能
4.  重构初始化权限数据,拆分权限与系统设置权限结构
5.  重写前端系统设置页面,支持按类型渲染不同配置表单
6.  添加python-multipart依赖支持文件上传

遗留问题:
1.系统设置页面的添加设置按钮无法使用
This commit is contained in:
jayhgq 2026-05-27 15:39:13 +08:00
parent 01bda3d0cc
commit 82cf0192e8
9 changed files with 380 additions and 50 deletions

View File

@ -1,7 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from fastcrud import FastCRUD
from pathlib import Path
import os
import shutil
from datetime import datetime
from database import get_db
from models import SystemSetting, User
@ -18,6 +22,10 @@ router = APIRouter(prefix="/settings", tags=["settings"])
# 创建FastCRUD实例
setting_crud = FastCRUD(SystemSetting, SystemSettingResponse)
# 图片存储目录
UPLOAD_DIR = Path(__file__).parent.parent.parent / "uploads"
UPLOAD_DIR.mkdir(exist_ok=True)
@router.post("/", response_model=SystemSettingResponse)
async def create_setting(
@ -36,6 +44,8 @@ async def create_setting(
db_setting = SystemSetting(
key=setting_data.key,
value=setting_data.value,
type=setting_data.type or "string",
options=setting_data.options,
description=setting_data.description,
)
@ -129,3 +139,39 @@ async def delete_setting(
# 使用FastCRUD删除设置
deleted = await setting_crud.delete(db, id=setting_id)
return {"message": "设置项删除成功"}
@router.post("/upload")
async def upload_image(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
):
"""上传图片"""
# 检查文件类型
if not file.filename or not file.filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
raise HTTPException(status_code=400, detail="只支持图片格式png, jpg, jpeg, gif, webp")
# 检查文件大小2MB
file_size = 0
chunk = await file.read(1024)
while chunk:
file_size += len(chunk)
if file_size > 2 * 1024 * 1024:
raise HTTPException(status_code=400, detail="文件大小不能超过2MB")
chunk = await file.read(1024)
# 重置文件指针
await file.seek(0)
# 生成文件名
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
ext = file.filename.split('.')[-1]
filename = f"setting_{timestamp}.{ext}"
file_path = UPLOAD_DIR / filename
# 保存文件
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 返回文件路径
return {"url": f"/uploads/{filename}"}

View File

@ -50,49 +50,71 @@ async def init_permissions(db: AsyncSession):
# 定义需要初始化的权限(树形结构)
# parentid: 0 = 顶级模块, >0 = 子权限
permission_tree = [
# ========================================
# 顶级模块
# ========================================
{"name": "权限管理", "code": "perm", "parentid": 0, "description": "权限管理模块"},
{"name": "系统设置", "code": "system", "parentid": 0, "description": "系统设置模块"},
# 二级子模块
# ========================================
# 权限管理 -> 二级子模块
# ========================================
{"name": "用户管理", "code": "perm:user", "parentid": 1, "description": "用户管理子模块"},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "description": "角色管理子模块"},
{"name": "权限配置", "code": "perm:config", "parentid": 1, "description": "权限配置子模块"},
{"name": "系统设置", "code": "perm:system", "parentid": 1, "description": "系统设置子模块"},
# 用户管理操作
{"name": "查看列表", "code": "perm:user:list", "parentid": 2, "description": "查看用户列表"},
{"name": "查看详情", "code": "perm:user:detail", "parentid": 2, "description": "查看用户详情"},
{"name": "搜索", "code": "perm:user:search", "parentid": 2, "description": "搜索用户"},
{"name": "创建", "code": "perm:user:create", "parentid": 2, "description": "创建新用户"},
{"name": "编辑", "code": "perm:user:edit", "parentid": 2, "description": "编辑用户信息"},
{"name": "删除", "code": "perm:user:delete", "parentid": 2, "description": "删除用户"},
{"name": "导出", "code": "perm:user:export", "parentid": 2, "description": "导出用户数据"},
# ========================================
# 系统设置 -> 二级子模块
# ========================================
{"name": "基础配置", "code": "system:basic", "parentid": 2, "description": "系统基础配置"},
{"name": "高级配置", "code": "system:advanced", "parentid": 2, "description": "系统高级配置"},
# 角色管理操作
{"name": "查看列表", "code": "perm:role:list", "parentid": 3, "description": "查看角色列表"},
{"name": "查看详情", "code": "perm:role:detail", "parentid": 3, "description": "查看角色详情"},
{"name": "搜索", "code": "perm:role:search", "parentid": 3, "description": "搜索角色"},
{"name": "创建", "code": "perm:role:create", "parentid": 3, "description": "创建新角色"},
{"name": "编辑", "code": "perm:role:edit", "parentid": 3, "description": "编辑角色信息"},
{"name": "删除", "code": "perm:role:delete", "parentid": 3, "description": "删除角色"},
{"name": "导出", "code": "perm:role:export", "parentid": 3, "description": "导出角色数据"},
{"name": "分配权限", "code": "perm:role:assign", "parentid": 3, "description": "为角色分配权限"},
# ========================================
# 用户管理操作parentid=3
# ========================================
{"name": "查看列表", "code": "perm:user:list", "parentid": 3, "description": "查看用户列表"},
{"name": "查看详情", "code": "perm:user:detail", "parentid": 3, "description": "查看用户详情"},
{"name": "搜索", "code": "perm:user:search", "parentid": 3, "description": "搜索用户"},
{"name": "创建", "code": "perm:user:create", "parentid": 3, "description": "创建新用户"},
{"name": "编辑", "code": "perm:user:edit", "parentid": 3, "description": "编辑用户信息"},
{"name": "删除", "code": "perm:user:delete", "parentid": 3, "description": "删除用户"},
{"name": "导出", "code": "perm:user:export", "parentid": 3, "description": "导出用户数据"},
{"name": "批量操作", "code": "perm:user:batch", "parentid": 3, "description": "批量操作用户"},
# 权限配置操作
{"name": "查看列表", "code": "perm:config:list", "parentid": 4, "description": "查看权限列表"},
{"name": "查看详情", "code": "perm:config:detail", "parentid": 4, "description": "查看权限详情"},
{"name": "搜索", "code": "perm:config:search", "parentid": 4, "description": "搜索权限"},
{"name": "创建", "code": "perm:config:create", "parentid": 4, "description": "创建新权限"},
{"name": "编辑", "code": "perm:config:edit", "parentid": 4, "description": "编辑权限信息"},
{"name": "删除", "code": "perm:config:delete", "parentid": 4, "description": "删除权限"},
{"name": "导出", "code": "perm:config:export", "parentid": 4, "description": "导岀权限数据"},
# ========================================
# 角色管理操作parentid=4
# ========================================
{"name": "查看列表", "code": "perm:role:list", "parentid": 4, "description": "查看角色列表"},
{"name": "查看详情", "code": "perm:role:detail", "parentid": 4, "description": "查看角色详情"},
{"name": "搜索", "code": "perm:role:search", "parentid": 4, "description": "搜索角色"},
{"name": "创建", "code": "perm:role:create", "parentid": 4, "description": "创建新角色"},
{"name": "编辑", "code": "perm:role:edit", "parentid": 4, "description": "编辑角色信息"},
{"name": "删除", "code": "perm:role:delete", "parentid": 4, "description": "删除角色"},
{"name": "导出", "code": "perm:role:export", "parentid": 4, "description": "导出角色数据"},
{"name": "分配权限", "code": "perm:role:assign", "parentid": 4, "description": "为角色分配权限"},
# 系统设置操作parentid 为 5对应系统设置子模块
{"name": "查看列表", "code": "perm:system:list", "parentid": 5, "description": "查看系统设置列表"},
{"name": "查看详情", "code": "perm:system:detail", "parentid": 5, "description": "查看系统设置详情"},
{"name": "创建", "code": "perm:system:create", "parentid": 5, "description": "创建设置项"},
{"name": "编辑", "code": "perm:system:edit", "parentid": 5, "description": "编辑设置项"},
{"name": "删除", "code": "perm:system:delete", "parentid": 5, "description": "删除设置项"},
# ========================================
# 权限配置操作parentid=5
# ========================================
{"name": "查看列表", "code": "perm:config:list", "parentid": 5, "description": "查看权限列表"},
{"name": "查看详情", "code": "perm:config:detail", "parentid": 5, "description": "查看权限详情"},
{"name": "搜索", "code": "perm:config:search", "parentid": 5, "description": "搜索权限"},
{"name": "创建", "code": "perm:config:create", "parentid": 5, "description": "创建新权限"},
{"name": "编辑", "code": "perm:config:edit", "parentid": 5, "description": "编辑权限信息"},
{"name": "删除", "code": "perm:config:delete", "parentid": 5, "description": "删除权限"},
{"name": "导出", "code": "perm:config:export", "parentid": 5, "description": "导岀权限数据"},
# ========================================
# 系统设置 -> 基础配置操作parentid=6
# ========================================
{"name": "查看配置", "code": "system:basic:view", "parentid": 6, "description": "查看基础配置"},
{"name": "修改配置", "code": "system:basic:edit", "parentid": 6, "description": "修改基础配置"},
# ========================================
# 系统设置 -> 高级配置操作parentid=7
# ========================================
{"name": "查看配置", "code": "system:advanced:view", "parentid": 7, "description": "查看高级配置"},
{"name": "修改配置", "code": "system:advanced:edit", "parentid": 7, "description": "修改高级配置"},
]
# 检查现有权限

View File

@ -88,7 +88,9 @@ class SystemSetting(Base):
__tablename__ = "system_setting"
id = Column(Integer, primary_key=True, index=True, comment="设置ID")
key = Column(String(100), nullable=False, unique=True, comment="设置键名")
value = Column(String(1000), nullable=False, comment="设置值")
value = Column(String(2000), nullable=False, comment="设置值")
type = Column(String(20), default="string", comment="类型string/image/select/boolean")
options = Column(String(500), nullable=True, comment="下拉选项(用|分隔)")
description = Column(String(255), nullable=True, comment="设置描述")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(

View File

@ -28,4 +28,7 @@ asyncpg>=0.31.0
redis>=7.2.0
# 验证码生成
captcha>=0.7.1
captcha>=0.7.1
# 文件上传
python-multipart>=0.0.9

View File

@ -190,7 +190,9 @@ class SystemSettingBase(BaseModel):
"""系统设置基础模型"""
key: str = Field(..., max_length=100, description="设置键名")
value: str = Field(..., max_length=1000, description="设置值")
value: str = Field(..., max_length=2000, description="设置值")
type: Optional[str] = Field("string", max_length=20, description="类型string/image/select/boolean")
options: Optional[str] = Field(None, max_length=500, description="下拉选项(用|分隔)")
description: Optional[str] = Field(None, max_length=255, description="设置描述")
model_config = {"extra": "ignore"}
@ -212,6 +214,8 @@ class SystemSettingResponse(BaseModel):
id: int = Field(..., description="设置ID")
key: str = Field(..., description="设置键名")
value: str = Field(..., description="设置值")
type: str = Field(..., description="类型")
options: Optional[str] = Field(None, description="下拉选项")
description: Optional[str] = Field(None, description="设置描述")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")

View File

@ -98,7 +98,7 @@ const hasPermissionPermission = computed(() => {
});
const hasSystemPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:system"));
return auth.permissions.some(p => p.code.startsWith("system:"));
});
const hasAnyPermission = computed(() => {

View File

@ -1,25 +1,245 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" />
<el-card>
<template #header>
<div class="flex items-center justify-between">
<span>系统设置管理</span>
<el-button type="primary" @click="openAddModal">添加设置</el-button>
</div>
</template>
<el-table :data="settings" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="key" label="设置键名" />
<el-table-column prop="value" label="设置值" :show-overflow-tooltip="true" />
<el-table-column prop="type" label="类型" width="120">
<template #default="scope">
{{ getTypeLabel(scope.row.type) }}
</template>
</el-table-column>
<el-table-column prop="description" label="描述" />
<el-table-column prop="createtime" label="创建时间" width="180" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="openEditModal(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog :title="isEdit ? '编辑设置' : '添加设置'" :visible.sync="showModal" width="600px">
<el-form :model="form" label-width="120px">
<el-form-item label="设置键名" prop="key">
<el-input v-model="form.key" placeholder="请输入设置键名" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" placeholder="请选择类型">
<el-option label="字符串" value="string" />
<el-option label="图片" value="image" />
<el-option label="下拉列表" value="select" />
<el-option label="布尔值" value="boolean" />
</el-select>
</el-form-item>
<!-- 字符串类型 -->
<el-form-item v-if="form.type === 'string'" label="设置值" prop="value">
<el-input v-model="form.value" type="textarea" :rows="3" placeholder="请输入值" />
</el-form-item>
<!-- 图片类型 -->
<el-form-item v-if="form.type === 'image'" label="设置值" prop="value">
<el-upload
action="#"
accept="image/*"
list-type="picture-card"
:show-file-list="false"
:before-upload="handleImageUpload"
:on-remove="handleImageRemove"
>
<div v-if="!form.value">
<el-icon><UploadIcon /></el-icon>
<div>上传图片</div>
</div>
<img v-else :src="form.value" style="width: 100%; height: 100%" />
</el-upload>
</el-form-item>
<!-- 下拉列表类型 -->
<template v-if="form.type === 'select'">
<el-form-item label="选项配置" prop="options">
<el-input v-model="form.options" type="textarea" :rows="2" placeholder="多个选项用 | 分隔选项1|选项2|选项3" />
</el-form-item>
<el-form-item label="设置值" prop="value">
<el-select v-model="form.value" placeholder="请选择">
<el-option v-for="opt in optionsList" :key="opt" :label="opt" :value="opt" />
</el-select>
</el-form-item>
</template>
<!-- 布尔值类型 -->
<el-form-item v-if="form.type === 'boolean'" label="设置值" prop="value">
<el-switch v-model="form.booleanValue" active-text="开启" inactive-text="关闭" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="请输入描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</fs-page>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import { createSettingCrudOptions } from "./setting-crud";
import { ref, computed, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { http } from "@/modules/admin/api/http";
import type { SettingRow } from "./setting-crud";
const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions: createSettingCrudOptions,
const settings = ref<SettingRow[]>([]);
const showModal = ref(false);
const isEdit = ref(false);
const form = ref({
id: 0,
key: "",
value: "",
type: "string",
options: "",
description: "",
booleanValue: false,
});
onMounted(() => {
crudExpose.doRefresh();
const optionsList = computed(() => {
if (!form.value.options) return [];
return form.value.options.split("|").filter(Boolean).map((o) => o.trim());
});
watch(() => form.value.type, (newType) => {
if (newType === "boolean") {
form.value.booleanValue = form.value.value === "true";
}
});
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
string: "字符串",
image: "图片",
select: "下拉列表",
boolean: "布尔值",
};
return types[type] || type;
};
const loadData = async () => {
try {
const { data } = await http.get<SettingRow[]>("/settings/");
settings.value = data;
} catch (error) {
ElMessage.error("加载失败");
}
};
const openAddModal = () => {
isEdit.value = false;
form.value = {
id: 0,
key: "",
value: "",
type: "string",
options: "",
description: "",
booleanValue: false,
};
showModal.value = true;
};
const openEditModal = (row: SettingRow) => {
isEdit.value = true;
form.value = {
id: row.id,
key: row.key,
value: row.value,
type: row.type,
options: row.options || "",
description: row.description || "",
booleanValue: row.value === "true",
};
showModal.value = true;
};
const handleImageUpload = async (file: any) => {
const formData = new FormData();
formData.append("file", file.raw);
try {
const response = await http.post("/settings/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
form.value.value = response.data.url;
} catch (error) {
ElMessage.error("上传失败");
}
return false;
};
const handleImageRemove = () => {
form.value.value = "";
};
const handleSubmit = async () => {
if (!form.value.key) {
ElMessage.error("请输入设置键名");
return;
}
if (form.value.type === "boolean") {
form.value.value = form.value.booleanValue ? "true" : "false";
}
try {
if (isEdit.value) {
await http.put(`/settings/${form.value.id}`, form.value);
ElMessage.success("修改成功");
} else {
await http.post("/settings/", form.value);
ElMessage.success("添加成功");
}
showModal.value = false;
await loadData();
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || "操作失败");
}
};
const handleDelete = async (row: SettingRow) => {
try {
await ElMessageBox.confirm("确定要删除吗?");
await http.delete(`/settings/${row.id}`);
ElMessage.success("删除成功");
await loadData();
} catch {
//
}
};
loadData();
</script>
<style scoped>
.setting-manage {
height: 100%;
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
</style>

View File

@ -1,4 +1,5 @@
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import { dict } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { http } from "@/modules/admin/api/http";
@ -6,6 +7,8 @@ export type SettingRow = {
id: number;
key: string;
value: string;
type: string;
options?: string | null;
description?: string | null;
createtime: string;
updatetime: string;
@ -71,12 +74,42 @@ export const createSettingCrudOptions: CreateCrudOptions<SettingRow> = () => {
rules: [{ required: true, message: "请输入设置键名", trigger: "blur" }],
},
},
type: {
title: "类型",
key: "type",
type: "dict-select",
column: { width: 120 },
dict: dict({
data: [
{ value: "string", label: "字符串" },
{ value: "image", label: "图片" },
{ value: "select", label: "下拉列表" },
{ value: "boolean", label: "布尔值" },
],
value: "value",
label: "label",
}),
form: {
value: "string",
rules: [{ required: true, message: "请选择类型", trigger: "change" }],
},
},
value: {
title: "设置值",
key: "value",
type: "textarea",
column: { width: 200 },
form: {
rules: [{ required: true, message: "请输入设置值", trigger: "blur" }],
rules: [{ required: true, message: "请输入值", trigger: "blur" }],
},
},
options: {
title: "选项配置",
key: "options",
type: "textarea",
column: { show: false },
form: {
placeholder: "多个选项用 | 分隔选项1|选项2|选项3",
},
},
description: {

View File

@ -48,7 +48,7 @@ const router = createRouter({
{
path: "settings",
name: "admin-settings",
meta: { title: "系统设置", permission: "perm:system" },
meta: { title: "系统设置", permission: "system:" },
component: () =>
import("@/modules/admin/views/settings/SettingManage.vue"),
},