V1.2.0
This commit is contained in:
@@ -1,366 +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 typing import Dict
|
||||
import model
|
||||
import qrcode
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from fastapi import Request
|
||||
import model.database
|
||||
from tool import *
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def create():
|
||||
@ui.page('/admin')
|
||||
async def admin(request: Request):
|
||||
|
||||
dark_mode = ui.dark_mode(value=True)
|
||||
|
||||
# 表格列的显示隐藏开关
|
||||
def tableToggle(column: Dict, visible: bool, table) -> None:
|
||||
column['classes'] = '' if visible else 'hidden'
|
||||
column['headerClasses'] = '' if visible else 'hidden'
|
||||
table.update()
|
||||
|
||||
with ui.header() \
|
||||
.classes('items-center duration-300 py-2 px-5 no-wrap') \
|
||||
.style('box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1)'):
|
||||
with ui.tabs(value='main_page') as tabs:
|
||||
ui.button(icon='menu', on_click=lambda: left_drawer.toggle()).props('flat color=white round')
|
||||
ui.button(text="Findreve 仪表盘").classes('text-lg').props('flat color=white no-caps')
|
||||
|
||||
siteDomain = request.base_url.hostname
|
||||
with ui.left_drawer() as left_drawer:
|
||||
with ui.column().classes('w-full'):
|
||||
ui.image('/static/Findreve.png').classes('w-1/2 mx-auto')
|
||||
with ui.row(align_items='center').classes('w-full'):
|
||||
ui.label('Findreve').classes('text-2xl text-bold')
|
||||
if siteDomain == "127.0.0.1" or siteDomain == "localhost":
|
||||
ui.label("本地模式无需授权").classes('text-gray-600 -mt-3')
|
||||
else:
|
||||
ui.label("免费版,无需授权").classes('text-red-600 -mt-3')
|
||||
|
||||
ui.button('首页 & 信息', icon='fingerprint', on_click=lambda: tabs.set_value('main_page')) \
|
||||
.classes('w-full').props('flat no-caps')
|
||||
ui.button('物品 & 库存', icon='settings', on_click=lambda: tabs.set_value('item')) \
|
||||
.classes('w-full').props('flat no-caps')
|
||||
ui.button('产品 & 授权', icon='settings', on_click=lambda: tabs.set_value('auth')) \
|
||||
.classes('w-full').props('flat no-caps')
|
||||
ui.button('关于 & 反馈', icon='settings', on_click=lambda: tabs.set_value('about')) \
|
||||
.classes('w-full').props('flat no-caps')
|
||||
|
||||
with ui.tab_panels(tabs, value='main_page').classes('w-full').props('vertical'):
|
||||
# 站点一览
|
||||
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')
|
||||
|
||||
# 物品一览
|
||||
with ui.tab_panel('item'):
|
||||
|
||||
# 列表选择函数
|
||||
async def objectTableOnClick():
|
||||
try:
|
||||
status = str(object_table.selected[0]['status'])
|
||||
except:
|
||||
# 当物品列表未选中,显示添加物品按钮,其他按钮不显示
|
||||
addObjectFAB.set_visibility(True)
|
||||
lostObjectFAB.set_visibility(False)
|
||||
findObjectFAB.set_visibility(False)
|
||||
return
|
||||
|
||||
if status == "正常":
|
||||
# 选中正常物品,显示丢失按钮
|
||||
addObjectFAB.set_visibility(False)
|
||||
lostObjectFAB.set_visibility(True)
|
||||
findObjectFAB.set_visibility(False)
|
||||
elif status == "丢失":
|
||||
# 选中丢失物品,显示发现按钮
|
||||
addObjectFAB.set_visibility(False)
|
||||
lostObjectFAB.set_visibility(False)
|
||||
findObjectFAB.set_visibility(True)
|
||||
else:
|
||||
# 选中其他状态,隐藏所有按钮
|
||||
addObjectFAB.set_visibility(False)
|
||||
lostObjectFAB.set_visibility(False)
|
||||
findObjectFAB.set_visibility(False)
|
||||
|
||||
# 添加物品
|
||||
async def addObject():
|
||||
dialogAddObjectIcon.disable()
|
||||
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())
|
||||
|
||||
try:
|
||||
await model.database.Database().add_object(key=object_key.value, name=object_name.value, icon=object_icon.value, phone=object_phone.value)
|
||||
except ValueError as e:
|
||||
ui.notify(str(e), color='negative')
|
||||
else:
|
||||
await reloadTable(tips=False)
|
||||
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()
|
||||
|
||||
# 展示二维码
|
||||
ui.image(f'data:image/png;base64,{img_str}')
|
||||
|
||||
# 添加下载二维码按钮
|
||||
ui.button("下载二维码", on_click=lambda: ui.download(buffered.getvalue(), 'qrcode.png')) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
ui.button("返回", on_click=lambda: (addObjectDialog.close(), addObjectSuccessDialog.close(), addObjectSuccessDialog.delete())) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
addObjectSuccessDialog.open()
|
||||
|
||||
|
||||
|
||||
# 添加物品对话框
|
||||
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')
|
||||
object_name_tips = 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('将在右侧实时预览图标')
|
||||
object_icon_link = ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons')
|
||||
object_icon_preview = 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')
|
||||
object_phone_tips = ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
|
||||
object_key = ui.input('物品Key(可选,不填自动生成)').classes('w-full')
|
||||
object_key_tips = 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 lostObject():
|
||||
try:
|
||||
# 获取选中物品
|
||||
object_id = object_table.selected[0]['id']
|
||||
await model.database.Database().update_object(id=object_id, status='lost')
|
||||
await model.database.Database().update_object(id=object_id, lost_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
# 如果设置了留言,则更新留言
|
||||
if lostReason.value != "":
|
||||
await model.database.Database().update_object(id=object_id, context=lostReason.value)
|
||||
except Exception as e:
|
||||
ui.notify(str(e), color='negative')
|
||||
else:
|
||||
ui.notify('设置丢失成功', color='positive')
|
||||
# 刷新表格
|
||||
await reloadTable(tips=False)
|
||||
lostObjectDialog.close()
|
||||
# 将FAB设置为正常
|
||||
addObjectFAB.set_visibility(True)
|
||||
lostObjectFAB.set_visibility(False)
|
||||
findObjectFAB.set_visibility(False)
|
||||
|
||||
|
||||
# 设置物品丢失对话框
|
||||
with ui.dialog() as lostObjectDialog, ui.card().style('width: 90%; max-width: 500px'):
|
||||
ui.button(icon='gpp_bad', color='red').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
|
||||
ui.label('设置物品丢失').classes('w-full text-h5 text-center')
|
||||
|
||||
ui.label('确定要设置这个物品为丢失吗?')
|
||||
ui.html('设置为丢失以后,<b>你的电话号码将会被完整地显示在物品页面</b>(不是“*** **** 8888”而是“188 8888 8888”),以供拾到者能够记下你的电话号码。此外,在页面底部将会显示一个按钮,这个按钮能够一键拨打预先设置好的电话。')
|
||||
lostReason = ui.input('物主留言') \
|
||||
.classes('block w-full text-gray-900')
|
||||
lostReasonTips = ui.label('非必填,但建议填写,以方便拾到者联系你').classes('-mt-3')
|
||||
|
||||
async def handle_lost_object():
|
||||
await lostObject()
|
||||
|
||||
ui.button("确认提交", color='red', on_click=handle_lost_object) \
|
||||
.classes('items-center w-full').props('rounded')
|
||||
ui.button("返回", on_click=lostObjectDialog.close) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
async def findObject():
|
||||
try:
|
||||
object_id = object_table.selected[0]['id']
|
||||
await model.database.Database().update_object(id=object_id, status='ok')
|
||||
await model.database.Database().update_object(id=object_id, context=None)
|
||||
await model.database.Database().update_object(id=object_id, find_ip=None)
|
||||
await model.database.Database().update_object(id=object_id, lost_at=None)
|
||||
except Exception as e:
|
||||
ui.notify(str(e), color='negative')
|
||||
else:
|
||||
ui.notify('解除丢失成功', color='positive')
|
||||
# 刷新表格
|
||||
await reloadTable(tips=False)
|
||||
findObjectDialog.close()
|
||||
# 将FAB设置为正常
|
||||
addObjectFAB.set_visibility(True)
|
||||
lostObjectFAB.set_visibility(False)
|
||||
findObjectFAB.set_visibility(False)
|
||||
|
||||
# 解除丢失对话框
|
||||
with ui.dialog() as findObjectDialog, ui.card().style('width: 90%; max-width: 500px'):
|
||||
ui.button(icon='remove_moderator').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
|
||||
ui.label('解除丢失').classes('w-full text-h5 text-center')
|
||||
|
||||
ui.label('确定物品已经找回了吗?')
|
||||
|
||||
async def handle_find_object():
|
||||
await findObject()
|
||||
|
||||
ui.button("确认提交", on_click=handle_find_object) \
|
||||
.classes('items-center w-full').props('rounded')
|
||||
ui.button("返回") \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
async def fetch_and_process_objects():
|
||||
# 获取所有物品
|
||||
objects = [dict(zip(['id', 'key', 'name', 'icon', 'status', 'phone', 'context',
|
||||
'find_ip', 'create_at', 'lost_at'], obj)) for obj in await model.database.Database().get_object()]
|
||||
status_map = {'ok': '正常', 'lost': '丢失'}
|
||||
for obj in objects:
|
||||
obj['status'] = status_map.get(obj['status'], obj['status'])
|
||||
if obj['create_at']:
|
||||
obj['create_at'] = format_time_diff(obj['create_at'])
|
||||
if obj['lost_at']:
|
||||
obj['lost_at'] = format_time_diff(obj['lost_at'])
|
||||
return objects
|
||||
|
||||
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_at', 'label': '物品创建时间', 'field': 'create_at', 'required': True, 'align': 'left'},
|
||||
{'name': 'lost_at', 'label': '物品丢失时间', 'field': 'lost_at', '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: objectTableOnClick()
|
||||
).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) as addObjectFAB:
|
||||
ui.button(icon='add', on_click=addObjectDialog.open) \
|
||||
.props('fab')
|
||||
with ui.page_sticky(x_offset=24, y_offset=24) as lostObjectFAB:
|
||||
ui.button(icon='gpp_bad', color='red', on_click=lostObjectDialog.open) \
|
||||
.props('fab')
|
||||
# 单独拉出来默认隐藏,防止无法再设置其显示
|
||||
lostObjectFAB.set_visibility(False)
|
||||
with ui.page_sticky(x_offset=24, y_offset=24) as findObjectFAB:
|
||||
ui.button(icon='remove_moderator', on_click=findObjectDialog.open) \
|
||||
.props('fab')
|
||||
# 单独拉出来默认隐藏,防止无法再设置其显示
|
||||
findObjectFAB.set_visibility(False)
|
||||
|
||||
# Findreve 授权
|
||||
with ui.tab_panel('auth'):
|
||||
|
||||
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')
|
||||
|
||||
# 关于 Findreve
|
||||
with ui.tab_panel('about'):
|
||||
ui.label('关于 Findreve')
|
||||
4
routes/frontend/admin/__init__.py
Normal file
4
routes/frontend/admin/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import about
|
||||
from . import auth
|
||||
from . import home
|
||||
from . import items
|
||||
90
routes/frontend/admin/about.py
Normal file
90
routes/frontend/admin/about.py
Normal file
@@ -0,0 +1,90 @@
|
||||
'''
|
||||
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 ..framework import frame
|
||||
|
||||
|
||||
def create():
|
||||
@ui.page('/admin/about')
|
||||
async def admin_about():
|
||||
|
||||
dark_mode = ui.dark_mode(value=True)
|
||||
|
||||
ui.add_head_html(
|
||||
code="""
|
||||
<style>
|
||||
a:link:not(.browser-window *),
|
||||
a:visited:not(.browser-window *) {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover:not(.browser-window *),
|
||||
a:active:not(.browser-window *) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.bold-links a:link {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.arrow-links a:link:not(.auto-link)::after {
|
||||
content: "north_east";
|
||||
font-family: "Material Icons";
|
||||
font-weight: 100;
|
||||
vertical-align: -10%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
async function get_about() {
|
||||
try {
|
||||
const response = await fetch('/static/readme.md', {
|
||||
method: 'GET',
|
||||
cache: 'no-cache' // 防止缓存问题
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('获取readme失败:', response.status);
|
||||
return '无法加载内容: ' + response.statusText;
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取readme错误:', error);
|
||||
return '加载错误: ' + error.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
async with frame():
|
||||
|
||||
# 关于 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)}')
|
||||
|
||||
43
routes/frontend/admin/auth.py
Normal file
43
routes/frontend/admin/auth.py
Normal file
@@ -0,0 +1,43 @@
|
||||
'''
|
||||
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 ..framework import frame
|
||||
|
||||
|
||||
def create():
|
||||
@ui.page('/admin/auth')
|
||||
async def admin_auth():
|
||||
|
||||
dark_mode = ui.dark_mode(value=True)
|
||||
|
||||
# Findreve 授权
|
||||
async with frame():
|
||||
|
||||
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')
|
||||
30
routes/frontend/admin/home.py
Normal file
30
routes/frontend/admin/home.py
Normal file
@@ -0,0 +1,30 @@
|
||||
'''
|
||||
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 ..framework import frame
|
||||
|
||||
|
||||
def create():
|
||||
@ui.page('/admin')
|
||||
async def jump():
|
||||
ui.navigate.to('/admin/home')
|
||||
|
||||
@ui.page('/admin/home')
|
||||
async def admin_home():
|
||||
|
||||
dark_mode = ui.dark_mode(value=True)
|
||||
|
||||
async with frame():
|
||||
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')
|
||||
541
routes/frontend/admin/items.py
Normal file
541
routes/frontend/admin/items.py
Normal file
@@ -0,0 +1,541 @@
|
||||
'''
|
||||
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 model
|
||||
import qrcode
|
||||
import base64
|
||||
import json
|
||||
from io import BytesIO
|
||||
from fastapi import Request
|
||||
import model.database
|
||||
from tool import *
|
||||
from datetime import datetime
|
||||
from ..framework import frame
|
||||
|
||||
|
||||
def create():
|
||||
@ui.page('/admin/items')
|
||||
async def admin_items(request: Request):
|
||||
|
||||
ui.add_head_html(
|
||||
'''
|
||||
<script>
|
||||
async function getItems() {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
|
||||
if (!accessToken) {
|
||||
return {'status': 'failed', 'detail': 'Access token not found'};
|
||||
}
|
||||
|
||||
const url = '/api/admin/items';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {'status': 'success', 'data': data};
|
||||
|
||||
} catch (error) {
|
||||
return {'status': 'failed', 'detail': error.message};
|
||||
}
|
||||
}
|
||||
|
||||
async function addItems(key, name, icon, phone) {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
|
||||
// 构建带参数的URL
|
||||
const url = `/api/admin/items?key=${encodeURIComponent(key)}&name=${encodeURIComponent(name)}&icon=${encodeURIComponent(icon)}&phone=${encodeURIComponent(phone)}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'accept': 'application/json'
|
||||
},
|
||||
body: ''
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {'status': 'success', 'data': result};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Add items error:', error);
|
||||
return {'status': 'failed', 'detail': error.message};
|
||||
}
|
||||
}
|
||||
|
||||
async function updateItems(id, key, name, icon, phone, status, context) {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
|
||||
// 通过URL查询参数发送数据
|
||||
let url = `/api/admin/items?id=${encodeURIComponent(id)}&key=${encodeURIComponent(key)}&name=${encodeURIComponent(name)}&icon=${encodeURIComponent(icon)}&phone=${encodeURIComponent(phone)}`;
|
||||
|
||||
// 添加状态参数
|
||||
if (status) {
|
||||
url += `&status=${encodeURIComponent(status)}`;
|
||||
}
|
||||
|
||||
// 只在有context且status为lost时添加context参数
|
||||
if (context && status === 'lost') {
|
||||
url += `&context=${encodeURIComponent(context)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'accept': 'application/json'
|
||||
},
|
||||
body: ''
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {'status': 'success', 'data': result};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Update items error:', error);
|
||||
return {'status': 'failed', 'detail': error.message};
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(id) {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
|
||||
// 构建URL,带上物品id参数
|
||||
const url = `/api/admin/items?id=${encodeURIComponent(id)}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {'status': 'success', 'data': result};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Delete item error:', error);
|
||||
return {'status': 'failed', 'detail': error.message};
|
||||
}
|
||||
}
|
||||
</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('context', ''))
|
||||
except:
|
||||
# 当物品列表未选中,显示添加物品按钮,其他按钮不显示
|
||||
addObjectFAB.set_visibility(True)
|
||||
return
|
||||
|
||||
# 添加物品
|
||||
async def addObject():
|
||||
dialogAddObjectIcon.disable()
|
||||
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())
|
||||
|
||||
try:
|
||||
# 正确序列化字符串参数
|
||||
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':
|
||||
ui.notify(f"添加失败: {result.get('detail', '未知错误')}", color='negative')
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
ui.notify(f"操作失败: {str(e)}", color='negative')
|
||||
return
|
||||
else:
|
||||
await reloadTable(tips=False)
|
||||
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()
|
||||
|
||||
# 展示二维码
|
||||
ui.image(f'data:image/png;base64,{img_str}')
|
||||
|
||||
# 添加下载二维码按钮
|
||||
ui.button("下载二维码", on_click=lambda: ui.download(buffered.getvalue(), 'qrcode.png')) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
ui.button("返回", on_click=lambda: (addObjectDialog.close(), addObjectSuccessDialog.close(), addObjectSuccessDialog.delete())) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
|
||||
addObjectSuccessDialog.open()
|
||||
finally:
|
||||
dialogAddObjectIcon.enable()
|
||||
|
||||
|
||||
|
||||
# 添加物品对话框
|
||||
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')
|
||||
object_name_tips = 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('将在右侧实时预览图标')
|
||||
object_icon_link = ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons')
|
||||
object_icon_preview = 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')
|
||||
object_phone_tips = ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
|
||||
object_key = ui.input('物品Key(可选,不填自动生成)').classes('w-full')
|
||||
object_key_tips = 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 editObject():
|
||||
dialogEditObjectIcon.disable()
|
||||
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
|
||||
|
||||
try:
|
||||
# 获取选中物品的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':
|
||||
ui.notify(f"更新失败: {result.get('detail', '未知错误')}", color='negative')
|
||||
dialogEditObjectIcon.enable()
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
ui.notify(f"操作失败: {str(e)}", color='negative')
|
||||
return
|
||||
else:
|
||||
await reloadTable(tips=True)
|
||||
editObjectDialog.close()
|
||||
status_msg = "物品已设置为丢失" if edit_set_object_lost.value else "物品信息更新成功"
|
||||
ui.notify(status_msg, color='positive')
|
||||
finally:
|
||||
dialogEditObjectIcon.enable()
|
||||
|
||||
|
||||
# 设置物品丢失对话框
|
||||
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')
|
||||
edit_object_icon_preview = 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')
|
||||
lostReasonTips = 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')
|
||||
delete_btn = 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():
|
||||
try:
|
||||
# 获取选中物品的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':
|
||||
ui.notify(f"删除失败: {result.get('detail', '未知错误')}", color='negative')
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
ui.notify(f"操作失败: {str(e)}", color='negative')
|
||||
return
|
||||
else:
|
||||
await reloadTable(tips=False)
|
||||
delete_confirmation_dialog.close()
|
||||
editObjectDialog.close()
|
||||
ui.notify('物品已删除', color='positive')
|
||||
|
||||
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中提取数据
|
||||
# 注意:根据API文档,数据可能在response['data']['data']中
|
||||
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) as addObjectFAB:
|
||||
ui.button(icon='add', on_click=addObjectDialog.open) \
|
||||
.props('fab')
|
||||
with ui.page_sticky(x_offset=24, y_offset=24) as editObjectFAB:
|
||||
ui.button(icon='edit', on_click=editObjectDialog.open) \
|
||||
.props('fab')
|
||||
# 单独拉出来默认隐藏,防止无法再设置其显示
|
||||
editObjectFAB.set_visibility(False)
|
||||
@@ -40,6 +40,7 @@ def create() -> None:
|
||||
db = Database()
|
||||
await db.init_db()
|
||||
object_data = await db.get_object(key=key)
|
||||
print(object_data)
|
||||
|
||||
if not object_data:
|
||||
with ui.card().classes('absolute-center w-3/4 h-2/3'):
|
||||
@@ -89,7 +90,9 @@ def create() -> None:
|
||||
|
||||
ui.label('物主(Owner):'+ format_phone(object_data[5], private=False)).classes('text-md w-full text-center -mt-3')
|
||||
|
||||
ui.label('丢失时间(Lost time):'+ object_data[9]).classes('text-md w-full text-center -mt-3')
|
||||
try:
|
||||
ui.label('丢失时间(Lost time):'+ object_data[9]).classes('text-md w-full text-center -mt-3')
|
||||
except: pass
|
||||
|
||||
ui.space()
|
||||
|
||||
|
||||
@@ -1,29 +1,67 @@
|
||||
from contextlib import contextmanager
|
||||
from contextlib import asynccontextmanager
|
||||
from nicegui import ui
|
||||
from fastapi import Request
|
||||
|
||||
@contextmanager
|
||||
def frame(request: Request):
|
||||
@asynccontextmanager
|
||||
async def frame():
|
||||
|
||||
ui.add_head_html(
|
||||
'''
|
||||
<script>
|
||||
async function is_login() {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/admin', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
</script>
|
||||
''')
|
||||
|
||||
await ui.context.client.connected()
|
||||
is_login = await ui.run_javascript('is_login()', timeout=3)
|
||||
if str(is_login).lower() != 'true':
|
||||
ui.navigate.to('/login?redirect_to=/admin')
|
||||
|
||||
with ui.header() \
|
||||
.classes('items-center duration-300 py-2 px-5 no-wrap') \
|
||||
.style('box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1)'):
|
||||
with ui.tabs(value='main_page') as tabs:
|
||||
.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 仪表盘").classes('text-lg').props('flat color=white no-caps')
|
||||
|
||||
with ui.left_drawer() as left_drawer:
|
||||
with ui.column().classes('w-full'):
|
||||
with ui.column(align_items='center').classes('w-full'):
|
||||
ui.image('/static/Findreve.png').classes('w-1/2 mx-auto')
|
||||
ui.label('Findreve').classes('text-2xl text-bold')
|
||||
ui.label("免费版,无需授权").classes('text-red-600 -mt-3')
|
||||
ui.label("免费版,无需授权").classes('text-sm text-gray-500')
|
||||
|
||||
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/item')) \
|
||||
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')
|
||||
|
||||
yield
|
||||
with ui.column().classes('w-full'):
|
||||
yield
|
||||
@@ -35,7 +35,7 @@ def create() -> Optional[RedirectResponse]:
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid username or password');
|
||||
throw new Error('登录失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -53,22 +53,17 @@ def create() -> Optional[RedirectResponse]:
|
||||
""")
|
||||
|
||||
ui.page_title('登录 Findreve')
|
||||
async def try_login() -> None:
|
||||
app.storage.user.update({'authenticated': True})
|
||||
# 跳转到用户上一页
|
||||
ui.navigate.to(app.storage.user.get('referrer_path', '/'))
|
||||
|
||||
async def login():
|
||||
if username.value == "" or password.value == "":
|
||||
ui.notify('账号或密码不能为空', color='negative')
|
||||
return
|
||||
|
||||
try:
|
||||
result = await ui.run_javascript(f"login('{username}', '{password}')")
|
||||
result = await ui.run_javascript(f"login('{username.value}', '{password.value}')")
|
||||
if result['status'] == 'success':
|
||||
ui.navigate.to(redirect_to)
|
||||
else:
|
||||
ui.notify("账号或密码错误", type="negative")
|
||||
ui.notify(f"登录失败: {result['detail']}", type="negative")
|
||||
except Exception as e:
|
||||
ui.notify(f"登录失败: {str(e)}", type="negative")
|
||||
|
||||
@@ -86,10 +81,10 @@ def create() -> Optional[RedirectResponse]:
|
||||
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', try_login) \
|
||||
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', try_login).classes('block w-full text-gray-900').props('filled')
|
||||
.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')
|
||||
|
||||
Reference in New Issue
Block a user