This commit is contained in:
mizhexiaoxiao 2025-02-25 19:30:06 +08:00
parent 7c3818f7c4
commit f6ce09a0f5
16 changed files with 1556 additions and 180 deletions

View File

@ -20,9 +20,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends gcc python3-dev bash nginx vim curl procps net-tools && apt-get install -y --no-install-recommends gcc python3-dev bash nginx vim curl procps net-tools
RUN pip install poetry -i https://pypi.tuna.tsinghua.edu.cn/simple \ RUN pip install uv -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& poetry config virtualenvs.create false \ && uv add --requirements requirements.txt
&& poetry install
COPY --from=web /opt/vue-fastapi-admin/web/dist /opt/vue-fastapi-admin/web/dist COPY --from=web /opt/vue-fastapi-admin/web/dist /opt/vue-fastapi-admin/web/dist
ADD /deploy/web.conf /etc/nginx/sites-available/web.conf ADD /deploy/web.conf /etc/nginx/sites-available/web.conf

View File

@ -36,14 +36,15 @@ targets:
.PHONY: install .PHONY: install
install: ## Install dependencies install: ## Install dependencies
poetry install uv add pyproject.toml
.PHONY: run .PHONY: run
run: start run: start
.PHONY: start .PHONY: start
start: ## Starts the server start: ## Starts the server
poetry run python run.py python run.py
# Check, lint and format targets # Check, lint and format targets
# ------------------------------ # ------------------------------
@ -53,28 +54,24 @@ check: check-format lint
.PHONY: check-format .PHONY: check-format
check-format: ## Dry-run code formatter check-format: ## Dry-run code formatter
poetry run black ./ --check black ./ --check
poetry run isort ./ --profile black --check isort ./ --profile black --check
.PHONY: lint .PHONY: lint
lint: ## Run ruff lint: ## Run ruff
poetry run ruff check ./app ruff check ./app
.PHONY: format .PHONY: format
format: ## Run code formatter format: ## Run code formatter
poetry run black ./ black ./
poetry run isort ./ --profile black isort ./ --profile black
.PHONY: check-lockfile
check-lockfile: ## Compares lock file with pyproject.toml
poetry lock --check
.PHONY: test .PHONY: test
test: ## Run the test suite test: ## Run the test suite
$(eval include .env) $(eval include .env)
$(eval export $(sh sed 's/=.*//' .env)) $(eval export $(sh sed 's/=.*//' .env))
pytest -vv -s --cache-clear ./
poetry run pytest -vv -s --cache-clear ./
.PHONY: clean-db .PHONY: clean-db
clean-db: ## 删除migrations文件夹和db.sqlite3 clean-db: ## 删除migrations文件夹和db.sqlite3
@ -83,8 +80,8 @@ clean-db: ## 删除migrations文件夹和db.sqlite3
.PHONY: migrate .PHONY: migrate
migrate: ## 运行aerich migrate命令生成迁移文件 migrate: ## 运行aerich migrate命令生成迁移文件
poetry run aerich migrate aerich migrate
.PHONY: upgrade .PHONY: upgrade
upgrade: ## 运行aerich upgrade命令应用迁移 upgrade: ## 运行aerich upgrade命令应用迁移
poetry run aerich upgrade aerich upgrade

View File

@ -86,19 +86,48 @@ password123456
#### Backend #### Backend
The backend service requires the following environment: The backend service requires the following environment:
- Python 3.11 - Python 3.11
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
#### Method 1 (Recommended): Install Dependencies with uv
1. Install uv
```sh
pip install uv
```
2. Create and activate virtual environment
```sh
uv venv
source .venv/bin/activate # Linux/Mac
# or
.\.venv\Scripts\activate # Windows
```
3. Install dependencies
```sh
uv add pyproject.toml
```
4. Start the backend service
```sh
python run.py
```
#### Method 2: Install Dependencies with Pip
1. Create a Python virtual environment: 1. Create a Python virtual environment:
```sh ```sh
poetry shell python3 -m venv venv
source venv/bin/activate # Linux/Mac
# or
.\venv\Scripts\activate # Windows
``` ```
2. Install project dependencies: 2. Install project dependencies:
```sh ```sh
poetry install pip install -r requirements.txt
``` ```
3. Start the backend service: 3. Start the backend service:
```sh ```sh
make run python run.py
``` ```
The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation. The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation.

View File

@ -87,33 +87,49 @@ password123456
启动项目需要以下环境: 启动项目需要以下环境:
- Python 3.11 - Python 3.11
#### 方法一(推荐):[Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) 安装依赖 #### 方法一(推荐):使用 uv 安装依赖
1. 安装 uv
```sh
pip install uv
```
2. 创建并激活虚拟环境
```sh
uv venv
source .venv/bin/activate # Linux/Mac
# 或
.\.venv\Scripts\activate # Windows
```
3. 安装依赖
```sh
uv add pyproject.toml
```
4. 启动服务
```sh
python run.py
```
#### 方法二:使用 Pip 安装依赖
1. 创建虚拟环境 1. 创建虚拟环境
```sh ```sh
poetry shell python3 -m venv venv
```
2. 安装依赖
```sh
poetry install
```
3. 启动服务
```sh
make run
```
#### 方法二Pip 安装依赖
1. 创建虚拟环境
```sh
python3.11 -m venv venv
``` ```
2. 激活虚拟环境 2. 激活虚拟环境
```sh ```sh
source venv/bin/activate source venv/bin/activate # Linux/Mac
# 或
.\venv\Scripts\activate # Windows
``` ```
3. 安装依赖 3. 安装依赖
```sh ```sh
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
``` ```
3. 启动服务
4. 启动服务
```sh ```sh
python run.py python run.py
``` ```

View File

@ -30,7 +30,7 @@ async def get_audit_log_list(
if summary: if summary:
q &= Q(summary__icontains=summary) q &= Q(summary__icontains=summary)
if status: if status:
q &= Q(status__icontains=status) q &= Q(status=status)
if start_time and end_time: if start_time and end_time:
q &= Q(created_at__range=[start_time, end_time]) q &= Q(created_at__range=[start_time, end_time])
elif start_time: elif start_time:

View File

@ -43,6 +43,7 @@ def make_middlewares():
HttpAuditLogMiddleware, HttpAuditLogMiddleware,
methods=["GET", "POST", "PUT", "DELETE"], methods=["GET", "POST", "PUT", "DELETE"],
exclude_paths=[ exclude_paths=[
"/api/v1/base/access_token",
"/docs", "/docs",
"/openapi.json", "/openapi.json",
], ],

View File

@ -1,5 +1,7 @@
import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Any, AsyncGenerator
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import Response from fastapi.responses import Response
@ -45,10 +47,77 @@ class BackGroundTaskMiddleware(SimpleBaseMiddleware):
class HttpAuditLogMiddleware(BaseHTTPMiddleware): class HttpAuditLogMiddleware(BaseHTTPMiddleware):
def __init__(self, app, methods: list, exclude_paths: list): def __init__(self, app, methods: list[str], exclude_paths: list[str]):
super().__init__(app) super().__init__(app)
self.methods = methods self.methods = methods
self.exclude_paths = exclude_paths self.exclude_paths = exclude_paths
self.audit_log_paths = ["/api/v1/auditlog/list"]
self.max_body_size = 1024 * 1024 # 1MB 响应体大小限制
async def get_request_args(self, request: Request) -> dict:
args = {}
# 获取查询参数
for key, value in request.query_params.items():
args[key] = value
# 获取请求体
if request.method in ["POST", "PUT", "PATCH"]:
try:
body = await request.json()
args.update(body)
except json.JSONDecodeError:
try:
body = await request.form()
args.update(body)
except Exception:
pass
return args
async def get_response_body(self, request: Request, response: Response) -> Any:
# 检查Content-Length
content_length = response.headers.get("content-length")
if content_length and int(content_length) > self.max_body_size:
return {"code": 0, "msg": "Response too large to log", "data": None}
if hasattr(response, "body"):
body = response.body
else:
body_chunks = []
async for chunk in response.body_iterator:
if not isinstance(chunk, bytes):
chunk = chunk.encode(response.charset)
body_chunks.append(chunk)
response.body_iterator = self._async_iter(body_chunks)
body = b"".join(body_chunks)
if any(request.url.path.startswith(path) for path in self.audit_log_paths):
try:
data = self.lenient_json(body)
# 只保留基本信息,去除详细的响应内容
if isinstance(data, dict):
data.pop("response_body", None)
if "data" in data and isinstance(data["data"], list):
for item in data["data"]:
item.pop("response_body", None)
return data
except Exception:
return None
return self.lenient_json(body)
def lenient_json(self, v: Any) -> Any:
if isinstance(v, (str, bytes)):
try:
return json.loads(v)
except (ValueError, TypeError):
pass
return v
async def _async_iter(self, items: list[bytes]) -> AsyncGenerator[bytes, None]:
for item in items:
yield item
async def get_request_log(self, request: Request, response: Response) -> dict: async def get_request_log(self, request: Request, response: Response) -> dict:
""" """
@ -73,23 +142,29 @@ class HttpAuditLogMiddleware(BaseHTTPMiddleware):
user_obj: User = await AuthControl.is_authed(token) user_obj: User = await AuthControl.is_authed(token)
data["user_id"] = user_obj.id if user_obj else 0 data["user_id"] = user_obj.id if user_obj else 0
data["username"] = user_obj.username if user_obj else "" data["username"] = user_obj.username if user_obj else ""
except Exception as e: except Exception:
data["user_id"] = 0 data["user_id"] = 0
data["username"] = "" data["username"] = ""
return data return data
async def before_request(self, request: Request): async def before_request(self, request: Request):
pass request_args = await self.get_request_args(request)
request.state.request_args = request_args
async def after_request(self, request: Request, response: Response, process_time: int): async def after_request(self, request: Request, response: Response, process_time: int):
if request.method in self.methods: # 请求方法为配置的记录方法 if request.method in self.methods:
for path in self.exclude_paths: for path in self.exclude_paths:
if re.search(path, request.url.path, re.I) is not None: if re.search(path, request.url.path, re.I) is not None:
return return
data: dict = await self.get_request_log(request=request, response=response) data: dict = await self.get_request_log(request=request, response=response)
data["response_time"] = process_time # 响应时间 data["response_time"] = process_time
data["request_args"] = request.state.request_args
data["response_body"] = await self.get_response_body(request, response)
await AuditLog.create(**data) await AuditLog.create(**data)
return response
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
start_time: datetime = datetime.now() start_time: datetime = datetime.now()
await self.before_request(request) await self.before_request(request)

View File

@ -49,7 +49,7 @@ class Menu(BaseModel, TimestampMixin):
icon = fields.CharField(max_length=100, null=True, description="菜单图标") icon = fields.CharField(max_length=100, null=True, description="菜单图标")
path = fields.CharField(max_length=100, description="菜单路径", index=True) path = fields.CharField(max_length=100, description="菜单路径", index=True)
order = fields.IntField(default=0, description="排序", index=True) order = fields.IntField(default=0, description="排序", index=True)
parent_id = fields.IntField(default=0, max_length=10, description="父菜单ID", index=True) parent_id = fields.IntField(default=0, description="父菜单ID", index=True)
is_hidden = fields.BooleanField(default=False, description="是否隐藏") is_hidden = fields.BooleanField(default=False, description="是否隐藏")
component = fields.CharField(max_length=100, description="组件") component = fields.CharField(max_length=100, description="组件")
keepalive = fields.BooleanField(default=True, description="存活") keepalive = fields.BooleanField(default=True, description="存活")
@ -85,3 +85,5 @@ class AuditLog(BaseModel, TimestampMixin):
path = fields.CharField(max_length=255, default="", description="请求路径", index=True) path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
status = fields.IntField(default=-1, description="状态码", index=True) status = fields.IntField(default=-1, description="状态码", index=True)
response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True) response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True)
request_args = fields.JSONField(null=True, description="请求参数")
response_body = fields.JSONField(null=True, description="返回数据")

View File

@ -10,8 +10,7 @@ class BaseApi(BaseModel):
tags: str = Field(..., description="API标签", example="User") tags: str = Field(..., description="API标签", example="User")
class ApiCreate(BaseApi): class ApiCreate(BaseApi): ...
...
class ApiUpdate(BaseApi): class ApiUpdate(BaseApi):

View File

@ -8,8 +8,7 @@ class BaseDept(BaseModel):
parent_id: int = Field(0, description="父部门ID") parent_id: int = Field(0, description="父部门ID")
class DeptCreate(BaseDept): class DeptCreate(BaseDept): ...
...
class DeptUpdate(BaseDept): class DeptUpdate(BaseDept):

View File

@ -1,55 +1,87 @@
[tool.poetry] [project]
name = "vue-fastapi-admin" name = "vue-fastapi-admin"
version = "0.1.0" version = "0.1.0"
description = "Vue Fastapi admin" description = "Vue Fastapi admin"
authors = ["mizhexiaoxiao <mizhexiaoxiao@gmail.com>"] authors = [
readme = "README.md" {name = "mizhexiaoxiao", email = "mizhexiaoxiao@gmail.com"},
]
[tool.poetry.dependencies] requires-python = ">=3.11"
python = "^3.11" dependencies = [
fastapi = "0.111.0" "fastapi==0.111.0",
tortoise-orm = "^0.20.1" "tortoise-orm==0.23.0",
pydantic = "^2.7.1" "pydantic==2.10.5",
email-validator = "^2.0.0.post2" "email-validator==2.2.0",
passlib = "^1.7.4" "passlib==1.7.4",
pyjwt = "^2.7.0" "pyjwt==2.10.1",
black = "^23.7.0" "black==24.10.0",
isort = "^5.12.0" "isort==5.13.2",
ruff = "^0.0.281" "ruff==0.9.1",
loguru = "^0.7.0" "loguru==0.7.3",
pydantic-settings = "^2.0.3" "pydantic-settings==2.7.1",
argon2-cffi = "^23.1.0" "argon2-cffi==23.1.0",
pydantic-core = "^2.18.2" "pydantic-core==2.27.2",
annotated-types = "^0.6.0" "annotated-types==0.7.0",
setuptools = "^70.0.0" "setuptools==75.8.0",
uvicorn = "^0.30.1" "uvicorn==0.34.0",
h11 = "^0.14.0" "h11==0.14.0",
aerich = "^0.7.2" "aerich==0.8.1",
"aiosqlite==0.20.0",
"anyio==4.8.0",
"argon2-cffi-bindings==21.2.0",
"asyncclick==8.1.8",
"certifi==2024.12.14",
"cffi==1.17.1",
"click==8.1.8",
"dictdiffer==0.9.0",
"dnspython==2.7.0",
"fastapi-cli==0.0.7",
"httpcore==1.0.7",
"httptools==0.6.4",
"httpx==0.28.1",
"idna==3.10",
"iso8601==2.1.0",
"jinja2==3.1.5",
"markdown-it-py==3.0.0",
"markupsafe==3.0.2",
"mdurl==0.1.2",
"mypy-extensions==1.0.0",
"orjson==3.10.14",
"packaging==24.2",
"pathspec==0.12.1",
"platformdirs==4.3.6",
"pycparser==2.22",
"pygments==2.19.1",
"pypika-tortoise==0.3.2",
"python-dotenv==1.0.1",
"python-multipart==0.0.20",
"pytz==2024.2",
"pyyaml==6.0.2",
"rich==13.9.4",
"rich-toolkit==0.13.2",
"shellingham==1.5.4",
"sniffio==1.3.1",
"starlette==0.37.2",
"typer==0.15.1",
"typing-extensions==4.12.2",
"ujson==5.10.0",
"uvloop==0.21.0",
"watchfiles==1.0.4",
"websockets==14.1",
"pyproject-toml>=0.1.0",
]
[tool.black] [tool.black]
line-length = 120 line-length = 120
target-version = ["py310", "py311"] target-version = ["py310", "py311"]
[[tool.poetry.source]]
name = "tsinghua"
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
priority = "primary"
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
extend-select = [ lint.extend-select = []
# "I", # isort lint.ignore = [
# "B", # flake8-bugbear
# "C4", # flake8-comprehensions
# "PGH", # pygrep-hooks
# "RUF", # ruff
# "W", # pycodestyle
# "YTT", # flake8-2020
]
ignore = [
"F403", "F403",
"F405", "F405",
] ]
[tool.aerich] [tool.aerich]
tortoise_orm = "app.settings.TORTOISE_ORM" tortoise_orm = "app.settings.TORTOISE_ORM"
location = "./migrations" location = "./migrations"

View File

@ -1,60 +1,60 @@
aerich==0.7.2 aerich==0.8.1
aiosqlite==0.17.0 aiosqlite==0.20.0
annotated-types==0.6.0 annotated-types==0.7.0
anyio==4.4.0 anyio==4.8.0
argon2-cffi==23.1.0 argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0 argon2-cffi-bindings==21.2.0
black==23.12.1 asyncclick==8.1.8
certifi==2024.7.4 black==24.10.0
cffi==1.17.0 certifi==2024.12.14
click==8.1.7 cffi==1.17.1
click==8.1.8
dictdiffer==0.9.0 dictdiffer==0.9.0
dnspython==2.6.1 dnspython==2.7.0
email_validator==2.2.0 email-validator==2.2.0
fastapi==0.111.0 fastapi==0.111.0
fastapi-cli==0.0.5 fastapi-cli==0.0.7
h11==0.14.0 h11==0.14.0
httpcore==1.0.5 httpcore==1.0.7
httptools==0.6.1 httptools==0.6.4
httpx==0.27.0 httpx==0.28.1
idna==3.8 idna==3.10
iso8601==1.1.0 iso8601==2.1.0
isort==5.13.2 isort==5.13.2
Jinja2==3.1.4 jinja2==3.1.5
loguru==0.7.2 loguru==0.7.3
markdown-it-py==3.0.0 markdown-it-py==3.0.0
MarkupSafe==2.1.5 markupsafe==3.0.2
mdurl==0.1.2 mdurl==0.1.2
mypy-extensions==1.0.0 mypy-extensions==1.0.0
orjson==3.10.7 orjson==3.10.14
packaging==24.1 packaging==24.2
passlib==1.7.4 passlib==1.7.4
pathspec==0.12.1 pathspec==0.12.1
platformdirs==4.2.2 platformdirs==4.3.6
pycparser==2.22 pycparser==2.22
pydantic==2.9.0b1 pydantic==2.10.5
pydantic-settings==2.4.0 pydantic-core==2.27.2
pydantic_core==2.23.0 pydantic-settings==2.7.1
Pygments==2.18.0 pygments==2.19.1
PyJWT==2.9.0 pyjwt==2.10.1
pypika-tortoise==0.1.6 pypika-tortoise==0.3.2
python-dotenv==1.0.1 python-dotenv==1.0.1
python-multipart==0.0.9 python-multipart==0.0.20
pytz==2024.1 pytz==2024.2
PyYAML==6.0.2 pyyaml==6.0.2
rich==13.8.0 rich==13.9.4
ruff==0.0.281 rich-toolkit==0.13.2
setuptools==70.3.0 ruff==0.9.1
setuptools==75.8.0
shellingham==1.5.4 shellingham==1.5.4
sniffio==1.3.1 sniffio==1.3.1
starlette==0.37.2 starlette==0.37.2
tomlkit==0.13.2 tortoise-orm==0.23.0
tortoise-orm==0.20.1 typer==0.15.1
typer==0.12.5 typing-extensions==4.12.2
typing_extensions==4.12.2
tzdata==2024.1
ujson==5.10.0 ujson==5.10.0
uvicorn==0.30.6 uvicorn==0.34.0
uvloop==0.20.0 uvloop==0.21.0; sys_platform != 'win32'
watchfiles==0.23.0 watchfiles==1.0.4
websockets==13.0 websockets==14.1

11
run.py
View File

@ -1,4 +1,13 @@
import uvicorn import uvicorn
from uvicorn.config import LOGGING_CONFIG
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config="uvicorn_loggin_config.json") # 修改默认日志配置
LOGGING_CONFIG["formatters"]["default"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
LOGGING_CONFIG["formatters"]["access"][
"fmt"
] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
uvicorn.run("app:app", host="0.0.0.0", port=9988, reload=True, log_config=LOGGING_CONFIG)

1197
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": null
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": "%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
}
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr"
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout"
}
},
"loggers": {
"uvicorn": {
"handlers": [
"default"
],
"level": "INFO"
},
"uvicorn.error": {
"level": "INFO"
},
"uvicorn.access": {
"handlers": [
"access"
],
"level": "INFO",
"propagate": false
}
}
}

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { NInput, NSelect } from 'naive-ui' import { NInput, NSelect, NPopover } from 'naive-ui'
import TheIcon from '@/components/icon/TheIcon.vue'
import CommonPage from '@/components/page/CommonPage.vue' import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue' import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
@ -78,6 +79,16 @@ const methodOptions = [
}, },
] ]
function formatJSON(data) {
try {
return typeof data === 'string'
? JSON.stringify(JSON.parse(data), null, 2)
: JSON.stringify(data, null, 2)
} catch (e) {
return data || '无数据'
}
}
const columns = [ const columns = [
{ {
title: '用户名称', title: '用户名称',
@ -121,6 +132,62 @@ const columns = [
width: 'auto', width: 'auto',
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
}, },
{
title: '请求体',
key: 'request_body',
align: 'center',
width: 80,
render: (row) => {
return h(
NPopover,
{
trigger: 'hover',
placement: 'right',
},
{
trigger: () =>
h('div', { style: 'cursor: pointer;' }, [h(TheIcon, { icon: 'carbon:data-view' })]),
default: () =>
h(
'pre',
{
style:
'max-height: 400px; overflow: auto; background-color: #f5f5f5; padding: 8px; border-radius: 4px;',
},
formatJSON(row.request_args)
),
}
)
},
},
{
title: '响应体',
key: 'response_body',
align: 'center',
width: 80,
render: (row) => {
return h(
NPopover,
{
trigger: 'hover',
placement: 'right',
},
{
trigger: () =>
h('div', { style: 'cursor: pointer;' }, [h(TheIcon, { icon: 'carbon:data-view' })]),
default: () =>
h(
'pre',
{
style:
'max-height: 400px; overflow: auto; background-color: #f5f5f5; padding: 8px; border-radius: 4px;',
},
formatJSON(row.response_body)
),
}
)
},
},
{ {
title: '响应时间(s)', title: '响应时间(s)',
key: 'response_time', key: 'response_time',
@ -203,7 +270,7 @@ const columns = [
@keypress.enter="$table?.handleSearch()" @keypress.enter="$table?.handleSearch()"
/> />
</QueryBarItem> </QueryBarItem>
<QueryBarItem label="创建时间" :label-width="70"> <QueryBarItem label="操作时间" :label-width="70">
<NDatePicker <NDatePicker
v-model:value="datetimeRange" v-model:value="datetimeRange"
type="datetimerange" type="datetimerange"