V2.0.0-alpha1

This commit is contained in:
2025-04-22 03:22:17 +08:00
parent 2a217c4b8c
commit 39bbc94d07
53 changed files with 5019 additions and 1203 deletions

177
routes/admin.py Normal file
View File

@@ -0,0 +1,177 @@
from fastapi import APIRouter
from typing import Annotated, Literal, Optional
from fastapi import Depends, Query
from fastapi import HTTPException
import JWT
from model import database
from model.response import DefaultResponse
from model.items import Item
# 验证是否为管理员
async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]) -> Literal[True]:
'''
验证是否为管理员。
使用方法:
>>> APIRouter(dependencies=[Depends(is_admin)])
'''
return True
Router = APIRouter(
prefix='/api/admin',
tags=['管理员 Admin'],
dependencies=[Depends(is_admin)]
)
@Router.get(
path='/',
summary='验证管理员身份',
description='返回管理员身份验证结果',
response_model=DefaultResponse,
response_description='当前为管理员'
)
async def verity_admin(
is_admin: Annotated[str, Depends(is_admin)]
) -> Literal[True]:
'''
使用 API 验证是否为管理员。
- 若为管理员,返回 `True`
- 若不是管理员,抛出 `401` 错误
'''
return is_admin
@Router.get(
path='/items',
summary='获取物品信息',
description='返回物品信息列表',
response_model=DefaultResponse,
response_description='物品信息列表'
)
async def get_items(
id: Optional[int] = Query(default=None, ge=1, description='物品ID'),
key: Optional[str] = Query(default=None, description='物品序列号')):
'''
获得物品信息。
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
'''
results = await database.Database().get_object(id=id, key=key)
if results is not None:
if not isinstance(results, list):
items = [results]
else:
items = results
item = []
for i in items:
item.append(Item(
id=i[0],
key=i[1],
name=i[2],
icon=i[3],
status=i[4],
phone=i[5],
lost_description=i[6],
find_ip=i[7],
create_time=i[8],
lost_time=i[9]
))
return DefaultResponse(data=item)
else:
return DefaultResponse(data=[])
@Router.post(
path='/items',
summary='添加物品信息',
description='添加新的物品信息',
response_model=DefaultResponse,
response_description='添加物品成功'
)
async def add_items(
key: str,
name: str,
icon: str,
phone: str) -> DefaultResponse:
'''
添加物品信息。
- **key**: 物品的关键字
- **name**: 物品的名称
- **icon**: 物品的图标
- **phone**: 联系电话
'''
try:
await database.Database().add_object(
key=key, name=name, icon=icon, phone=phone)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()
@Router.patch(
path='/items',
summary='更新物品信息',
description='更新现有物品的信息',
response_model=DefaultResponse,
response_description='更新物品成功'
)
async def update_items(
id: int = Query(ge=1),
key: Optional[str] = None,
name: Optional[str] = None,
icon: Optional[str] = None,
status: Optional[str] = None,
phone: Optional[int] = None,
lost_description: Optional[str] = None,
find_ip: Optional[str] = None,
lost_time: Optional[str] = None) -> DefaultResponse:
'''
更新物品信息。
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
- **id**: 物品的ID
- **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效**
- **name**: 物品的名称
- **icon**: 物品的图标
- **status**: 物品的状态
- **phone**: 联系电话
- **lost_description**: 物品丢失描述
- **find_ip**: 找到物品的IP
- **lost_time**: 物品丢失时间
'''
try:
await database.Database().update_object(
id=id,
key=key, name=name, icon=icon, status=status, phone=phone,
lost_description=lost_description, find_ip=find_ip,
lost_time=lost_time
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()
@Router.delete(
path='/items',
summary='删除物品信息',
description='删除指定的物品信息',
response_model=DefaultResponse,
response_description='删除物品成功'
)
async def delete_items(
id: int) -> DefaultResponse:
'''
删除物品信息。
- **id**: 物品的ID
'''
try:
await database.Database().delete_object(id=id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()

View File

@@ -1,109 +0,0 @@
from fastapi import APIRouter
from typing import Annotated, Optional
from fastapi import Depends
from fastapi import HTTPException, status
from jwt import InvalidTokenError
import jwt, JWT
from model import database
from model.response import DefaultResponse
from model.items import Item
async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Login required",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT.SECRET_KEY, algorithms=["HS256"])
username = payload.get("sub")
if username is None:
raise credentials_exception
except InvalidTokenError:
raise credentials_exception
if not username == await database.Database().get_setting('account'):
raise credentials_exception
return True
Router = APIRouter(
prefix='/api/admin',
tags=['admin'],
dependencies=[Depends(is_admin)]
)
@Router.get('/')
async def verity_admin(
is_admin: Annotated[str, Depends(is_admin)]
):
return is_admin
@Router.get('/items')
async def get_items(
id: Optional[int] = None,
key: Optional[str] = None):
results = await database.Database().get_object(id=id, key=key)
if results is not None:
if not isinstance(results, list):
items = [results]
else:
items = results
item = []
for i in items:
item.append(Item(
id=i[0],
key=i[1],
name=i[2],
icon=i[3],
status=i[4],
phone=i[5],
lost_description=i[6],
find_ip=i[7],
create_time=i[8],
lost_time=i[9]
))
return DefaultResponse(data=item)
else:
return DefaultResponse(data=[])
@Router.post('/items')
async def add_items(
key: str,
name: str,
icon: str,
phone: str):
await database.Database().add_object(
key=key, name=name, icon=icon, phone=phone)
@Router.patch('/items')
async def update_items(
id: int,
key: Optional[str] = None,
name: Optional[str] = None,
icon: Optional[str] = None,
status: Optional[str] = None,
phone: Optional[int] = None,
lost_description: Optional[str] = None,
find_ip: Optional[str] = None,
lost_time: Optional[str] = None):
try:
await database.Database().update_object(
id=id,
key=key, name=name, icon=icon, status=status, phone=phone,
lost_description=lost_description, find_ip=find_ip,
lost_time=lost_time
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()
@Router.delete('/items')
async def delete_items(
id: int):
try:
await database.Database().delete_object(id=id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()

View File

@@ -1,4 +0,0 @@
from . import about
from . import auth
from . import home
from . import items

View File

@@ -1,43 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-12-14 20:03:49
FilePath: /Findreve/admin.py
Description: Findreve 后台管理 admin
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from fastapi import Request
from tool import *
from ..framework import frame
def create():
@ui.page('/admin/about')
async def admin_about(request: Request):
ui.add_head_html("""
<style type="text/css" src="/static/css/about.css"></style>
<script type="text/javascript" src="/static/js/main.js"></script>
""")
async with frame(request=request):
# 关于 Findreve
with ui.tab_panel('about'):
ui.label('关于 Findreve').classes('text-2xl font-bold')
about = ui.markdown('''加载中...''')
try:
# 延长超时时间到10秒
about_text = await ui.run_javascript('get_about()', timeout=10.0)
if isinstance(about_text, dict) and 'status' in about_text and about_text['status'] == 'failed':
about.set_content(f'加载失败: {about_text.get("detail", "未知错误")}')
else:
about.set_content(about_text)
except Exception as e:
ui.notify(f'加载失败: {str(e)}', color='negative')
about.set_content(f'### 无法加载内容\n\n出现错误: {str(e)}')

View File

@@ -1,41 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-12-14 20:03:49
FilePath: /Findreve/admin.py
Description: Findreve 后台管理 admin
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from tool import *
from fastapi import Request
from ..framework import frame
def create():
@ui.page('/admin/auth')
async def admin_auth(request: Request):
# Findreve 授权
async with frame(request=request):
ui.label('Findreve 授权').classes('text-2xl text-bold')
with ui.element('div').classes('p-2 bg-orange-100 w-full'):
with ui.row(align_items='center'):
ui.icon('favorite').classes('text-rose-500 text-2xl')
ui.label('感谢您使用 Findreve').classes('text-rose-500 text-bold')
with ui.column():
ui.markdown('> 使用付费版本请在下方进行授权验证'
'<br>'
'Findreve 是一款良心、厚道的好产品!创作不易,支持正版,从我做起!'
'<br>'
'如需在生产环境部署请前往 `auth.yxqi.cn` 购买正版'
).classes('text-rose-500')
ui.markdown('- Findreve 官网:[https://auth.yxqi.cn](https://auth.yxqi.cn)\n'
'- 作者联系方式QQ 2372526808\n'
'- 管理我的授权:[https://auth.yxqi.cn/product/5](https://auth.yxqi.cn/product/5)\n'
).classes('text-rose-500')
ui.label('您正在使用免费版本无需授权可体验完整版Findreve。').classes('text-bold')

View File

@@ -1,29 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-12-14 20:03:49
FilePath: /Findreve/admin.py
Description: Findreve 后台管理 admin
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui, app
from fastapi import Request
from fastapi.responses import RedirectResponse
from tool import *
from ..framework import frame
def create():
@app.get('/admin')
async def jump():
return RedirectResponse(url='/admin/home')
@ui.page('/admin/home')
async def admin_home(request: Request):
async with frame(request=request):
with ui.tab_panel('main_page'):
ui.label('首页配置').classes('text-2xl text-bold')
ui.label('暂不支持请直接修改main_page.py').classes('text-md text-gray-600').classes('w-full')

View File

@@ -1,453 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-12-14 20:03:49
FilePath: /Findreve/admin.py
Description: Findreve 后台管理 admin
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
import asyncio
from nicegui import ui
from typing import Dict
import qrcode
import base64
import json
from io import BytesIO
from fastapi import Request
from tool import *
from ..framework import frame, loding_process
def create():
@ui.page('/admin/items')
async def admin_items(request: Request):
ui.add_head_html("""
<script type="text/javascript" src="/static/js/main.js"></script>
""")
dark_mode = ui.dark_mode(value=True)
async with frame():
# 表格列的显示隐藏开关
def tableToggle(column: Dict, visible: bool, table) -> None:
column['classes'] = '' if visible else 'hidden'
column['headerClasses'] = '' if visible else 'hidden'
table.update()
# 列表选择函数
async def objectTableOnSelect():
try:
status = str(object_table.selected[0]['status'])
except:
status = None
# 刷新FAB按钮状态
if status:
# 选中正常物品,显示编辑按钮
addObjectFAB.set_visibility(False)
editObjectFAB.set_visibility(True)
else:
addObjectFAB.set_visibility(True)
editObjectFAB.set_visibility(False)
try:
# 预填充编辑表单
if object_table.selected:
selected_item = object_table.selected[0]
edit_object_name.set_value(selected_item.get('name', ''))
edit_object_icon.set_value(selected_item.get('icon', ''))
edit_object_phone.set_value(selected_item.get('phone', ''))
edit_object_key.set_value(selected_item.get('key', ''))
# 设置丢失状态开关
edit_set_object_lost.set_value(selected_item.get('status') == '丢失')
# 设置物主留言
lostReason.set_value(selected_item.get('lost_description', ''))
except:
# 当物品列表未选中,显示添加物品按钮,其他按钮不显示
addObjectFAB.set_visibility(True)
return
# 添加物品
async def addObject():
dialogAddObjectIcon.disable()
async def on_success():
await reloadTable(tips=False)
# 清空输入框
object_name.set_value('')
object_icon.set_value('')
object_phone.set_value('')
object_key.set_value('')
with ui.dialog() as addObjectSuccessDialog, ui.card().style('width: 90%; max-width: 500px'):
ui.button(icon='done').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
ui.label('添加成功').classes('w-full text-h5 text-center')
ui.label('你可以使用下面的链接来访问这个物品')
ui.code(request.base_url.hostname+ '/found?key=' + object_key.value).classes('w-full')
# 生成二维码
qr_data = request.base_url.hostname + '/found?key=' + object_key.value
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill='black', back_color='white')
# 将二维码转换为Base64
buffered = BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
# 展示二维码
with ui.row(align_items='center').classes('w-full'):
ui.space()
ui.image(f'data:image/png;base64,{img_str}').classes('w-1/3')
ui.space()
with ui.row(align_items='center').classes('w-full'):
ui.space()
ui.button("下载二维码", on_click=lambda: ui.download(buffered.getvalue(), 'qrcode.png')) \
.props('flat rounded')
ui.button("返回", on_click=lambda: (addObjectDialog.close(), addObjectSuccessDialog.close(), addObjectSuccessDialog.delete())) \
.props('flat rounded')
addObjectSuccessDialog.open()
if object_name.value == "" or object_icon == "" or object_phone == "":
ui.notify('必填字段不能为空', color='negative')
dialogAddObjectIcon.enable()
return
if not object_phone.validate():
ui.notify('号码输入有误,请检查!', color='negative')
dialogAddObjectIcon.enable()
return
if object_key.value == "":
object_key.set_value(generate_password())
async with loding_process(
success_content='添加成功',
on_success=on_success,
on_finally=dialogAddObjectIcon.enable()
):
# 正确序列化字符串参数
key = json.dumps(object_key.value)
name = json.dumps(object_name.value)
icon = json.dumps(object_icon.value)
phone = json.dumps(object_phone.value)
result = await ui.run_javascript(
f'addItems({key}, {name}, {icon}, {phone})'
)
if result.get('status') == 'failed':
raise Exception(f"添加失败: {result.get('detail', '未知错误')}")
# 添加物品对话框
with ui.dialog() as addObjectDialog, ui.card().style('width: 90%; max-width: 500px'):
ui.button(icon='add_circle').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
ui.label('添加物品').classes('w-full text-h5 text-center')
with ui.scroll_area().classes('w-full'):
object_name = ui.input('物品名称').classes('w-full')
ui.label('显示的物品名称').classes('-mt-3')
with ui.row(align_items='center').classes('w-full'):
with ui.column().classes('w-1/2 flex-grow'):
object_icon = ui.input('物品图标').classes('w-full')
with ui.row(align_items='center').classes('-mt-3'):
ui.label('将在右侧实时预览图标')
ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons')
ui.icon('').classes('text-2xl flex-grow').bind_name_from(object_icon, 'value')
object_phone = ui.input('物品绑定手机号', validation={'请输入中国大陆格式的11位手机号': lambda value: len(value) == 11 and value.isdigit()}).classes('w-full')
ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
object_key = ui.input('物品Key(可选,不填自动生成)').classes('w-full')
ui.label('物品Key为物品的唯一标识可用于物品找回').classes('-mt-3')
async def handle_add_object():
await addObject()
dialogAddObjectIcon = ui.button("添加并生成二维码", icon='qr_code', on_click=handle_add_object) \
.classes('items-center w-full').props('rounded')
ui.button("返回", on_click=addObjectDialog.close) \
.classes('w-full').props('flat rounded')
async def editObjectPrepare():
'''
读取选中物品的ID并预填充编辑表单
'''
try:
# 获取选中物品的ID
item_id = str(object_table.selected[0]['id'])
id_json = json.dumps(item_id)
result: dict = await ui.run_javascript(f'getItem({id_json})')
if result.get('status') == 'failed':
ui.notify(f"获取物品信息失败: {result.get('detail', '未知错误')}", color='negative')
return
except Exception as e:
ui.notify(f"操作失败: {str(e)}", color='negative')
return
else:
result = result['data']['data'][0]
# 预填充编辑表单
edit_object_name.set_value(result['name'])
edit_object_icon.set_value(result['icon'])
edit_object_phone.set_value(result['phone'])
edit_object_key.set_value(result['key'])
edit_set_object_lost.set_value(result['status'] == 'lost')
lostReason.set_value(result['lost_description'])
editObjectDialog.open()
async def editObject():
dialogEditObjectIcon.disable()
async def on_success():
await reloadTable(tips=False)
edit_object_name.set_value('')
edit_object_icon.set_value('')
edit_object_phone.set_value('')
edit_object_key.set_value('')
lostReason.set_value('')
edit_set_object_lost.set_value(False)
editObjectDialog.close()
if edit_object_name.value == "" or edit_object_icon.value == "" or edit_object_phone.value == "":
ui.notify('必填字段不能为空', color='negative')
dialogEditObjectIcon.enable()
return
if not edit_object_phone.validate():
ui.notify('号码输入有误,请检查!', color='negative')
dialogEditObjectIcon.enable()
return
if edit_object_key.value == "":
ui.notify('物品Key不能为空', color='negative')
dialogEditObjectIcon.enable()
return
async with loding_process(
success_content='更新成功',
on_success=on_success,
on_error=dialogEditObjectIcon.enable(),
on_finally=dialogEditObjectIcon.enable()
):
# 获取选中物品的ID
item_id = str(object_table.selected[0]['id'])
# 正确序列化字符串参数
id_json = json.dumps(item_id)
key = json.dumps(edit_object_key.value)
name = json.dumps(edit_object_name.value)
icon = json.dumps(edit_object_icon.value)
phone = json.dumps(edit_object_phone.value)
# 处理状态和物主留言
status = json.dumps('lost' if edit_set_object_lost.value else 'ok')
context = json.dumps(lostReason.value if edit_set_object_lost.value else '')
result = await ui.run_javascript(
f'updateItems({id_json}, {key}, {name}, {icon}, {phone}, {status}, {context})'
)
if result.get('status') == 'failed':
raise Exception(f"更新失败: {result.get('detail', '未知错误')}")
# 编辑物品对话框
with ui.dialog() as editObjectDialog, ui.card().style('width: 90%; max-width: 500px'):
ui.button(icon='edit').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
ui.label('编辑物品信息').classes('w-full text-h5 text-center')
with ui.scroll_area().classes('w-full'):
edit_object_name = ui.input('物品名称').classes('w-full')
ui.label('显示的物品名称').classes('-mt-3')
with ui.row(align_items='center').classes('w-full'):
with ui.column().classes('w-1/2 flex-grow'):
edit_object_icon = ui.input('物品图标').classes('w-full')
with ui.row(align_items='center').classes('-mt-3'):
ui.label('将在右侧实时预览图标')
ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons')
ui.icon('').classes('text-2xl flex-grow').bind_name_from(edit_object_icon, 'value')
edit_object_phone = ui.input('物品绑定手机号').classes('w-full')
ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
edit_object_key = ui.input('物品Key').classes('w-full').props('readonly')
ui.label('物品Key为物品的唯一标识不可修改').classes('-mt-3')
edit_set_object_lost = ui.switch('设置物品状态为丢失').classes('w-full')
ui.label('确定要设置这个物品为丢失吗?').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3')
ui.html('设置为丢失以后,<b>你的电话号码将会被完整地显示在物品页面</b>(不是“*** **** 8888”而是“188 8888 8888”),以供拾到者能够记下你的电话号码。此外,在页面底部将会显示一个按钮,这个按钮能够一键拨打预先设置好的电话。').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3')
lostReason = ui.input('物主留言') \
.classes('block w-full text-gray-900').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3')
ui.label('非必填,但建议填写,以方便拾到者联系你').classes('-mt-3').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3')
ui.separator().classes('my-4')
with ui.card().classes('w-full bg-red-50 dark:bg-red-900 q-pa-md'):
ui.label('危险区域').classes('text-red-500 font-bold')
ui.button('删除物品', icon='delete_forever') \
.classes('w-full text-red-500').props('flat').on_click(lambda: delete_confirmation_dialog.open())
async def handle_edit_object():
await editObject()
dialogEditObjectIcon = ui.button("确认提交", on_click=handle_edit_object) \
.classes('items-center w-full').props('rounded')
ui.button("返回", on_click=editObjectDialog.close) \
.classes('w-full').props('flat rounded')
# 删除确认对话框
with ui.dialog() as delete_confirmation_dialog, ui.card().style('width: 90%; max-width: 500px'):
ui.button(icon='warning').props('outline round').classes('mx-auto w-auto shadow-sm w-fill text-red-500')
ui.label('确认删除物品').classes('w-full text-h5 text-center text-red-500')
ui.label('此操作不可撤销,删除后物品数据将永久丢失!').classes('w-full text-center text-red-500')
async def handle_delete_item():
async def on_success():
await reloadTable(tips=False)
delete_confirmation_dialog.close()
editObjectDialog.close()
async with loding_process(
success_content='物品已删除',
on_success=on_success
):
# 获取选中物品的ID
item_id = str(object_table.selected[0]['id'])
id_json = json.dumps(item_id)
result = await ui.run_javascript(f'deleteItem({id_json})')
if result.get('status') == 'failed':
raise Exception(f"删除失败: {result.get('detail', '未知错误')}")
with ui.row().classes('w-full'):
ui.space()
ui.button('取消', icon='close', on_click=delete_confirmation_dialog.close).props('flat')
ui.button('确认删除', icon='delete_forever', on_click=handle_delete_item).classes('text-red-500').props('flat')
async def fetch_and_process_objects():
"""获取并处理所有物品数据"""
try:
# 调用前端JavaScript获取数据
response = await ui.run_javascript('getItems()')
if response['status'] == 'failed':
if str(response['detail']).find('Unauthorized'):
ui.notify('未登录或登录已过期,请重新登录', color='negative')
await asyncio.sleep(2)
ui.navigate.to('/login?redirect_to=/admin/items')
ui.notify(response['detail'], color='negative')
return []
# 从response中提取数据
raw_data = response.get('data', {})
objects = raw_data.get('data', []) if isinstance(raw_data, dict) else raw_data
# 进行数据处理,类似旧版本的逻辑
status_map = {'ok': '正常', 'lost': '丢失'}
processed_objects = []
for obj in objects:
# 确保obj是字典
if isinstance(obj, dict):
# 复制对象避免修改原始数据
processed_obj = obj.copy()
# 状态映射
if 'status' in processed_obj:
processed_obj['status'] = status_map.get(processed_obj['status'], processed_obj['status'])
# 时间格式化
if 'create_time' in processed_obj and processed_obj['create_time']:
processed_obj['create_time'] = format_time_diff(processed_obj['create_time'])
if 'lost_time' in processed_obj and processed_obj['lost_time']:
processed_obj['lost_time'] = format_time_diff(processed_obj['lost_time'])
processed_objects.append(processed_obj)
return processed_objects
except Exception as e:
ui.notify(f"获取数据失败: {str(e)}", color='negative')
print(f"Error in fetch_and_process_objects: {str(e)}")
return []
async def reloadTable(tips: bool = True):
objects = await fetch_and_process_objects()
object_table.update_rows(objects)
if tips:
ui.notify('刷新成功')
object_columns = [
{'name': 'id', 'label': '内部ID', 'field': 'id', 'required': True, 'align': 'left'},
{'name': 'key', 'label': '物品Key', 'field': 'key', 'required': True, 'align': 'left'},
{'name': 'name', 'label': '物品名称', 'field': 'name', 'required': True, 'align': 'left'},
{'name': 'icon', 'label': '物品图标', 'field': 'icon', 'required': True, 'align': 'left'},
{'name': 'status', 'label': '物品状态', 'field': 'status', 'required': True, 'align': 'left'},
{'name': 'phone', 'label': '物品绑定手机', 'field': 'phone', 'required': True, 'align': 'left'},
{'name': 'context', 'label': '丢失描述', 'field': 'context', 'required': True, 'align': 'left'},
{'name': 'find_ip', 'label': '物品发现IP', 'field': 'find_ip', 'required': True, 'align': 'left'},
{'name': 'create_time', 'label': '物品创建时间', 'field': 'create_time', 'required': True, 'align': 'left'},
{'name': 'lost_time', 'label': '物品丢失时间', 'field': 'lost_time', 'required': True, 'align': 'left'}
]
objects = await fetch_and_process_objects()
object_table = ui.table(
title='物品 & 库存',
row_key='id',
pagination=10,
selection='single',
columns=object_columns,
rows=objects,
# on_select=lambda: objectTableOnSelect()
).classes('w-full').props('flat')
object_table.add_slot('body-cell-status', '''
<q-td key="status" :props="props">
<q-badge :color="props.value === '正常' ? 'green' : 'red'">
{{ props.value }}
</q-badge>
</q-td>
''')
with object_table.add_slot('top-right'):
ui.input('搜索物品').classes('px-2') \
.bind_value(object_table, 'filter') \
.props('rounded outlined dense clearable')
ui.button(icon='refresh', on_click=lambda: reloadTable()).classes('px-2').props('flat fab-mini')
with ui.button(icon='menu').classes('px-2').props('flat fab-mini'):
with ui.menu(), ui.column().classes('gap-0 p-4'):
for column in object_columns:
ui.switch(column['label'], value=True, on_change=lambda e,
column=column: tableToggle(column=column, visible=e.value, table=object_table))
# FAB按钮
with ui.page_sticky(x_offset=24, y_offset=24) \
.bind_visibility_from(object_table, 'selected', backward=lambda x: not x) as addObjectFAB:
ui.button(icon='add', on_click=addObjectDialog.open) \
.props('fab')
with ui.page_sticky(x_offset=24, y_offset=24) \
.bind_visibility_from(addObjectFAB, 'visible', backward=lambda x: not x) as editObjectFAB:
ui.button(icon='edit', on_click=editObjectPrepare) \
.props('fab')

View File

@@ -1,124 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-11-29 20:03:58
FilePath: /Findreve/found.py
Description: Findreve 物品详情页 found
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from fastapi import Request
from .framework import frame
from tool import format_phone
def create_header(object_data, status):
"""创建卡片标题部分"""
icon_color = 'red' if status == 'lost' else None
ui.button(icon=object_data[3], color=icon_color).props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden')
title = ui.label('关于此 '+ object_data[2]).classes('text-h5 w-full text-center')
if status == 'lost':
with title:
ui.badge('已被标记为丢失 Already lost', color='red').classes('text-lg -right-10').props('floating')
ui.label('About this '+ object_data[2]).classes('text-xs w-full text-center text-gray-500 -mt-3')
def create_basic_info(object_data):
"""创建基本信息部分"""
ui.label('序列号(Serial number)'+ object_data[1]).classes('text-md w-full text-center -mt-1')
# 根据状态决定是否隐藏手机号
is_private = object_data[4] != 'ok' and object_data[4] != 'lost'
ui.label('物主(Owner)'+ format_phone(object_data[5], private=is_private)).classes('text-md w-full text-center -mt-3')
# 丢失时间(如果有)
if object_data[4] == 'lost' and len(object_data) > 9 and object_data[9]:
ui.label('丢失时间(Lost time)'+ object_data[9]).classes('text-md w-full text-center -mt-3')
def create_status_message(object_data, status):
"""根据状态创建提示信息"""
ui.space()
# 如果是丢失状态且有留言,显示留言
if status == 'lost' and len(object_data) > 6 and object_data[6]:
ui.label('物主留言(Owner message)'+ object_data[6]).classes('text-md w-full text-center')
ui.space()
messages = {
'ok': ('此物品尚未标记为丢失状态。如果你意外捡到了此物品,请尽快联系物主。',
'This item has not been marked as lost. If you accidentally picked it up, please contact the owner as soon as possible.'),
'lost': ('此物品已被物主标记为丢失。您可以通过上面的电话号码来联系物主。',
'This item has been marked as lost by the owner. You can contact the owner through the phone number above.'),
'default': ('此物品状态信息已丢失。如果您捡到了这个物品,请尽快联系物主。如果你是物主,请修改物品信息状态。',
'The item status information has been lost. If you have found this item, please contact the owner as soon as possible. If you are the owner, please modify the item status information.')
}
msg = messages.get(status, messages['default'])
ui.label(msg[0]).classes('text-md w-full text-center')
ui.label(msg[1]).classes('text-xs w-full text-center text-gray-500 -mt-3')
def create_contact_button(phone_number):
"""创建联系按钮"""
if phone_number:
ui.button('联系物主',
on_click=lambda: ui.navigate.to('tel:' + phone_number)) \
.classes('items-center w-full').props('rounded')
def display_item_card(object_data):
"""显示物品信息卡片"""
status = object_data[4]
with ui.card().classes('absolute-center w-3/4 h-3/4'):
# 创建卡片各部分
create_header(object_data, status)
create_basic_info(object_data)
create_status_message(object_data, status)
# 只有状态为'ok'或'lost'时显示联系按钮
if status in ['ok', 'lost']:
create_contact_button(object_data[5])
def create() -> None:
@ui.page('/found')
async def found_page(request: Request, key: str = "") -> None:
ui.add_head_html(
'''
<meta name="robots" content="noindex, nofollow">
<script type="text/javascript" src="/static/js/main.js"></script>
'''
)
await ui.context.client.connected()
async with frame(page='found', request=request):
if key == "" or key == None:
ui.navigate.to('/404')
return
# 加载dialog
with ui.dialog().props('persistent') as loading, ui.card():
with ui.row(align_items='center'):
ui.spinner(size='lg')
with ui.column():
ui.label('数据加载中...')
ui.label('Loading...').classes('text-xs text-gray-500 -mt-3')
loading.open()
try:
object_data = await ui.run_javascript(f'getObject("{key}")')
if object_data['status'] != 'success':
ui.navigate.to('/404')
else:
object_data = object_data['data']
display_item_card(object_data)
except Exception as e:
ui.notify(f'加载失败: {str(e)}', color='negative')
ui.navigate.to('/404')
finally:
loading.close()

View File

@@ -1,114 +0,0 @@
from contextlib import asynccontextmanager
from nicegui import ui
import asyncio
from fastapi import Request
from typing import Optional, Literal
@asynccontextmanager
async def frame(
request: Request = None,
page: Literal['admin', 'session', 'found'] = 'admin',
redirect_to: str = None
):
ui.add_head_html("""
<script type="text/javascript" src="/static/js/main.js"></script>
""")
await ui.context.client.connected()
is_login = await ui.run_javascript('is_login()', timeout=3)
if str(is_login).lower() != 'true':
if page not in ['session', 'found']:
ui.navigate.to(f'/login?redirect_to={request.url.path}')
else:
if page == 'session':
ui.navigate.to(redirect_to)
if page != 'found':
ui.dark_mode(value=True)
with ui.header() \
.classes('items-center py-2 px-5 no-wrap').props('elevated'):
ui.button(icon='menu', on_click=lambda: left_drawer.toggle()).props('flat color=white round')
ui.button(text="Findreve 仪表盘" if page == 'admin' else "Findreve").classes('text-lg').props('flat color=white no-caps')
ui.space()
if str(is_login).lower() == 'true':
ui.button(icon='logout', on_click=lambda: ui.run_javascript('logout()')) \
.props('flat color=white fab-mini').tooltip('退出登录')
with ui.left_drawer() as left_drawer:
with ui.column(align_items='center').classes('w-full'):
ui.image('/static/Findreve.png').classes('w-1/3 mx-auto')
ui.label('Findreve').classes('text-2xl text-bold')
ui.label("免费版,无需授权").classes('text-sm text-gray-500')
if page == 'admin':
ui.button('首页 & 信息', icon='fingerprint', on_click=lambda: ui.navigate.to('/admin/home')) \
.classes('w-full').props('flat no-caps')
ui.button('物品 & 库存', icon='settings', on_click=lambda: ui.navigate.to('/admin/items')) \
.classes('w-full').props('flat no-caps')
ui.button('产品 & 授权', icon='settings', on_click=lambda: ui.navigate.to('/admin/auth')) \
.classes('w-full').props('flat no-caps')
ui.button('关于 & 反馈', icon='settings', on_click=lambda: ui.navigate.to('/admin/about')) \
.classes('w-full').props('flat no-caps')
if page == 'found':
left_drawer.hide()
with ui.column().classes('w-full'):
yield
@asynccontextmanager
async def loding_process(
content: str = '正在处理,请稍后...',
success_content: str = '操作成功',
error_content: str = '操作失败',
on_success: Optional[callable] = None,
on_error: Optional[callable] = None,
on_finally: Optional[callable] = None
):
"""
加载提示框
:param content: 提示内容
:param success_content: 成功提示内容
:param error_content: 失败提示内容
:param on_success: 成功回调函数
:param on_error: 失败回调函数
~~~
使用方法
>>> async with loding_process():
# 处理代码
"""
notify = ui.notification(
message=content,
timeout=None
)
notify.spinner = True
try:
yield
except Exception as e:
notify.spinner = False
notify.type = 'negative'
notify.message = error_content + ':' + str(e)
await asyncio.sleep(3)
notify.dismiss()
if on_error:
await on_error(e)
else:
notify.spinner = False
notify.type = 'positive'
notify.message = success_content
if on_success:
await on_success()
await asyncio.sleep(3)
notify.dismiss()
finally:
if on_finally:
await on_finally()

View File

@@ -1,56 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-11-29 20:29:26
FilePath: /Findreve/login.py
Description: Findreve 登录界面 Login
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from typing import Optional
from fastapi.responses import RedirectResponse
from .framework import frame
def create() -> Optional[RedirectResponse]:
@ui.page('/login')
async def session(redirect_to: str = "/"):
ui.add_head_html("""
<script type="text/javascript" src="/static/js/main.js"></script>
""")
ui.page_title('登录 Findreve')
async with frame(page='session', redirect_to=redirect_to):
await ui.context.client.connected()
async def login():
if username.value == "" or password.value == "":
ui.notify('账号或密码不能为空', color='negative')
return
try:
result = await ui.run_javascript(f"login('{username.value}', '{password.value}')")
if result['status'] == 'success':
ui.navigate.to(redirect_to)
else:
ui.notify(f"登录失败: {result['detail']}", type="negative")
except Exception as e:
ui.notify(f"登录失败: {str(e)}", type="negative")
# 创建一个绝对中心的登录卡片
with ui.card().classes('absolute-center round-lg').style('width: 70%; max-width: 500px'):
# 登录标签
ui.button(icon='lock').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
ui.label('登录 Findreve').classes('text-h5 w-full text-center')
# 用户名/密码框
username = ui.input('账号').on('keydown.enter', login) \
.classes('block w-full text-gray-900').props('filled')
password = ui.input('密码', password=True, password_toggle_button=True) \
.on('keydown.enter', login).classes('block w-full text-gray-900').props('filled')
# 按钮布局
ui.button('登录', on_click=lambda: login()).classes('items-center w-full').props('rounded')

View File

@@ -1,151 +0,0 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-11-29 20:04:24
FilePath: /Findreve/main_page.py
Description: Findreve 个人主页 main_page
Copyright (c) 2018-2025 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from fastapi import Request
def create_chip(name: str, color: str, tooltip: str) -> ui.chip:
"""Create a UI chip with tooltip"""
return ui.chip(name, color=color).classes('p-4').props('floating').tooltip(tooltip)
def create() -> None:
@ui.page('/')
async def main_page(request: Request) -> None:
dark_mode = ui.dark_mode(value=True)
# 添加页面过渡动画
ui.add_head_html('''
<style type="text/css" src="/static/css/main_page.css"></style>
''')
with ui.row(align_items='center').classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl p-24'):
with ui.column(align_items='center').classes('px-2 max-md:hidden'):
ui.chip('🐍 Python 是最好的语言').classes('text-xs -mt-1 -right-3').props('floating outline')
ui.chip('🎹 精通 FL Studio Mobile').classes('text-xs -mt-1').props('floating outline')
ui.chip('🎨 熟悉 Ps/Pr/Ae/Au/Ai').classes('text-xs -mt-1').props('floating outline')
ui.chip('🏎 热爱竞速(如地平线5)').classes('text-xs -mt-1 -right-3').props('floating outline')
with ui.avatar().classes('w-32 h-32 transition-transform duration-300 hover:scale-110 cursor-pointer'):
ui.image('/static/heyfun.jpg').classes('w-32 h-32')
with ui.column().classes('px-2 max-md:hidden'):
ui.chip('喜欢去广州图书馆看书 📕').classes('text-xs -mt-1 -left-3').props('floating outline')
ui.chip('致力做安卓苹果开发者 📱').classes('text-xs -mt-1').props('floating outline')
ui.chip('正在自研全链个人生态 🔧').classes('text-xs -mt-1').props('floating outline')
ui.chip('致力与开源社区同发展 🤝').classes('text-xs -mt-1 -left-3').props('floating outline')
ui.label('关于本站').classes('w-full text-4xl text-bold text-center py-6 subpixel-antialiased')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl py-4'):
with ui.card().classes('w-full sm:w-1/5 lg:w-1/7 flex-grow p-8 bg-gradient-to-br from-indigo-700 to-blue-500'):
ui.label('你好,很高兴认识你👋').classes('text-md text-white')
with ui.row(align_items='center'):
ui.label('我叫').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased')
ui.label('于小丘').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased').tooltip('英文名叫Yuerchu也可以叫我海枫')
ui.label('是一名 开发者、音乐人').classes('text-md text-white -mt-1')
with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow flex flex-col justify-center'):
ui.code('void main() {\n printf("为了尚未完成的未来");\n}', language='c').classes('text-3xl max-[768px]:text-xl text-bold text-white flex-grow w-full h-full')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl -mt-3'):
with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow p-4'):
ui.label('技能').classes('text-md text-gray-500')
ui.label('开启创造力').classes('text-4xl text-bold -mt-1 right-4')
with ui.row().classes('items-center'):
create_chip('Python', 'amber-400', 'Python是世界上最好的语言')
create_chip('Kotlin', 'violet-400', 'Kotlin给安卓开发APP')
create_chip('Golang', 'sky-400', 'Golang写后端')
create_chip('Lua', 'blue-900', '用aLua给安卓开发给罗技鼠标写鼠标宏')
create_chip('c', 'red-400', 'C写嵌入式开发')
create_chip('FL Studio', 'orange-600', 'FL Studio是世界上最好的宿主')
create_chip('Photoshop', 'blue-950', '修图/抠图/画画一站通')
create_chip('Premiere', 'indigo-900', '剪视频比较顺手,但是一开风扇狂转')
create_chip('After Effects', 'indigo-950', '制作特效,电脑太烂了做不了太花的')
create_chip('Audition', 'purple-900', '写歌做母带挺好用的')
create_chip('Illustrator', 'amber-800', '自制字体和画动态SVG')
create_chip('HTML', 'red-900', '前端入门三件套,不学这玩意其他学了没用')
create_chip('CSS3', 'cyan-900', '. window{ show: none; }')
create_chip('JavaScript', 'lime-900', '还在努力学习中,只会一些简单的')
create_chip('git', 'amber-700', '版本管理是真好用')
create_chip('Docker', 'sky-600', '容器化部署')
create_chip('chatGPT', 'emerald-600', '文本助驾,写代码/写文章/写论文')
create_chip('SAI2', 'gray-950', '入门绘画')
create_chip('ips Draw', 'gray-900', '自认为是iOS端最佳绘画软件')
create_chip('AutoCAD', 'gray-950', '画图/绘制电路图')
create_chip('SolidWorks', 'gray-900', '画图/绘制3D模型')
create_chip('EasyEDA', 'gray-950', '画图/绘制电路图')
create_chip('KiCad', 'gray-900', '画图/绘制电路图')
create_chip('Altium Designer', 'gray-950', '画图/绘制电路图')
ui.label('...').classes('text-md text-gray-500')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/6 flex-grow flex flex-col justify-center'):
ui.label('生涯').classes('text-md text-gray-500')
ui.label('无限进步').classes('text-4xl text-bold -mt-1 right-4')
with ui.timeline(side='right', layout='comfortable'):
ui.timeline_entry('那天我买了第一台服务器并搭建了我第一个Wordpress站点',
title='梦开始的地方',
subtitle='2022年1月21日')
ui.timeline_entry('准备从Cloudreve项目脱离自建网盘系统DiskNext',
title='自建生态计划开始',
subtitle='2024年3月1日')
ui.timeline_entry('目前正在开发HeyAuth、Findreve、DiskNext',
title='项目框架仍在研发中',
subtitle='现在',
icon='rocket')
ui.label('我的作品').classes('w-full text-center text-2xl text-bold p-4')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'):
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('DiskNext').classes('text-lg text-bold')
ui.chip('B端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://pan.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的网盘系统性能与Golang媲美').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('Findreve').classes('text-lg text-bold')
ui.chip('C端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://i.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的个人主页配合物品丢失找回系统').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('HeyAuth').classes('text-lg text-bold')
ui.chip('B端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://auth.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的B+C端多应用授权系统').classes('text-sm -mt-3')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'):
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('与枫同奔 Run With Fun').classes('text-lg text-bold')
ui.chip('词曲').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=2148944359'))).props('flat fab-mini')
ui.label('我愿如流星赶月那样飞奔').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('HeyFun\'s Story').classes('text-lg text-bold')
ui.chip('自设印象曲').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1889436124'))).props('flat fab-mini')
ui.label('飞奔在星辰大海之间的少年').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('2020Fall').classes('text-lg text-bold')
ui.chip('年度纯音乐').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1863630345'))).props('flat fab-mini')
ui.label('耗时6个月完成的年度纯音乐').classes('text-sm -mt-3')

View File

@@ -2,12 +2,18 @@ import random
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from model.database import Database
from model.response import DefaultResponse
from model.response import DefaultResponse, ObjectData
import asyncio
Router = APIRouter(prefix='/api/object', tags=['object'])
Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
@Router.get('/{item_key}')
@Router.get(
path='/{item_key}',
summary="获取物品信息",
description="根据物品键获取物品信息",
response_model=DefaultResponse,
response_description="物品信息"
)
async def get_object(item_key: str, request: Request):
"""
获取物品信息 / Get object information
@@ -27,9 +33,15 @@ async def get_object(item_key: str, request: Request):
else:
await asyncio.sleep(random.uniform(0.10, 0.30))
return DefaultResponse(
data=object_data
)
return DefaultResponse(data=ObjectData(
id=object_data[0],
key=object_data[1],
name=object_data[2],
icon=object_data[3],
status=object_data[4],
phone=object_data[5],
context=object_data[6]
).model_dump())
else: return JSONResponse(
status_code=404,
content=DefaultResponse(

View File

@@ -1,4 +1,4 @@
from nicegui import app
# 导入库
from typing import Annotated
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, status
@@ -10,8 +10,9 @@ from model.token import Token
from model import database
from tool import verify_password
Router = APIRouter()
Router = APIRouter(tags=["令牌 session"])
# 创建令牌
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
@@ -22,6 +23,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
encoded_jwt = jwt.encode(to_encode, JWT.SECRET_KEY, algorithm='HS256')
return encoded_jwt
# 验证账号密码
async def authenticate_user(username: str, password: str):
# 验证账号和密码
account = await database.Database().get_setting('account')
@@ -33,7 +35,13 @@ async def authenticate_user(username: str, password: str):
return {'is_authenticated': True}
# FastAPI 登录路由 / FastAPI login route
@app.post("/api/token")
@Router.post(
path="/api/token",
summary="获取访问令牌",
description="使用用户名和密码获取访问令牌",
response_model=Token,
response_description="访问令牌"
)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token: