diff --git a/README.md b/README.md
index 3d0f92f..d10ba53 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
-# simple_ams
-简单的资产管理系统
+# Fastapi Admin
+前后端分离项目实现的一个后端管理框架
+* 前端:vue3 + element plus
+* 后端:fastapi + sqlmodel
**个人学习项目,只会对明显的bug进行修复,不会进行过多新功能的更新**
@@ -17,103 +19,106 @@
uvicorn server.main:app --reload
```
+## 约束
+1. 后端数据库对于布尔值的传递统一数据库设置为tinyint,0为假,1为真
+2. 前端所有bool都0为假,1为真
# 功能实现介绍
-## 权限分配
-实力有限,实现简化的权限分配功能,思路如下:
+
+## 分页查询实现
### 前端
-1. 页面下的按钮需要权限管控的按钮,单独列出,再对这个按钮做权限分配
-2. 在“菜单管理”中对应页面下添加“按钮”,**路径**字段需要跟到时候做权限判断时对应
-3. 进入“角色管理”页面,分配对应的角色,可选择页面的按钮
- **NOTICE:**上级页面需要手动勾选,不然登录无法带出来
-4. 给按钮增加**v-if**判断语法,用法如下:
- ```
- v-if="$route.meta.import === true"
- ```
+```js
+
+import usePagination from '@/composables/usePagination'
+
+
+const searchForm = {
+ name: null,
+ email: null,
+ enable: null
+ }
+
+
+ const {
+ search,
+ tableData,
+ currentPage,
+ pageSize,
+ orderModel,
+ total,
+ freshCurrentPage,
+ handleSearch
+ } = usePagination('/api/users/search', searchForm)
+```
### 后端
-* 用户请求权限,返回子页面带meta元素,示例如下:
- ```
- [
- {
- "id": 1,
- "component": 'Layout,
- "parent_id": null,
- "url": null,
- "path": "/assets",
- "name": "资产管理",
- "type": "page",
- "enable": 1,
- "children": [
- {
- "id": 2,
- "parent_id": 1,
- "url": null,
- "path": "add",
- "name": "增加资产",
- "type": "page",
- "enable": 1
- },
- {
- "id": 11,
- "parent_id": 1,
- "url": null,
- "path": "system",
- "name": "系统",
- "type": "page",
- "enable": 1,
-
- "meta": {
- "import": true,
- "output": true
- }
- }
- ]
- },
- {
- "id": 3,
- "component": "Layout",
- "parent_id": null,
- "url": null,
- "path": "/system",
- "name": "系统管理",
- "type": "page",
- "enable": 1,
- "children": [
- {
- "id": 4,
- "parent_id": 3,
- "url": null,
- "path": "user",
- "name": "用户管理",
- "type": "page",
- "enable": 1
- },
- {
- "id": 6,
- "parent_id": 3,
- "url": null,
- "path": "roles",
- "name": "角色管理",
- "type": "page",
- "enable": 1
- },
- {
- "id": 7,
- "parent_id": 3,
- "url": null,
- "path": "menus",
- "name": "菜单管理",
- "type": "page",
- "enable": 1
- }
- ]
- }
- ]
- ```
-
-## 资产信息的动态字段设计
-充分利用MySQL的JSON字段特性,把动态字段放入JSON数据类型中。
-### 导入细节
-批量导入时,分为两类:1. 固定字段的列,2. 动态字段的列。
-对于**动态字段的列**,通过pandas函数拼接成str,然后进行格式转换导入。
+定义了一个分页模型
+```python
+from typing import Optional, Generic, TypeVar
+from pydantic import BaseModel
+
+T = TypeVar('T')
+
+
+class Pagination(BaseModel, Generic[T]):
+ search: T
+ page: Optional[int] = 1
+ page_size: Optional[int] = 10
+ model: Optional[str] = 'asc'
+```
+
+使用
+```python
+@router.post('/dict/item/search', summary="字典列表查询", response_model=ApiResponse[SearchResponse[DictRead]])
+async def search_items(search: Pagination[DictItemSearch], session: Session = Depends(get_session)):
+ # 需要定义一个filter_type,用于区分各个字段的匹配形式,可用为:l_like、r_like、like、eq、ne、lt、le、gt、ge
+ filter_type = DictItemSearchFilter(dict_id='eq', label='like', enable='eq', value='like')
+ total = crud.internal.dict_item.search_total(session, search.search, filter_type.dict())
+ items: List[DictRead] = crud.internal.dict_item.search(session, search, filter_type.dict())
+ # 转义下数据类型,不然在执行return的时候,会去获取外键、关联字段相关的内容,容易造成数据量过多等问题
+ item_list = [DictRead.from_orm(item) for item in items]
+ return ApiResponse(
+ data={
+ 'total': total,
+ 'data': item_list
+ }
+
+ )
+```
+
+## 权限管控
+通过casbin实现简化的权限管控功能,思路如下:
+1. 对于不需要token验证的,写入settings的APISettings.NO_VERIFY_URL中
+2. 对于需要权限管控的接口,写入casbin中,并且对需要权限验证的接口使用casbin验证
+3. 前端通过权限字段,进行显示
+4. 只能对按钮级别的功能实现权限管控
+5. 页面管控,只是后端返回菜单列表,前端根据菜单列表进行显示,后端没有对页面进行权限管控
+
+### 前端
+v-permission定义了权限标识,当拥有权限时,可以页面上能显示按钮,同时,后端也会进行权限的判断。
+```js
+ 编辑
+
+```
+### 后端
+```python
+@router.put('/roles', summary="更新角色", response_model=ApiResponse[Role],
+ dependencies=[Depends(Authority('role:update'))])
+async def update_roles(role_info: RoleUpdate, session: Session = Depends(get_session)):
+ print(role_info)
+ if role_info.name == 'admin':
+ ApiResponse(code=status.HTTP_400_BAD_REQUEST, message='admin权限组无法更新信息')
+ db_obj = crud.internal.role.get(session, role_info.id)
+ enable_menus = role_info.menus
+ delattr(role_info, 'menus')
+ db_obj = crud.internal.role.update(session, db_obj, role_info)
+ crud.internal.role.update_menus(session, db_obj, enable_menus)
+ return ApiResponse(
+ data=db_obj
+ )
+```
+
+
+### 参考项目:
+* https://github.com/xingxingzaixian/FastAPI-MySQL-Tortoise-Casbin
\ No newline at end of file
diff --git "a/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py" "b/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py"
new file mode 100644
index 0000000..90a94d4
--- /dev/null
+++ "b/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py"
@@ -0,0 +1,47 @@
+"""添加数据字典
+
+Revision ID: 001da528b756
+Revises:
+Create Date: 2022-11-08 10:11:55.957440
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision = '001da528b756'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('data_dict',
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='字典名称'),
+ sa.Column('code', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='字典编号'),
+ sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('dict_item',
+ sa.Column('enable', sa.Boolean(), nullable=True, comment='是否启用'),
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称'),
+ sa.Column('data', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值'),
+ sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'),
+ sa.Column('sort', sa.Integer(), nullable=True, comment='排序值,越小越靠前'),
+ sa.Column('dict_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['dict_id'], ['data_dict.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('dict_item')
+ op.drop_table('data_dict')
+ # ### end Alembic commands ###
diff --git "a/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py" "b/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py"
deleted file mode 100644
index 5842287..0000000
--- "a/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py"
+++ /dev/null
@@ -1,29 +0,0 @@
-"""菜单添加icon图标字段
-
-Revision ID: 02cae67f3be4
-Revises: b703662d9cf9
-Create Date: 2022-11-02 13:54:41.361623
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlmodel
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = '02cae67f3be4'
-down_revision = 'b703662d9cf9'
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('menu', sa.Column('icon', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='Icon图标'))
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_column('menu', 'icon')
- # ### end Alembic commands ###
diff --git a/server/alembic/versions/334040db4d77_delete_sysapi.py b/server/alembic/versions/334040db4d77_delete_sysapi.py
deleted file mode 100644
index 0ecc8da..0000000
--- a/server/alembic/versions/334040db4d77_delete_sysapi.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""delete sysapi
-
-Revision ID: 334040db4d77
-Revises: a21539d7fbb7
-Create Date: 2022-10-28 15:00:57.698249
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlmodel
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = '334040db4d77'
-down_revision = 'a21539d7fbb7'
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('menu_apis')
- op.drop_table('sys_api')
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('menu_apis',
- sa.Column('menu_id', mysql.INTEGER(), autoincrement=False, nullable=False),
- sa.Column('api_id', mysql.INTEGER(), autoincrement=False, nullable=False),
- sa.ForeignKeyConstraint(['api_id'], ['sys_api.id'], name='menu_apis_ibfk_2'),
- sa.ForeignKeyConstraint(['menu_id'], ['menu.id'], name='menu_apis_ibfk_1'),
- sa.PrimaryKeyConstraint('menu_id', 'api_id'),
- mysql_collate='utf8mb4_general_ci',
- mysql_default_charset='utf8mb4',
- mysql_engine='InnoDB',
- mysql_row_format='DYNAMIC'
- )
- op.create_table('sys_api',
- sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
- sa.Column('tags', mysql.VARCHAR(collation='utf8mb4_general_ci', length=10), nullable=False, comment='标签'),
- sa.Column('path', mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), nullable=False, comment='API路径'),
- sa.Column('method', mysql.VARCHAR(collation='utf8mb4_general_ci', length=10), nullable=False, comment='HTTP方法'),
- sa.Column('summary', mysql.VARCHAR(collation='utf8mb4_general_ci', length=20), nullable=False, comment='描述'),
- sa.Column('deprecated', mysql.TINYINT(display_width=1), server_default=sa.text("'0'"), autoincrement=False, nullable=True, comment='是否废弃'),
- sa.PrimaryKeyConstraint('id'),
- mysql_collate='utf8mb4_general_ci',
- mysql_default_charset='utf8mb4',
- mysql_engine='InnoDB',
- mysql_row_format='DYNAMIC'
- )
- # ### end Alembic commands ###
diff --git "a/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py" "b/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py"
new file mode 100644
index 0000000..1a30376
--- /dev/null
+++ "b/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py"
@@ -0,0 +1,43 @@
+"""update数据字典
+
+Revision ID: 9edc223ae20a
+Revises: 001da528b756
+Create Date: 2022-11-11 10:57:04.750421
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision = '9edc223ae20a'
+down_revision = '001da528b756'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('dict_item', sa.Column('label', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称'))
+ op.add_column('dict_item', sa.Column('value', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值'))
+ op.drop_column('dict_item', 'data')
+ op.drop_column('dict_item', 'name')
+ op.alter_column('menu', 'icon',
+ existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50),
+ nullable=True,
+ existing_comment='Icon图标')
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.alter_column('menu', 'icon',
+ existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50),
+ nullable=False,
+ existing_comment='Icon图标')
+ op.add_column('dict_item', sa.Column('name', mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), nullable=False, comment='名称'))
+ op.add_column('dict_item', sa.Column('data', mysql.VARCHAR(collation='utf8mb4_general_ci', length=100), nullable=False, comment='数据值'))
+ op.drop_column('dict_item', 'value')
+ op.drop_column('dict_item', 'label')
+ # ### end Alembic commands ###
diff --git a/server/alembic/versions/a21539d7fbb7_init.py b/server/alembic/versions/a21539d7fbb7_init.py
deleted file mode 100644
index b9ff371..0000000
--- a/server/alembic/versions/a21539d7fbb7_init.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""init
-
-Revision ID: a21539d7fbb7
-Revises:
-Create Date: 2022-10-28 10:38:17.100830
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlmodel
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'a21539d7fbb7'
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('user_roles',
- sa.Column('user_id', sa.Integer(), nullable=False),
- sa.Column('role_id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
- sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
- sa.PrimaryKeyConstraint('user_id', 'role_id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('user_roles')
- # ### end Alembic commands ###
diff --git "a/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" "b/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py"
deleted file mode 100644
index b4b27c0..0000000
--- "a/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py"
+++ /dev/null
@@ -1,29 +0,0 @@
-"""添加菜单排序字段
-
-Revision ID: a5e2cea226d4
-Revises: 334040db4d77
-Create Date: 2022-10-31 09:44:30.289049
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlmodel
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'a5e2cea226d4'
-down_revision = '334040db4d77'
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('menu', sa.Column('sort', sa.Integer(), nullable=True, comment='菜单排序'))
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_column('menu', 'sort')
- # ### end Alembic commands ###
diff --git "a/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" "b/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py"
deleted file mode 100644
index 1f582ba..0000000
--- "a/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py"
+++ /dev/null
@@ -1,37 +0,0 @@
-"""更新菜单排序字段
-
-Revision ID: b703662d9cf9
-Revises: a5e2cea226d4
-Create Date: 2022-10-31 10:22:22.613017
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlmodel
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'b703662d9cf9'
-down_revision = 'a5e2cea226d4'
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.alter_column('menu', 'sort',
- existing_type=mysql.INTEGER(),
- type_=sa.Float(),
- existing_comment='菜单排序',
- existing_nullable=True)
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.alter_column('menu', 'sort',
- existing_type=sa.Float(),
- type_=mysql.INTEGER(),
- existing_comment='菜单排序',
- existing_nullable=True)
- # ### end Alembic commands ###
diff --git a/server/common/response_code.py b/server/common/response_code.py
index 24497eb..30584ee 100644
--- a/server/common/response_code.py
+++ b/server/common/response_code.py
@@ -1,8 +1,9 @@
-from typing import Generic, TypeVar, Optional
+from typing import Generic, TypeVar, Optional, List
from pydantic import Field
from pydantic.generics import GenericModel
T = TypeVar("T")
+DATA = TypeVar("DATA")
class ApiResponse(GenericModel, Generic[T]):
@@ -12,3 +13,8 @@ class ApiResponse(GenericModel, Generic[T]):
code: int = Field(default=200, description="返回码")
message: str = Field(default="success", description="消息内容")
data: Optional[T]
+
+
+class SearchResponse(GenericModel, Generic[DATA]):
+ total: int
+ data: List[DATA] = []
diff --git a/server/crud/base.py b/server/crud/base.py
index b02521a..2f3aedb 100644
--- a/server/crud/base.py
+++ b/server/crud/base.py
@@ -42,19 +42,19 @@ def delete(self, db: Session, id: int):
db.commit()
return obj
- def _make_search(self, sql, search: Optional[Dict[str, Any]] = None):
+ def _make_search(self, sql, search: Optional[Dict[str, Any]] = None, filter_type: Optional[Dict[str, str]] = None):
"""
用于构建专用的sql查询语句,子类需要重写此方法
:param sql:
:param search:
+ :param filter_type:指定的各属性值判断形式
:return:
"""
if search is None:
return sql
q = deepcopy(search)
- filter_type: Dict[str, Any] = q.pop('type', None)
for key in q:
- if (q[key] is not None) and (q[key]):
+ if (key in filter_type.keys()) and (q[key] is not None):
if filter_type[key] == 'l_like':
sql = sql.where(getattr(self.model, key).like(f'%{q[key]}'))
elif filter_type[key] == 'r_like':
@@ -75,11 +75,13 @@ def _make_search(self, sql, search: Optional[Dict[str, Any]] = None):
sql = sql.where(getattr(self.model, key) >= q[key])
return sql
- def search(self, session: Session, search: Pagination, columns: Optional[List] = None):
+ def search(self, session: Session, search: Pagination, filter_type: Optional[Dict[str, str]] = None,
+ columns: Optional[List] = None):
"""
分页查询方法
:param session:
:param search: Pagination实例对象,包含各搜索参数
+ :param filter_type: 指定的各属性值判断形式
:param columns: 查询返回指定columns
:return:
"""
@@ -87,9 +89,9 @@ def search(self, session: Session, search: Pagination, columns: Optional[List] =
sql = select(self.model)
else:
sql = select(*columns)
- sql = self._make_search(sql, search.search)
+ sql = self._make_search(sql, search.search, filter_type)
subquery = select(self.model.id)
- subquery = self._make_search(subquery, search.search)
+ subquery = self._make_search(subquery, search.search, filter_type)
if search.model == 'desc':
subquery = subquery.order_by(desc(self.model.id))
else:
@@ -104,14 +106,15 @@ def search(self, session: Session, search: Pagination, columns: Optional[List] =
results = session.exec(sql).all()
return results
- def search_total(self, session: Session, q: Dict[str, Any]):
+ def search_total(self, session: Session, q: Dict[str, Any], filter_type: Optional[Dict[str, str]] = None):
"""
每次进行分页查询的时候,都需要返回一个total值,表示对应搜索,现阶段数据库有多少内容,便于前端分页数
:param session:
:param q:
+ :param filter_type: 字段过滤形式
:return:
"""
sql = select(func.count(self.model.id))
- sql = self._make_search(sql, q)
- print(sql)
+ sql = self._make_search(sql, q, filter_type)
+ print(str(sql))
return session.execute(sql).scalar()
diff --git a/server/crud/internal/__init__.py b/server/crud/internal/__init__.py
index a7c086d..51bdf03 100644
--- a/server/crud/internal/__init__.py
+++ b/server/crud/internal/__init__.py
@@ -1,3 +1,4 @@
from .user import user
from .roles import role
from .menu import menu
+from .dictonary import data_dict, dict_item
diff --git a/server/crud/internal/dictonary.py b/server/crud/internal/dictonary.py
new file mode 100644
index 0000000..21b1e9c
--- /dev/null
+++ b/server/crud/internal/dictonary.py
@@ -0,0 +1,22 @@
+from typing import Union
+from sqlmodel import select, Session
+from ...models.internal.dictonary import DataDict, DictItem
+from ..base import CRUDBase
+from ...schemas.internal.user import UserInfo, UserLogin
+from .roles import role
+
+
+class CRUDDict(CRUDBase[DataDict]):
+ pass
+
+
+class CRUDItem(CRUDBase[DictItem]):
+ def get_items_by_code(self, db: Session, code: str):
+ dict_id = select(DataDict.id).where(DataDict.code == code).subquery()
+ sql = select(self.model).where(self.model.dict_id == dict_id).where(self.model.enable == 1).order_by(
+ self.model.id)
+ return db.exec(sql).all()
+
+
+data_dict = CRUDDict(DataDict)
+dict_item = CRUDItem(DictItem)
diff --git a/server/crud/internal/menu.py b/server/crud/internal/menu.py
index 1429fbb..10d1ddd 100644
--- a/server/crud/internal/menu.py
+++ b/server/crud/internal/menu.py
@@ -5,10 +5,8 @@
class CRUDMenu(CRUDBase[Menu]):
- def search(self, session: Session, q: Optional = None) -> List[Menu]:
+ def search_menus(self, session: Session) -> List[Menu]:
sql = select(self.model)
- if q is not None:
- sql = sql.where(self.model.name.like(f'%{q}%'))
sql = sql.order_by(self.model.sort)
return session.exec(sql).all()
diff --git a/server/crud/internal/user.py b/server/crud/internal/user.py
index 64744f9..b16d154 100644
--- a/server/crud/internal/user.py
+++ b/server/crud/internal/user.py
@@ -17,12 +17,6 @@ def check_name(self, session: Session, name: str):
sql = select(self.model).where(self.model.name == name)
return session.exec(sql).one()
- # 重写父类的查询构建命令
- def _make_search(self, sql, q: Union[int, str]):
- if q is not None:
- sql = sql.where(self.model.name.like(f'%{q}%'))
- return sql
-
def insert(self, session: Session, user_info: UserInfo) -> User:
updated_user = User(**user_info.user.dict())
user_roles = role.get_roles_by_id(session, user_info.roles)
diff --git a/server/main.py b/server/main.py
index a7b9bbf..7fca15c 100644
--- a/server/main.py
+++ b/server/main.py
@@ -1,6 +1,6 @@
from sqlmodel import Session
from fastapi import FastAPI, Depends
-from .routers.internal import login, user, menu, roles
+from .routers.internal import login, user, menu, roles, dictonary
from .common.security import auth_check
from .settings import engine
@@ -11,6 +11,7 @@
app.include_router(user.router, tags=['用户管理'])
app.include_router(menu.router, tags=['菜单管理'])
app.include_router(roles.router, tags=['角色管理'])
+app.include_router(dictonary.router, tags=['数据字典'])
@app.on_event("startup")
diff --git a/server/models/internal/__init__.py b/server/models/internal/__init__.py
index efb0daf..95cd37a 100644
--- a/server/models/internal/__init__.py
+++ b/server/models/internal/__init__.py
@@ -1,3 +1,4 @@
from .user import User
from .menu import Menu
from .role import Role, RoleMenu
+from .dictonary import DataDict, DictItem
diff --git a/server/models/internal/dictonary.py b/server/models/internal/dictonary.py
new file mode 100644
index 0000000..1b8da49
--- /dev/null
+++ b/server/models/internal/dictonary.py
@@ -0,0 +1,57 @@
+from typing import Optional, List
+from sqlmodel import SQLModel, Field, Relationship, Column, Boolean, Integer
+
+
+class DataDictBase(SQLModel):
+ name: str = Field(max_length=50, sa_column_kwargs={'comment': '字典名称'})
+ code: str = Field(max_length=100, sa_column_kwargs={'comment': '字典编号'})
+ desc: Optional[str] = Field(max_length=100, sa_column_kwargs={'comment': '描述'})
+
+
+class DataDict(DataDictBase, table=True):
+ __tablename__ = 'data_dict'
+ id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
+ dict_items: List["DictItem"] = Relationship(back_populates="dict")
+
+
+class DataDictSearch(SQLModel):
+ name: Optional[str]
+ code: Optional[str]
+
+
+class DictBase(SQLModel):
+ label: str = Field(max_length=50, sa_column_kwargs={'comment': '名称'})
+ value: str = Field(max_length=100, sa_column_kwargs={'comment': '数据值'})
+ desc: Optional[str] = Field(max_length=100, sa_column_kwargs={'comment': '描述'})
+ sort: Optional[int] = Field(sa_column_kwargs={'comment': '排序值,越小越靠前'})
+ enable: bool = Field(default=True, sa_column=Column(Boolean, comment='是否启用'))
+ dict_id: Optional[int] = Field(foreign_key="data_dict.id")
+
+
+class DictItem(DictBase, table=True):
+ __tablename__ = 'dict_item'
+
+ id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
+ dict: Optional[DataDict] = Relationship(back_populates='dict_items')
+
+
+class DictRead(DictBase):
+ id: int
+
+
+class DictUpdate(DictBase):
+ id: Optional[int]
+
+
+class DictItemSearch(SQLModel):
+ dict_id: int
+ name: Optional[str]
+ data: Optional[str]
+ enable: Optional[bool]
+
+
+class DictItemSearchFilter(SQLModel):
+ dict_id: str
+ label: str
+ value: str
+ enable: str
diff --git a/server/routers/internal/dictonary.py b/server/routers/internal/dictonary.py
new file mode 100644
index 0000000..c622c4f
--- /dev/null
+++ b/server/routers/internal/dictonary.py
@@ -0,0 +1,78 @@
+from typing import Optional, List
+from fastapi import APIRouter, Depends, status, HTTPException
+from sqlmodel import Session
+from ...schemas.internal.pagination import Pagination
+from ...models.internal.dictonary import DataDict, DictRead, DictUpdate, DictItem, DataDictSearch, \
+ DictItemSearch, DictItemSearchFilter
+from ...common.response_code import ApiResponse, SearchResponse
+from ...common.database import get_session
+from ... import crud
+
+router = APIRouter(prefix='/api')
+
+
+@router.post('/dict/item/search', summary="字典列表查询", response_model=ApiResponse[SearchResponse[DictRead]])
+async def search_items(search: Pagination[DictItemSearch], session: Session = Depends(get_session)):
+ filter_type = DictItemSearchFilter(dict_id='eq', label='like', enable='eq', value='like')
+ total = crud.internal.dict_item.search_total(session, search.search, filter_type.dict())
+ items: List[DictRead] = crud.internal.dict_item.search(session, search, filter_type.dict())
+ item_list = [DictRead.from_orm(item) for item in items]
+ return ApiResponse(
+ data={
+ 'total': total,
+ 'data': item_list
+ }
+
+ )
+
+
+@router.post('/dict/item', summary="添加字典字段", response_model=ApiResponse[DictRead])
+async def add_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)):
+ new_item = crud.internal.dict_item.insert(session, DictItem(**dict_item.dict()))
+ return ApiResponse(
+ data=DictRead.from_orm(new_item)
+ )
+
+
+@router.put('/dict/item', summary="更新字典元素", response_model=ApiResponse)
+async def update_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)):
+ db_obj = crud.internal.dict_item.get(session, dict_item.id)
+ crud.internal.dict_item.update(session, db_obj, dict_item)
+ return ApiResponse()
+
+
+@router.delete('/dict/item/{item_id}', summary="删除字典元素", )
+async def del_dict_item(item_id: int, session: Session = Depends(get_session)):
+ crud.internal.dict_item.delete(session, item_id)
+ return ApiResponse()
+
+
+@router.get("/dict/{dict_code}", summary="获取数据字典", response_model=ApiResponse[List[DictRead]],
+ response_model_exclude={'data': {'__all__': {'desc', 'sort', 'enable'}}})
+async def get_dict(dict_code: str, session: Session = Depends(get_session)):
+ dict_items: List[DictItem] = crud.internal.dict_item.get_items_by_code(session, dict_code)
+ return ApiResponse(
+ data=[DictRead.from_orm(item) for item in dict_items]
+ )
+
+
+@router.post("/dict", summary="新建数据字典", response_model=ApiResponse[DataDict])
+async def add_dict(data_dict: DataDict, session: Session = Depends(get_session)):
+ obj = crud.internal.data_dict.insert(session, data_dict)
+ return ApiResponse(
+ data=obj
+ )
+
+
+@router.post('/dict/search',
+ summary="查询数据字典")
+async def get_dicts(search: Pagination[DataDictSearch], session: Session = Depends(get_session)):
+ filter_type = DataDictSearch(name='like', code='like')
+ total = crud.internal.data_dict.search_total(session, search.search, filter_type.dict())
+ dicts: List[DataDict] = crud.internal.data_dict.search(session, search, filter_type.dict())
+ return ApiResponse(
+ data={
+ 'total': total,
+ 'data': dicts
+ }
+ )
diff --git a/server/routers/internal/menu.py b/server/routers/internal/menu.py
index cf912be..268bdca 100644
--- a/server/routers/internal/menu.py
+++ b/server/routers/internal/menu.py
@@ -12,10 +12,10 @@
router = APIRouter(prefix='/api')
-@router.get('/menus', summary="查询菜单", response_model=ApiResponse[List[MenusWithChild]])
-async def get_all_menu(q: Optional[str] = None, session: Session = Depends(get_session)):
+@router.get('/menus', summary="列出菜单", response_model=ApiResponse[List[MenusWithChild]])
+async def get_all_menu(session: Session = Depends(get_session)):
# 复用crud.get_menu_list,默认role为admin就是返回所有的菜单列表
- menu_list: List[Menu] = crud.menu.search(session, q)
+ menu_list: List[Menu] = crud.menu.search_menus(session)
user_menus = utils.menu_convert(menu_list)
return ApiResponse(
data=user_menus
diff --git a/server/routers/internal/roles.py b/server/routers/internal/roles.py
index 64f00e6..5b90e4c 100644
--- a/server/routers/internal/roles.py
+++ b/server/routers/internal/roles.py
@@ -7,9 +7,8 @@
from ...common.database import get_session
from ... import crud
from ...models.internal import Role, Menu
-from ...models.internal.role import RoleWithMenus, RoleInsert, RoleUpdate
+from ...models.internal.role import RoleBase, RoleWithMenus, RoleInsert, RoleUpdate
from ...schemas.internal.pagination import Pagination
-from ...schemas.internal.roles import RoleSearch
from ...common.utils import menu_convert
router = APIRouter(prefix='/api')
@@ -37,10 +36,10 @@ async def get_role_menus(id: Optional[int] = None, session: Session = Depends(ge
@router.post('/roles/search',
summary="查询角色")
-async def get_roles(search: Pagination[RoleSearch], session: Session = Depends(get_session)):
- total = crud.internal.role.search_total(session, search.search)
+async def get_roles(search: Pagination[RoleBase], session: Session = Depends(get_session)):
+ total = crud.internal.role.search_total(session, search.search, {'name': 'like', 'enable': 'eq'})
print(total)
- roles: List[Role] = crud.internal.role.search(session, search)
+ roles: List[Role] = crud.internal.role.search(session, search, {'name': 'like', 'enable': 'eq'})
role_with_menus: List[RoleWithMenus] = []
for role in roles:
new_role = RoleWithMenus(**role.dict(), menus=role.menus)
diff --git a/server/routers/internal/user.py b/server/routers/internal/user.py
index 2ea1814..4605bc2 100644
--- a/server/routers/internal/user.py
+++ b/server/routers/internal/user.py
@@ -65,9 +65,9 @@ async def get_all_user(search: Pagination[UserWithOutPasswd],
:param session:
:return:
"""
- total = crud.internal.user.search_total(session, search.search)
+ total = crud.internal.user.search_total(session, search.search, {'name': 'like', 'enable': 'eq'})
print(total)
- users = crud.internal.user.search(session, search)
+ users = crud.internal.user.search(session, search, {'name': 'like', 'enable': 'eq'})
users_list = [user.dict(exclude={"password"}) for user in users]
print(users_list)
return ApiResponse(
@@ -97,11 +97,11 @@ async def update_user(user_info: UserCreateWithRoles, session: Session = Depends
@router.put('/users/password', summary='重置密码', dependencies=[Depends(Authority('user:reset'))])
async def update_password(user: UserUpdatePassword, session: Session = Depends(get_session)):
crud.internal.user.update_passwd(session, uid=user.id, passwd=user.password)
+ return ApiResponse()
@router.put('/users/{uid}',
- summary='更新用户', dependencies=[Depends(Authority("user:update"))],
- status_code=status.HTTP_204_NO_CONTENT)
+ summary='更新用户', dependencies=[Depends(Authority("user:update"))])
async def update_user(uid: int, user_info: UserUpdateWithRoles, session: Session = Depends(get_session)):
"""
更新用户信息的所有操作,可涉及更新用户名、密码、角色等
@@ -110,18 +110,16 @@ async def update_user(uid: int, user_info: UserUpdateWithRoles, session: Session
:param session:
:return:
"""
- print('update...')
print(user_info.dict(exclude_unset=True, exclude_none=True))
user = crud.internal.user.update(session, uid, user_info)
new_roles = [role.id for role in user.roles]
casbin_enforcer.delete_roles_for_user(f'uid_{user.id}')
for role in new_roles:
casbin_enforcer.add_role_for_user(f'uid_{user.id}', f'role_{role}')
- return user
+ return ApiResponse()
-@router.delete('/users/{uid}', summary='删除用户', dependencies=[Depends(Authority("user:del"))],
- status_code=status.HTTP_204_NO_CONTENT)
+@router.delete('/users/{uid}', summary='删除用户', dependencies=[Depends(Authority("user:del"))])
async def delete_user(uid: int, session: Session = Depends(get_session)):
try:
user = session.exec(select(User).where(User.id == uid)).one()
@@ -130,3 +128,4 @@ async def delete_user(uid: int, session: Session = Depends(get_session)):
casbin_enforcer.delete_roles_for_user(f'uid_{user.id}')
session.delete(user)
session.commit()
+ return ApiResponse()
diff --git a/server/schemas/internal/dictonary.py b/server/schemas/internal/dictonary.py
new file mode 100644
index 0000000..9a548ef
--- /dev/null
+++ b/server/schemas/internal/dictonary.py
@@ -0,0 +1,16 @@
+from typing import TypeVar, Generic
+from pydantic import BaseModel
+from .pagination import SearchBase
+from ...models.internal.dictonary import DataDict, DictItem, DictBase
+
+
+class DictSearch(DataDict, SearchBase):
+ pass
+
+
+class DictItemSearch(DictItem, SearchBase):
+ pass
+
+
+class DictItem(DictBase):
+ dict_id: int
diff --git a/server/schemas/internal/pagination.py b/server/schemas/internal/pagination.py
index af0a638..d26e106 100644
--- a/server/schemas/internal/pagination.py
+++ b/server/schemas/internal/pagination.py
@@ -1,4 +1,4 @@
-from typing import Optional, Generic, TypeVar
+from typing import Optional, Generic, TypeVar, Dict
from pydantic import BaseModel
T = TypeVar('T')
@@ -9,3 +9,7 @@ class Pagination(BaseModel, Generic[T]):
page: Optional[int] = 1
page_size: Optional[int] = 10
model: Optional[str] = 'asc'
+
+
+class SearchBase(BaseModel):
+ type: Dict[str, str]
diff --git a/server/schemas/internal/roles.py b/server/schemas/internal/roles.py
deleted file mode 100644
index bd6647d..0000000
--- a/server/schemas/internal/roles.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from typing import Dict
-from ...models.internal.role import RoleBase
-
-
-class RoleSearch(RoleBase):
- type: Dict[str, str]
\ No newline at end of file
diff --git a/server/schemas/internal/sysapi.py b/server/schemas/internal/sysapi.py
deleted file mode 100644
index b402b3d..0000000
--- a/server/schemas/internal/sysapi.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from typing import Dict
-from ...models.internal.api import ApiBase
-
-
-class ApiSearch(ApiBase):
- type: Dict[str, str]
diff --git a/www/src/api/dictonary.js b/www/src/api/dictonary.js
new file mode 100644
index 0000000..0ccaf58
--- /dev/null
+++ b/www/src/api/dictonary.js
@@ -0,0 +1,7 @@
+import {GET, POST, PUT, DELETE} from '@/utils/request'
+
+export const PostNewDict = (dict) => POST('/api/dict', dict)
+export const PostNewDictItem = (item) => POST('/api/dict/item', item)
+export const PutDictItem = (item) => PUT('/api/dict/item', item)
+export const DelDictItem = (id) => DELETE('/api/dict/item/' + id)
+export const GetDictItems = (code) => GET('/api/dict/'+ code)
diff --git a/www/src/api/menus.js b/www/src/api/menus.js
index 9f34511..25935d0 100644
--- a/www/src/api/menus.js
+++ b/www/src/api/menus.js
@@ -1,8 +1,7 @@
import { GET, POST, PUT, DELETE } from '@/utils/request'
// 菜单接口
-export const GetAllMenus = (q) => GET('/api/menus', { q })
+export const GetAllMenus = () => GET('/api/menus')
export const PostNewMenu = (menu) => POST('/api/menus', menu )
export const PutMenu = (menu) => PUT('/api/menus', menu)
-export const DeleteMenu = (id) => DELETE('/api/menus/' + id)
-export const GetMenuApis = (id) => GET('/api/menus/' + id + '/apis')
\ No newline at end of file
+export const DeleteMenu = (id) => DELETE('/api/menus/' + id)
\ No newline at end of file
diff --git a/www/src/components/AutoDict.vue b/www/src/components/AutoDict.vue
new file mode 100644
index 0000000..01987a5
--- /dev/null
+++ b/www/src/components/AutoDict.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/www/src/views/system/dictonary/AddDict.vue b/www/src/views/system/dictonary/AddDict.vue
new file mode 100644
index 0000000..bc3d3a9
--- /dev/null
+++ b/www/src/views/system/dictonary/AddDict.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+ 确定
+
+
+
+
+
\ No newline at end of file
diff --git a/www/src/views/system/dictonary/AddItem.vue b/www/src/views/system/dictonary/AddItem.vue
new file mode 100644
index 0000000..a39c715
--- /dev/null
+++ b/www/src/views/system/dictonary/AddItem.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+ 确定
+
+
+
+
+
\ No newline at end of file
diff --git a/www/src/views/system/dictonary/DictItem.vue b/www/src/views/system/dictonary/DictItem.vue
new file mode 100644
index 0000000..7fd4a5d
--- /dev/null
+++ b/www/src/views/system/dictonary/DictItem.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+ 重置
+
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/www/src/views/system/dictonary/index.vue b/www/src/views/system/dictonary/index.vue
new file mode 100644
index 0000000..2ced786
--- /dev/null
+++ b/www/src/views/system/dictonary/index.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ 重置
+
+
+
+
+
+ 添加
+ 导出
+ 导入
+ 刷新缓存
+ 回收站
+
+
+
+
+
+
+
+
+
+ 操作
+
+
+ 字典配置
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/www/src/views/system/menus/ButtonForm.vue b/www/src/views/system/menus/ButtonForm.vue
index 010527b..90e18a4 100644
--- a/www/src/views/system/menus/ButtonForm.vue
+++ b/www/src/views/system/menus/ButtonForm.vue
@@ -12,16 +12,14 @@
-
- 禁用
- 启用
-
+