1. 去除重复的pages.py

2. 编辑和删除操作增加鼠标悬停提示
3. 搜索栏布局的优化工作:

- 调整了搜索框容器的宽度和间距
- 优化了搜索框组件的弹性布局
- 设置了下拉框和按钮的最小宽度
- 改进了按钮的对齐方式和间距
- 优化了移动端的响应式布局
This commit is contained in:
古月 2025-03-04 13:23:43 +08:00
parent 8a0925ec1a
commit e71d485629
11 changed files with 148 additions and 241 deletions

View File

@ -1,10 +1,9 @@
from fastapi import APIRouter
from app.api.endpoints import types, configs, pages
from app.api.endpoints import types, configs
api_router = APIRouter()
# 添加页面路由,不带前缀
api_router.include_router(pages.router, tags=["pages"])
# 页面路由已移至 app.api.pages.py
# API路由不带前缀因为前缀在main.py中添加
api_router.include_router(types.router, prefix="/types", tags=["types"])

View File

@ -1,72 +0,0 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional
from app.core.config import settings
from app.models.database import get_db
from app.models.config import Config
async def is_privilege_mode(db: AsyncSession) -> bool:
"""检查是否启用了特权模式"""
# 首先检查环境变量中的特权模式设置
if settings.PRIVILEGE_MODE:
return True
# 如果环境变量中没有启用特权模式,则检查数据库中的配置
try:
result = await db.execute(
select(Config).where(
Config.key == "privilege_mode"
)
)
config = result.scalar_one_or_none()
return config is not None and config.value.lower() == "true"
except Exception as e:
print(f"检查特权模式出错: {e}")
return False
# 以下函数用于兼容性目的返回None表示无用户
async def get_current_user_optional(db: AsyncSession = Depends(get_db)) -> None:
"""获取当前用户可选未登录返回None"""
return None
# 兼容性函数
async def get_current_active_user(db: AsyncSession = Depends(get_db)) -> None:
"""获取当前活跃用户(兼容性函数)"""
return None
# 兼容性函数
async def get_current_admin_user(db: AsyncSession = Depends(get_db)) -> None:
"""获取当前管理员用户(兼容性函数)"""
return None
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
username: str = payload.get("sub")
if username is None:
return None
except JWTError:
return None
result = await db.execute(select(User).where(User.username == username))
user = result.scalar_one_or_none()
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""获取当前活跃用户"""
return current_user
async def get_current_admin_user(
current_user: User = Depends(get_current_user),
) -> User:
"""获取当前管理员用户"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="权限不足")
return current_user

View File

@ -1,5 +0,0 @@
from app.core.config import settings
def is_privilege_mode():
"""检查是否处于特权模式"""
return settings.PRIVILEGE_MODE

View File

@ -6,9 +6,6 @@ from app.models.database import get_db
from app.models.config import Config
from app.models.type import Type
from app.schemas.config import ConfigCreate, ConfigUpdate, Config as ConfigSchema, ConfigList, ConfigSearch
from app.api.deps import is_privilege_mode
# 添加缺少的导入
from app import schemas
router = APIRouter()
@ -378,6 +375,48 @@ async def delete_config_by_type_and_key(
raise HTTPException(status_code=404, detail=f"类型 '{type_name}' 下不存在键 '{key}'")
# 修改删除配置项的端点s
@router.put("/{type_name}/{key}", response_model=ConfigSchema)
async def update_config_by_type_and_key(
type_name: str,
key: str,
config_data: ConfigUpdate,
db: AsyncSession = Depends(get_db)
):
"""
通过类型名称和键更新配置
"""
# 查询配置项
result = await db.execute(
select(Config).join(Type).where(
Type.type_name == type_name,
Config.key == key
)
)
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail=f"找不到配置项: {type_name}.{key}")
# 更新配置
if config_data.value is not None:
config.value = config_data.value
if config_data.key_description is not None:
config.key_description = config_data.key_description
await db.commit()
await db.refresh(config)
return ConfigSchema(
config_id=config.config_id,
type_id=config.type_id,
key=config.key,
value=config.value,
key_description=config.key_description,
created_at=config.created_at,
updated_at=config.updated_at,
type_name=type_name
)
@router.delete("/{type_name}/{key}", response_model=dict)
async def delete_config(
type_name: str,

View File

@ -1,99 +0,0 @@
from fastapi import APIRouter, Request, Depends
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from pathlib import Path
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.database import get_db
from app.models.type import Type
from app.models.config import Config
from typing import Optional
router = APIRouter()
templates = Jinja2Templates(directory=str(Path(__file__).parents[2] / "templates"))
# 修改根路径处理函数
@router.get("/", response_class=HTMLResponse)
async def index(request: Request, db: AsyncSession = Depends(get_db)):
"""首页"""
# 查询类型数量
result = await db.execute(select(func.count()).select_from(Type))
type_count = result.scalar()
# 查询配置项数量
result = await db.execute(select(func.count()).select_from(Config))
config_count = result.scalar()
return templates.TemplateResponse(
"index.html",
{
"request": request,
"type_count": type_count,
"config_count": config_count
}
)
@router.get("/types", response_class=HTMLResponse)
async def types_page(
request: Request,
search: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""配置类型页面"""
# 构建查询
query = select(Type)
if search:
query = query.where(Type.type_name.contains(search) | Type.description.contains(search))
# 执行查询
result = await db.execute(query)
types = result.scalars().all()
return templates.TemplateResponse(
"types.html",
{
"request": request,
"types": types,
"search": search
}
)
@router.get("/configs", response_class=HTMLResponse)
async def configs_page(
request: Request,
type_name: Optional[str] = None,
key: Optional[str] = None,
value: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""配置项页面"""
# 构建查询
query = select(Config, Type).join(Type)
# 添加筛选条件
if type_name:
query = query.where(Type.type_name.contains(type_name))
if key:
query = query.where(Config.key.contains(key))
if value:
query = query.where(Config.value.contains(value))
# 执行查询
result = await db.execute(query)
rows = result.all()
# 查询所有类型(用于筛选)
types_result = await db.execute(select(Type))
types = types_result.scalars().all()
return templates.TemplateResponse(
"configs.html",
{
"request": request,
"configs": rows,
"types": types,
"type_name": type_name,
"key": key,
"value": value
}
)

View File

@ -6,7 +6,6 @@ from app.models.database import get_db
from app.models.type import Type
from app.models.config import Config
from app.schemas.type import TypeCreate, TypeUpdate, Type as TypeSchema, TypeList
from app.api.deps import is_privilege_mode
router = APIRouter()

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter, Request, Depends
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from pathlib import Path
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import Optional
from app.models.database import get_db
from app.models.type import Type
@ -14,30 +16,31 @@ page_router = APIRouter()
# 设置模板目录
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
@page_router.get("/")
@page_router.get("/", response_class=HTMLResponse)
async def index_page(request: Request, db: AsyncSession = Depends(get_db)):
"""首页"""
# 查询类型数量
result = await db.execute(select(func.count()).select_from(Type))
types_count = result.scalar()
type_count = result.scalar()
# 查询配置项数量
result = await db.execute(select(func.count()).select_from(Config))
configs_count = result.scalar()
config_count = result.scalar()
return templates.TemplateResponse(
"index.html",
{"request": request,
"types_count": types_count,
"configs_count": configs_count
{
"request": request,
"types_count": type_count,
"configs_count": config_count
}
)
@page_router.get("/types")
@page_router.get("/types", response_class=HTMLResponse)
async def types_page(
request: Request,
search: str = None,
search: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""配置类型页面"""
@ -52,34 +55,51 @@ async def types_page(
"types.html",
{
"request": request,
"types": types
"types": types,
"search": search
}
)
@page_router.get("/configs")
@page_router.get("/configs", response_class=HTMLResponse)
async def configs_page(
request: Request,
type_name: str = None,
search: str = None,
type_name: Optional[str] = None,
key: Optional[str] = None,
value: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""配置项页面"""
# 构建查询
query = select(Config, Type).join(Type)
# 添加筛选条件
if type_name:
query = query.where(Type.type_name.contains(type_name))
if key:
query = query.where(Config.key.contains(key))
if value:
query = query.where(Config.value.contains(value))
# 执行查询
result = await db.execute(query)
rows = result.all()
# 处理查询结果将Config对象的属性转换为字典
configs = [{
"config_id": row[0].config_id,
"type_id": row[0].type_id,
"key": row[0].key,
"value": row[0].value,
"key_description": row[0].key_description,
"created_at": row[0].created_at,
"updated_at": row[0].updated_at,
"type_name": row[1].type_name
} for row in rows]
# 获取所有类型
types_result = await db.execute(select(Type))
types = types_result.scalars().all()
# 构建查询
query = select(Config).join(Type)
if type_name:
query = query.where(Type.type_name == type_name)
if search:
query = query.where(Config.key.contains(search) | Config.value.contains(search) | Config.key_description.contains(search))
result = await db.execute(query)
configs = result.scalars().all()
return templates.TemplateResponse(
"configs.html",
{

View File

@ -260,29 +260,32 @@ tbody tr:hover {
background-color: rgba(99, 164, 255, 0.1);
transition: background-color 0.2s;
}
.config-value {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--secondary-color);
border-radius: 4px;
font-family: monospace;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value-container {
.btn-group .btn {
position: relative;
max-width: 300px;
}
.btn-group {
display: inline-flex;
gap: 0.5rem;
.btn-group .btn::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 12px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
z-index: 1000;
}
.btn-group .btn:hover::before {
opacity: 1;
visibility: visible;
}
.btn-group .btn {
border-radius: 4px;
}
@ -400,10 +403,10 @@ tr:hover {
border-color: var(--primary-color);
outline: none;
}
/* 搜索框 */
.search-container {
margin: 1.5rem 0;
margin-bottom: 1.5rem;
padding: 0;
}
.search-box {
@ -411,12 +414,39 @@ tr:hover {
gap: 1rem;
flex-wrap: wrap;
align-items: center;
background: white;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
width: 100%;
}
.search-box select {
min-width: 150px;
max-width: 200px;
}
.search-input {
flex: 1;
min-width: 200px;
}
.search-box .btn {
flex-shrink: 0;
white-space: nowrap;
min-width: 80px;
}
@media (max-width: 768px) {
.search-box {
gap: 0.5rem;
}
.search-box select,
.search-input,
.search-box .btn {
width: 100%;
margin: 0;
}
.search-box select {
max-width: 100%;
}
}
.input-group {
display: flex;
gap: 0.5rem;
@ -432,7 +462,6 @@ tr:hover {
border-radius: 4px;
color: var(--text-light);
}
.search-box select {
min-width: 150px;
max-width: 200px;
@ -441,7 +470,6 @@ tr:hover {
padding: 0.5rem;
background: white;
}
.search-input {
flex: 1;
min-width: 200px;
@ -450,12 +478,10 @@ tr:hover {
padding: 0.5rem;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: var(--primary-color);
outline: none;
}
/* 标签样式 */
.tag {
display: inline-block;

View File

@ -48,7 +48,7 @@
<tbody>
{% for config in configs %}
<tr>
<td><span class="badge bg-primary">{{ config.type.type_name }}</span></td>
<td><span class="badge bg-primary">{{ config.type_name }}</span></td>
<td>{{ config.key }}</td>
<td>
<div class="value-container">
@ -60,10 +60,10 @@
<td>{{ config.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td class="text-end">
<div class="btn-group">
<button class="btn btn-sm btn-secondary" onclick="showEditConfigModal('{{ config.type.type_name }}', '{{ config.key }}', '{{ config.value }}', '{{ config.key_description }}')">
<button class="btn btn-sm btn-secondary" data-tooltip="编辑" onclick="showEditConfigModal('{{ config.type_name }}', '{{ config.key }}', '{{ config.value }}', '{{ config.key_description }}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="confirmDeleteConfig('{{ config.type.type_name }}', '{{ config.key }}')">
<button class="btn btn-sm btn-danger" data-tooltip="删除" onclick="confirmDeleteConfig('{{ config.type_name }}', '{{ config.key }}')">
<i class="fas fa-trash"></i>
</button>
</div>

View File

@ -46,10 +46,10 @@
<td>{{ type.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td class="text-end">
<div class="btn-group">
<button class="btn btn-sm btn-secondary" onclick="showEditTypeModal('{{ type.type_name }}', '{{ type.description }}')">
<button class="btn btn-sm btn-secondary" data-tooltip="编辑" onclick="showEditTypeModal('{{ type.type_name }}', '{{ type.description }}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="confirmDeleteType('{{ type.type_name }}')">
<button class="btn btn-sm btn-danger" data-tooltip="删除" onclick="confirmDeleteType('{{ type.type_name }}')">
<i class="fas fa-trash"></i>
</button>
</div>

Binary file not shown.