Merge pull request #39 from mizhexiaoxiao/dev

Dev
This commit is contained in:
mizhexiaoxiao 2024-07-31 14:58:54 +08:00 committed by GitHub
commit fd72748258
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 4180 additions and 2520 deletions

View File

@ -2,8 +2,7 @@ FROM node:18.12.0-alpine3.16 as web
WORKDIR /opt/vue-fastapi-admin
COPY /web ./web
RUN cd /opt/vue-fastapi-admin/web && npm i -g pnpm --registry=https://registry.npmmirror.com \
&& pnpm i --registry=https://registry.npmmirror.com && pnpm run build
RUN cd /opt/vue-fastapi-admin/web && npm i --registry=https://registry.npmmirror.com && npm run build
FROM python:3.11-slim-bullseye

View File

@ -190,7 +190,20 @@ pnpm dev
你可以在群里提出任何疑问,我会尽快回复答疑。
<img alt="Vue FastAPI Admin Logo" width="400" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/group.jpg">
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/group.jpg">
## 打赏
如果项目有帮助到你,可以请作者喝杯咖啡~
<div style="display: flex">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/1.jpg" width="300">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/2.jpg" width="300">
</div>
## 定制开发
如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/3.jpg">
### Visitors Count

View File

@ -8,6 +8,7 @@ from .depts import depts_router
from .menus import menus_router
from .roles import roles_router
from .users import users_router
from .auditlog import auditlog_router
v1_router = APIRouter()
@ -17,3 +18,4 @@ v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermi
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson])
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermisson])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermisson])

View File

@ -1,10 +1,7 @@
from fastapi import APIRouter, Query
from fastapi.routing import APIRoute
from tortoise.expressions import Q
from app.controllers.api import api_controller
from app.log import logger
from app.models.admin import Api
from app.schemas import Success, SuccessExtra
from app.schemas.apis import *
@ -66,33 +63,5 @@ async def delete_api(
@router.post("/refresh", summary="刷新API列表")
async def refresh_api():
from app import app
# 删除废弃API数据
all_api_list = []
for route in app.routes:
if isinstance(route, APIRoute):
all_api_list.append((list(route.methods)[0], route.path_format))
delete_api = []
for api in await Api.all():
if (api.method, api.path) not in all_api_list:
delete_api.append((api.method, api.path))
for item in delete_api:
method, path = item
logger.debug(f"API Deleted {method} {path}")
await Api.filter(method=method, path=path).delete()
for route in app.routes:
if isinstance(route, APIRoute):
method = list(route.methods)[0]
path = route.path_format
summary = route.summary
tags = list(route.tags)[0]
api_obj = await Api.filter(method=method, path=path).first()
if api_obj:
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
else:
logger.debug(f"API Created {method} {path}")
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
await api_controller.refresh_api()
return Success(msg="OK")

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .auditlog import router
auditlog_router = APIRouter()
auditlog_router.include_router(router, tags=["审计日志模块"])
__all__ = ["auditlog_router"]

View File

@ -0,0 +1,39 @@
from fastapi import APIRouter, Query
from tortoise.expressions import Q
from app.models.admin import AuditLog
from app.schemas import SuccessExtra
from app.schemas.apis import *
from app.core.dependency import DependPermisson
router = APIRouter()
@router.get('/list', summary="查看操作日志", dependencies=[DependPermisson])
async def get_audit_log_list(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
username: str = Query("", description="操作人名称"),
module: str = Query("", description="功能模块"),
summary: str = Query("", description="接口描述"),
start_time: str = Query("", description="开始时间"),
end_time: str = Query("", description="结束时间"),
):
q = Q()
if username:
q &= Q(username__icontains=username)
if module:
q &= Q(module__icontains=module)
if summary:
q &= Q(summary__icontains=summary)
if start_time and end_time:
q &= Q(created_at__range=[start_time, end_time])
elif start_time:
q &= Q(created_at__gte=start_time)
elif end_time:
q &= Q(created_at__lte=end_time)
audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at")
total = await AuditLog.filter(q).count()
data = [await audit_log.to_dict() for audit_log in audit_log_objs]
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)

View File

@ -1,8 +1,8 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter
from app.controllers.user import UserController, user_controller
from app.controllers.user import user_controller
from app.core.ctx import CTX_USER_ID
from app.core.dependency import DependAuth
from app.models.admin import Api, Menu, Role, User
@ -21,7 +21,7 @@ async def login_access_token(credentials: CredentialsSchema):
user: User = await user_controller.authenticate(credentials)
await user_controller.update_last_login(user.id)
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + access_token_expires
expire = datetime.now(timezone.utc) + access_token_expires
data = JWTOut(
access_token=create_access_token(
@ -91,10 +91,10 @@ async def get_user_api():
return Success(data=apis)
@router.post("/update_password", summary="更新用户密码", dependencies=[DependAuth])
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
async def update_user_password(req_in: UpdatePassword):
user_controller = UserController()
user = await user_controller.get(req_in.id)
user_id = CTX_USER_ID.get()
user = await user_controller.get(user_id)
verified = verify_password(req_in.old_password, user.password)
if not verified:
return Fail(msg="旧密码验证错误!")

View File

@ -1,12 +1,11 @@
import logging
from fastapi import APIRouter, Query
from fastapi.exceptions import HTTPException
from fastapi import APIRouter, Body, Query
from tortoise.expressions import Q
from app.controllers.dept import dept_controller
from app.controllers.user import UserController
from app.schemas.base import Success, SuccessExtra
from app.controllers.user import user_controller
from app.schemas.base import Fail, Success, SuccessExtra
from app.schemas.users import *
logger = logging.getLogger(__name__)
@ -22,7 +21,6 @@ async def list_user(
email: str = Query("", description="邮箱地址"),
dept_id: int = Query(None, description="部门ID"),
):
user_controller = UserController()
q = Q()
if username:
q &= Q(username__contains=username)
@ -43,7 +41,6 @@ async def list_user(
async def get_user(
user_id: int = Query(..., description="用户ID"),
):
user_controller = UserController()
user_obj = await user_controller.get(id=user_id)
user_dict = await user_obj.to_dict(exclude_fields=["password"])
return Success(data=user_dict)
@ -53,14 +50,10 @@ async def get_user(
async def create_user(
user_in: UserCreate,
):
user_controller = UserController()
user = await user_controller.get_by_email(user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
new_user = await user_controller.create(obj_in=user_in)
return Fail(code=400, msg="The user with this email already exists in the system.")
new_user = await user_controller.create_user(obj_in=user_in)
await user_controller.update_roles(new_user, user_in.role_ids)
return Success(msg="Created Successfully")
@ -69,8 +62,7 @@ async def create_user(
async def update_user(
user_in: UserUpdate,
):
user_controller = UserController()
user = await user_controller.update(obj_in=user_in)
user = await user_controller.update(id=user_in.id, obj_in=user_in)
await user_controller.update_roles(user, user_in.role_ids)
return Success(msg="Updated Successfully")
@ -79,6 +71,11 @@ async def update_user(
async def delete_user(
user_id: int = Query(..., description="用户ID"),
):
user_controller = UserController()
await user_controller.remove(id=user_id)
return Success(msg="Deleted Successfully")
@router.post("/reset_password", summary="重置密码")
async def reset_password(user_id: int = Body(..., description="用户ID")):
await user_controller.reset_password(user_id)
return Success(msg="密码已重置为123456")

View File

@ -1,4 +1,7 @@
from fastapi.routing import APIRoute
from app.core.crud import CRUDBase
from app.log import logger
from app.models.admin import Api
from app.schemas.apis import ApiCreate, ApiUpdate
@ -7,5 +10,35 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
def __init__(self):
super().__init__(model=Api)
async def refresh_api(self):
from app import app
# 删除废弃API数据
all_api_list = []
for route in app.routes:
if isinstance(route, APIRoute):
all_api_list.append((list(route.methods)[0], route.path_format))
delete_api = []
for api in await Api.all():
if (api.method, api.path) not in all_api_list:
delete_api.append((api.method, api.path))
for item in delete_api:
method, path = item
logger.debug(f"API Deleted {method} {path}")
await Api.filter(method=method, path=path).delete()
for route in app.routes:
if isinstance(route, APIRoute):
method = list(route.methods)[0]
path = route.path_format
summary = route.summary
tags = list(route.tags)[0]
api_obj = await Api.filter(method=method, path=path).first()
if api_obj:
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
else:
logger.debug(f"API Created {method} {path}")
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
api_controller = ApiController()

View File

@ -22,14 +22,11 @@ class UserController(CRUDBase[User, UserCreate, UserUpdate]):
async def get_by_username(self, username: str) -> Optional[User]:
return await self.model.filter(username=username).first()
async def create(self, obj_in: UserCreate) -> User:
async def create_user(self, obj_in: UserCreate) -> User:
obj_in.password = get_password_hash(password=obj_in.password)
obj = await super().create(obj_in.create_dict())
obj = await self.create(obj_in)
return obj
async def update(self, obj_in: UserUpdate) -> User:
return await super().update(id=obj_in.id, obj_in=obj_in)
async def update_last_login(self, id: int) -> None:
user = await self.model.get(id=id)
user.last_login = datetime.now()
@ -52,5 +49,12 @@ class UserController(CRUDBase[User, UserCreate, UserUpdate]):
role_obj = await role_controller.get(id=role_id)
await user.roles.add(role_obj)
async def reset_password(self, user_id: int):
user_obj = await self.get(id=user_id)
if user_obj.is_superuser:
raise HTTPException(status_code=403, detail="不允许重置超级管理员密码")
user_obj.password = get_password_hash(password="123456")
await user_obj.save()
user_controller = UserController()

View File

@ -20,7 +20,7 @@ from app.models.admin import Menu
from app.schemas.menus import MenuType
from app.settings.config import settings
from .middlewares import BackGroundTaskMiddleware
from .middlewares import BackGroundTaskMiddleware, HttpAuditLogMiddleware
def make_middlewares():
@ -33,6 +33,14 @@ def make_middlewares():
allow_headers=settings.CORS_ALLOW_HEADERS,
),
Middleware(BackGroundTaskMiddleware),
Middleware(
HttpAuditLogMiddleware,
methods=["GET", "POST", "PUT", "DELETE"],
exclude_paths=[
"/docs",
"/openapi.json",
],
),
]
return middleware
@ -52,7 +60,7 @@ def register_routers(app: FastAPI, prefix: str = "/api"):
async def init_superuser():
user = await user_controller.model.exists()
if not user:
await user_controller.create(
await user_controller.create_user(
UserCreate(
username="admin",
email="admin@admin.com",
@ -75,7 +83,7 @@ async def init_menus():
icon="carbon:gui-management",
is_hidden=False,
component="Layout",
keepalive=True,
keepalive=False,
redirect="/system/user",
)
children_menu = [
@ -88,7 +96,7 @@ async def init_menus():
icon="material-symbols:person-outline-rounded",
is_hidden=False,
component="/system/user",
keepalive=True,
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
@ -99,7 +107,7 @@ async def init_menus():
icon="carbon:user-role",
is_hidden=False,
component="/system/role",
keepalive=True,
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
@ -110,7 +118,7 @@ async def init_menus():
icon="material-symbols:list-alt-outline",
is_hidden=False,
component="/system/menu",
keepalive=True,
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
@ -121,7 +129,7 @@ async def init_menus():
icon="ant-design:api-outlined",
is_hidden=False,
component="/system/api",
keepalive=True,
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
@ -132,8 +140,19 @@ async def init_menus():
icon="mingcute:department-line",
is_hidden=False,
component="/system/dept",
keepalive=True,
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="审计日志",
path="auditlog",
order=6,
parent_id=parent_menu.id,
icon="ph:clipboard-text-bold",
is_hidden=False,
component="/system/auditlog",
keepalive=False,
)
]
await Menu.bulk_create(children_menu)
parent_menu = await Menu.create(
@ -145,7 +164,7 @@ async def init_menus():
icon="mdi-fan-speed-1",
is_hidden=False,
component="Layout",
keepalive=True,
keepalive=False,
redirect="",
)
await Menu.create(
@ -157,5 +176,5 @@ async def init_menus():
icon="mdi-fan-speed-1",
is_hidden=False,
component="/top-menu",
keepalive=True,
keepalive=False,
)

View File

@ -1,6 +1,16 @@
import re
from datetime import datetime
from fastapi import FastAPI
from fastapi.responses import Response
from fastapi.routing import APIRoute
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send
from app.core.dependency import AuthControl
from app.models.admin import AuditLog, User
from .bgtask import BgTasks
@ -32,3 +42,55 @@ class BackGroundTaskMiddleware(SimpleBaseMiddleware):
async def after_request(self, request):
await BgTasks.execute_tasks()
class HttpAuditLogMiddleware(BaseHTTPMiddleware):
def __init__(self, app, methods: list, exclude_paths: list):
super().__init__(app)
self.methods = methods
self.exclude_paths = exclude_paths
async def get_request_log(self, request: Request, response: Response) -> dict:
"""
根据request和response对象获取对应的日志记录数据
"""
data: dict = {"path": request.url.path, "status": response.status_code, "method": request.method}
# 路由信息
app: FastAPI = request.app
for route in app.routes:
if (
isinstance(route, APIRoute)
and route.path_regex.match(request.url.path)
and request.method in route.methods
):
data["module"] = ",".join(route.tags)
data["summary"] = route.summary
# 获取用户信息
token = request.headers.get("token")
user_obj = None
if token:
user_obj: User = await AuthControl.is_authed(token)
data["user_id"] = user_obj.id if user_obj else 0
data["username"] = user_obj.username if user_obj else ""
return data
async def before_request(self, request: Request):
pass
async def after_request(self, request: Request, response: Response, process_time: int):
if request.method in self.methods: # 请求方法为配置的记录方法
for path in self.exclude_paths:
if re.search(path, request.url.path, re.I) is not None:
return
data: dict = await self.get_request_log(request=request, response=response)
data["response_time"] = process_time # 响应时间
await AuditLog.create(**data)
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
start_time: datetime = datetime.now()
await self.before_request(request)
response = await call_next(request)
end_time: datetime = datetime.now()
process_time = int((end_time.timestamp() - start_time.timestamp()) * 1000)
await self.after_request(request, response, process_time)
return response

View File

@ -79,3 +79,14 @@ class DeptClosure(BaseModel, TimestampMixin):
ancestor = fields.IntField(description="父代")
descendant = fields.IntField(description="子代")
level = fields.IntField(default=0, description="深度")
class AuditLog(BaseModel, TimestampMixin):
user_id = fields.IntField(description="用户ID", index=True)
username = fields.CharField(max_length=64, default="", description="用户名称", index=True)
module = fields.CharField(max_length=64, default="", description="功能模块", index=True)
summary = fields.CharField(max_length=128, default="", description="请求描述", index=True)
method = fields.CharField(max_length=10, default="", description="请求方法", index=True)
path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
status = fields.IntField(default=-1, description="状态码", index=True)
response_time = fields.IntField(default=0, description="响应时间(单位ms)")

View File

@ -40,6 +40,5 @@ class UserUpdate(BaseModel):
class UpdatePassword(BaseModel):
id: int = Field(description="用户ID")
old_password: str = Field(description="旧密码")
new_password: str = Field(description="新密码")

View File

@ -36,7 +36,7 @@ class Settings(BaseSettings):
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 24 * 7 # 7 day
TORTOISE_ORM: dict = {
"connections": {
"sqlite": {

BIN
deploy/sample-picture/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
deploy/sample-picture/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
deploy/sample-picture/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -12,7 +12,7 @@
"prettier": "npx prettier --write ."
},
"dependencies": {
"@iconify/json": "^2.2.101",
"@iconify/json": "^2.2.228",
"@iconify/vue": "^4.1.1",
"@unocss/eslint-config": "^0.55.0",
"@vueuse/core": "^10.3.0",

6144
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -36,4 +36,6 @@ export default {
createDept: (data = {}) => request.post('/dept/create', data),
updateDept: (data = {}) => request.post('/dept/update', data),
deleteDept: (params = {}) => request.delete('/dept/delete', { params }),
// auditlog
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
}

View File

@ -74,7 +74,23 @@ const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
const loading = ref(false)
const initQuery = { ...props.queryItems }
const tableData = ref([])
const pagination = reactive({ page: 1, page_size: 10 })
const pagination = reactive({
page: 1,
page_size: 10,
pageSizes: [10, 20, 50, 100],
showSizePicker: true,
prefix({ itemCount }) {
return `${itemCount}`
},
onChange: (page) => {
pagination.page = page
},
onUpdatePageSize: (pageSize) => {
pagination.page_size = pageSize
pagination.page = 1
handleQuery()
},
})
async function handleQuery() {
try {
@ -90,7 +106,7 @@ async function handleQuery() {
...paginationParams,
})
tableData.value = data
pagination.itemCount = total
pagination.itemCount = total || 0
} catch (error) {
tableData.value = []
pagination.itemCount = 0

View File

@ -0,0 +1,231 @@
<script setup>
import { onMounted, ref, resolveDirective } from 'vue'
import { NInput, NSelect } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
import CrudTable from '@/components/table/CrudTable.vue'
import { useCRUD } from '@/composables'
import api from '@/api'
defineOptions({ name: '操作日志' })
const $table = ref(null)
const queryItems = ref({})
const {
modalVisible,
modalTitle,
modalLoading,
handleSave,
modalForm,
modalFormRef,
handleEdit,
handleDelete,
handleAdd,
} = useCRUD({
name: '操作日志',
initForm: {
args: {
ping_sleep: 1,
ping_threshold: 500,
agent_data_check_threshold: 2,
agent_incoming_traffic_threshold: 30,
before_start_clean_sleep: 2,
before_stop_disposal_sleep: 20,
},
},
doCreate: api.createHostMonitor,
doUpdate: api.updateHostMonitor,
doDelete: api.deleteHostMonitor,
refresh: () => $table.value?.handleSearch(),
})
onMounted(() => {
$table.value?.handleSearch()
})
function formatTimestamp(timestamp) {
const date = new Date(timestamp)
const pad = (num) => num.toString().padStart(2, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1) // 0+1
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
function getStartOfDayTimestamp() {
const now = new Date()
now.setHours(0, 0, 0, 0) // 0
return now.getTime()
}
//
function getEndOfDayTimestamp() {
const now = new Date()
now.setHours(23, 59, 59, 999) // 235959999
return now.getTime()
}
// 使
const startOfDayTimestamp = getStartOfDayTimestamp()
const endOfDayTimestamp = getEndOfDayTimestamp()
const datetimeRange = ref([startOfDayTimestamp, endOfDayTimestamp])
const handleDateRangeChange = (value) => {
queryItems.value.start_time = formatTimestamp(value[0])
queryItems.value.end_time = formatTimestamp(value[1])
}
const methodOptions = [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
{
label: 'DELETE',
value: 'DELETE',
},
]
const columns = [
{
title: '用户名称',
key: 'username',
width: 'auto',
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '接口概要',
key: 'summary',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '功能模块',
key: 'module',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '请求方法',
key: 'method',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '请求路径',
key: 'path',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '状态码',
key: 'status',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '响应时间(s)',
key: 'response_time',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '操作时间',
key: 'created_at',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
]
</script>
<template>
<!-- 业务页面 -->
<CommonPage>
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:get-data="api.getAuditLogList"
>
<template #queryBar>
<QueryBarItem label="用户名称" :label-width="70">
<NInput
v-model:value="queryItems.username"
clearable
type="text"
placeholder="请输入用户名称"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="功能模块" :label-width="70">
<NInput
v-model:value="queryItems.module"
clearable
type="text"
placeholder="请输入功能模块"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="接口概要" :label-width="70">
<NInput
v-model:value="queryItems.summary"
clearable
type="text"
placeholder="请输入接口概要"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="请求方法" :label-width="70">
<NSelect
v-model:value="modalForm.method"
style="width: 150px"
:options="methodOptions"
clearable
placeholder="请选择请求方法"
/>
</QueryBarItem>
<QueryBarItem label="API路径" :label-width="70">
<NInput
v-model:value="queryItems.path"
clearable
type="text"
placeholder="请输入API路径"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="创建时间" :label-width="70">
<NDatePicker
v-model:value="datetimeRange"
type="datetimerange"
clearable
placeholder="请选择时间范围"
@update:value="handleDateRangeChange"
/>
</QueryBarItem>
</template>
</CrudTable>
</CommonPage>
</template>