V1.3.0
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
from . import admin
|
||||
from . import session
|
||||
from . import session
|
||||
from . import object
|
||||
@@ -1,14 +1,15 @@
|
||||
from nicegui import app
|
||||
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 import token as Token
|
||||
from model.response import DefaultResponse
|
||||
from model.items import Item
|
||||
|
||||
Router = APIRouter(prefix='/api/admin', tags=['admin'])
|
||||
|
||||
async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -26,13 +27,13 @@ async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]):
|
||||
raise credentials_exception
|
||||
return True
|
||||
|
||||
@app.get('/api/admin')
|
||||
@Router.get('/')
|
||||
async def is_admin(
|
||||
is_admin: Annotated[str, Depends(is_admin)]
|
||||
):
|
||||
return is_admin
|
||||
|
||||
@app.get('/api/admin/items')
|
||||
@Router.get('/items')
|
||||
async def get_items(
|
||||
is_admin: Annotated[str, Depends(is_admin)],
|
||||
id: int = None,
|
||||
@@ -62,7 +63,7 @@ async def get_items(
|
||||
else:
|
||||
return DefaultResponse(data=[])
|
||||
|
||||
@app.post('/api/admin/items')
|
||||
@Router.post('/items')
|
||||
async def add_items(
|
||||
is_admin: Annotated[str, Depends(is_admin)],
|
||||
key: str,
|
||||
@@ -72,7 +73,7 @@ async def add_items(
|
||||
await database.Database().add_object(
|
||||
key=key, name=name, icon=icon, phone=phone)
|
||||
|
||||
@app.patch('/api/admin/items')
|
||||
@Router.patch('/items')
|
||||
async def update_items(
|
||||
is_admin: Annotated[str, Depends(is_admin)],
|
||||
id: int,
|
||||
@@ -96,7 +97,7 @@ async def update_items(
|
||||
else:
|
||||
return DefaultResponse()
|
||||
|
||||
@app.delete('/api/admin/items')
|
||||
@Router.delete('/items')
|
||||
async def delete_items(
|
||||
is_admin: Annotated[str, Depends(is_admin)],
|
||||
id: int):
|
||||
|
||||
39
routes/backend/object.py
Normal file
39
routes/backend/object.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import random
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from model.database import Database
|
||||
from model.response import DefaultResponse
|
||||
import asyncio
|
||||
|
||||
Router = APIRouter(prefix='/api/object', tags=['object'])
|
||||
|
||||
@Router.get('/{item_key}')
|
||||
async def get_object(item_key: str, request: Request):
|
||||
"""
|
||||
获取物品信息 / Get object information
|
||||
"""
|
||||
|
||||
db = Database()
|
||||
await db.init_db()
|
||||
object_data = await db.get_object(key=item_key)
|
||||
|
||||
if object_data:
|
||||
if object_data[4] == 'lost':
|
||||
# 物品已标记为丢失,更新IP地址
|
||||
await db.update_object(id=object_data[0], find_ip=str(request.client.host))
|
||||
|
||||
# 添加一些随机延迟,类似JWT身份验证时根据延迟爆破引发的问题
|
||||
await asyncio.sleep(random.uniform(0.10, 0.30))
|
||||
else:
|
||||
await asyncio.sleep(random.uniform(0.10, 0.30))
|
||||
|
||||
return DefaultResponse(
|
||||
data=object_data
|
||||
)
|
||||
else: return JSONResponse(
|
||||
status_code=404,
|
||||
content=DefaultResponse(
|
||||
code=404,
|
||||
msg='物品不存在或出现异常'
|
||||
).model_dump()
|
||||
)
|
||||
@@ -45,31 +45,13 @@ def create():
|
||||
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>
|
||||
"""
|
||||
)
|
||||
|
||||
ui.add_head_html("""
|
||||
<script type="text/javascript" src="/static/js/main.js"></script>
|
||||
""")
|
||||
|
||||
async with frame():
|
||||
|
||||
# 关于 Findreve
|
||||
|
||||
@@ -12,152 +12,22 @@ 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
|
||||
from ..framework import frame, loding_process
|
||||
|
||||
|
||||
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>
|
||||
''')
|
||||
ui.add_head_html("""
|
||||
<script type="text/javascript" src="/static/js/main.js"></script>
|
||||
""")
|
||||
|
||||
dark_mode = ui.dark_mode(value=True)
|
||||
|
||||
@@ -194,7 +64,7 @@ def create():
|
||||
# 设置丢失状态开关
|
||||
edit_set_object_lost.set_value(selected_item.get('status') == '丢失')
|
||||
# 设置物主留言
|
||||
lostReason.set_value(selected_item.get('context', ''))
|
||||
lostReason.set_value(selected_item.get('lost_description', ''))
|
||||
except:
|
||||
# 当物品列表未选中,显示添加物品按钮,其他按钮不显示
|
||||
addObjectFAB.set_visibility(True)
|
||||
@@ -203,39 +73,16 @@ def create():
|
||||
# 添加物品
|
||||
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:
|
||||
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')
|
||||
@@ -261,20 +108,52 @@ def create():
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
|
||||
# 展示二维码
|
||||
ui.image(f'data:image/png;base64,{img_str}')
|
||||
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()
|
||||
|
||||
# 添加下载二维码按钮
|
||||
ui.button("下载二维码", on_click=lambda: ui.download(buffered.getvalue(), 'qrcode.png')) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
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())) \
|
||||
.classes('w-full').props('flat rounded')
|
||||
ui.button("返回", on_click=lambda: (addObjectDialog.close(), addObjectSuccessDialog.close(), addObjectSuccessDialog.delete())) \
|
||||
.props('flat rounded')
|
||||
|
||||
addObjectSuccessDialog.open()
|
||||
finally:
|
||||
dialogAddObjectIcon.enable()
|
||||
|
||||
|
||||
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'):
|
||||
@@ -283,18 +162,18 @@ def create():
|
||||
|
||||
with ui.scroll_area().classes('w-full'):
|
||||
object_name = ui.input('物品名称').classes('w-full')
|
||||
object_name_tips = ui.label('显示的物品名称').classes('-mt-3')
|
||||
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')
|
||||
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')
|
||||
object_phone_tips = ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
|
||||
ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3')
|
||||
object_key = ui.input('物品Key(可选,不填自动生成)').classes('w-full')
|
||||
object_key_tips = ui.label('物品Key为物品的唯一标识,可用于物品找回').classes('-mt-3')
|
||||
ui.label('物品Key为物品的唯一标识,可用于物品找回').classes('-mt-3')
|
||||
|
||||
async def handle_add_object():
|
||||
await addObject()
|
||||
@@ -304,8 +183,51 @@ def create():
|
||||
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()
|
||||
@@ -321,7 +243,12 @@ def create():
|
||||
dialogEditObjectIcon.enable()
|
||||
return
|
||||
|
||||
try:
|
||||
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'])
|
||||
|
||||
@@ -341,23 +268,10 @@ def create():
|
||||
)
|
||||
|
||||
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()
|
||||
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')
|
||||
@@ -371,7 +285,7 @@ def create():
|
||||
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')
|
||||
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')
|
||||
@@ -382,12 +296,12 @@ def create():
|
||||
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.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') \
|
||||
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():
|
||||
@@ -405,7 +319,16 @@ def create():
|
||||
ui.label('此操作不可撤销,删除后物品数据将永久丢失!').classes('w-full text-center text-red-500')
|
||||
|
||||
async def handle_delete_item():
|
||||
try:
|
||||
|
||||
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)
|
||||
@@ -413,17 +336,7 @@ def create():
|
||||
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')
|
||||
raise Exception(f"删除失败: {result.get('detail', '未知错误')}")
|
||||
|
||||
with ui.row().classes('w-full'):
|
||||
ui.space()
|
||||
@@ -445,7 +358,6 @@ def create():
|
||||
return []
|
||||
|
||||
# 从response中提取数据
|
||||
# 注意:根据API文档,数据可能在response['data']['data']中
|
||||
raw_data = response.get('data', {})
|
||||
objects = raw_data.get('data', []) if isinstance(raw_data, dict) else raw_data
|
||||
|
||||
@@ -505,7 +417,7 @@ def create():
|
||||
selection='single',
|
||||
columns=object_columns,
|
||||
rows=objects,
|
||||
on_select=lambda: objectTableOnSelect()
|
||||
# on_select=lambda: objectTableOnSelect()
|
||||
).classes('w-full').props('flat')
|
||||
|
||||
object_table.add_slot('body-cell-status', '''
|
||||
@@ -531,11 +443,11 @@ def create():
|
||||
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:
|
||||
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) as editObjectFAB:
|
||||
ui.button(icon='edit', on_click=editObjectDialog.open) \
|
||||
.props('fab')
|
||||
# 单独拉出来默认隐藏,防止无法再设置其显示
|
||||
editObjectFAB.set_visibility(False)
|
||||
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')
|
||||
@@ -11,126 +11,114 @@ Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
|
||||
|
||||
from nicegui import ui
|
||||
from fastapi import Request
|
||||
from model.database import Database
|
||||
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:
|
||||
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)'):
|
||||
|
||||
ui.button(icon='menu').props('flat color=white round')
|
||||
ui.button(text="Findreve").classes('text-lg').props('flat color=white no-caps')
|
||||
|
||||
if key == "" or key == None:
|
||||
ui.navigate.to('/404')
|
||||
|
||||
# 加载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')
|
||||
ui.add_head_html(
|
||||
'''
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<script type="text/javascript" src="/static/js/main.js"></script>
|
||||
'''
|
||||
)
|
||||
|
||||
loading.open()
|
||||
|
||||
db = Database()
|
||||
await db.init_db()
|
||||
object_data = await db.get_object(key=key)
|
||||
print(object_data)
|
||||
await ui.context.client.connected()
|
||||
|
||||
if not object_data:
|
||||
with ui.card().classes('absolute-center w-3/4 h-2/3'):
|
||||
async with frame(page='found'):
|
||||
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()
|
||||
|
||||
# 添加标题
|
||||
ui.button(icon='error', color='red').props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden')
|
||||
ui.label('物品不存在').classes('text-h5 w-full text-center')
|
||||
ui.label('Object not found').classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
ui.label('请输入正确的序列号').classes('text-md w-full text-center')
|
||||
ui.label('Please enter the correct serial number').classes('text-md w-full text-center')
|
||||
|
||||
elif object_data[4] == 'ok':
|
||||
# 物品存在, 但未标记为丢失
|
||||
with ui.card().classes('absolute-center w-3/4 h-3/4'):
|
||||
|
||||
# 添加标题
|
||||
ui.button(icon=object_data[3]).props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden')
|
||||
ui.label('关于此 '+ object_data[2]).classes('text-h5 w-full text-center')
|
||||
ui.label('About this '+ object_data[2]).classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
ui.label('序列号(Serial number):'+ object_data[1]).classes('text-md w-full text-center')
|
||||
try:
|
||||
object_data = await ui.run_javascript(f'getObject("{key}")')
|
||||
|
||||
ui.label('物主(Owner):'+ format_phone(object_data[5], private=True)).classes('text-md w-full text-center')
|
||||
|
||||
ui.space()
|
||||
|
||||
ui.label('此物品尚未标记为丢失状态。如果你意外捡到了此物品,请尽快联系物主。').classes('text-md w-full text-center')
|
||||
ui.label('This item has not been marked as lost. If you accidentally picked it up, please contact the owner as soon as possible. ').classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
|
||||
ui.button('返回首页',
|
||||
on_click=lambda: ui.navigate.to('/')) \
|
||||
.classes('items-center w-full').props('rounded')
|
||||
|
||||
elif object_data[4] == 'lost':
|
||||
# 物品存在, 且标记为丢失
|
||||
with ui.card().classes('absolute-center w-3/4 h-3/4'):
|
||||
|
||||
# 添加标题
|
||||
ui.button(icon=object_data[3], color='red').props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden')
|
||||
with ui.label('关于此 '+ object_data[2]).classes('text-h5 w-full text-center'):
|
||||
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')
|
||||
|
||||
ui.label('序列号(Serial number):'+ object_data[1]).classes('text-md w-full text-center -mt-1')
|
||||
|
||||
ui.label('物主(Owner):'+ format_phone(object_data[5], private=False)).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()
|
||||
|
||||
try:
|
||||
ui.label('物主留言(Owner message):'+ object_data[6]) \
|
||||
.classes('text-md w-full text-center')
|
||||
except: pass
|
||||
|
||||
ui.space()
|
||||
|
||||
ui.label('此物品已被物主标记为丢失。您可以通过上面的电话号码来联系物主。').classes('text-md w-full text-center')
|
||||
ui.label('This item has been marked as lost by the owner. You can contact the owner through the phone number above.').classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
ui.button('联系物主',
|
||||
on_click=lambda: ui.navigate.to('tel:' + object_data[5])) \
|
||||
.classes('items-center w-full').props('rounded')
|
||||
|
||||
await db.update_object(id=object_data[0], find_ip=str(request.client.host))
|
||||
|
||||
else:
|
||||
# 物品存在, 但未标记为丢失
|
||||
with ui.card().classes('absolute-center w-3/4 h-3/4'):
|
||||
|
||||
# 添加标题
|
||||
ui.button(icon=object_data[3]).props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden')
|
||||
ui.label('关于此 '+ object_data[2]).classes('text-h5 w-full text-center')
|
||||
ui.label('About this '+ object_data[2]).classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
ui.label('序列号(Serial number):'+ object_data[1]).classes('text-md w-full text-center')
|
||||
|
||||
ui.label('物主(Owner):'+ format_phone(object_data[5], private=True)).classes('text-md w-full text-center')
|
||||
|
||||
ui.space()
|
||||
|
||||
ui.label('此物品状态信息已丢失。如果您捡到了这个物品,请尽快联系物主。如果你是物主,请修改物品信息状态。').classes('text-md w-full text-center')
|
||||
ui.label('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.').classes('text-xs w-full text-center text-gray-500 -mt-3')
|
||||
|
||||
|
||||
loading.close()
|
||||
loading.clear()
|
||||
return
|
||||
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()
|
||||
@@ -1,67 +1,104 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from nicegui import ui
|
||||
import asyncio
|
||||
from typing import Optional, Literal
|
||||
|
||||
@asynccontextmanager
|
||||
async def frame():
|
||||
async def frame(
|
||||
page: Literal['admin', 'session', 'found'] = 'admin'
|
||||
):
|
||||
|
||||
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>
|
||||
''')
|
||||
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 str(is_login).lower() != 'true' and page != 'session':
|
||||
ui.navigate.to('/login?redirect_to=/admin')
|
||||
|
||||
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 仪表盘").classes('text-lg').props('flat color=white no-caps')
|
||||
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/2 mx-auto')
|
||||
ui.label('Findreve').classes('text-2xl text-bold')
|
||||
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/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 == '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
|
||||
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()
|
||||
@@ -9,82 +9,47 @@ Description: Findreve 登录界面 Login
|
||||
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
|
||||
'''
|
||||
|
||||
from nicegui import ui, app
|
||||
from nicegui import ui
|
||||
from typing import Optional
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi import Request
|
||||
from .framework import frame
|
||||
|
||||
def create() -> Optional[RedirectResponse]:
|
||||
@ui.page('/login')
|
||||
async def session(request: Request, redirect_to: str = "/"):
|
||||
async def session(redirect_to: str = "/"):
|
||||
ui.add_head_html("""
|
||||
<script>
|
||||
async function login(username, password) {
|
||||
const url = '/api/token';
|
||||
const data = new URLSearchParams();
|
||||
data.append('username', username);
|
||||
data.append('password', password);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('登录失败: ' + response.statusText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 处理登录成功后的数据,返回access_token
|
||||
localStorage.setItem('access_token', result.access_token);
|
||||
|
||||
return {'status': 'success'};
|
||||
|
||||
} catch (error) {
|
||||
return {'status': 'failed', 'detail': error.message};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="/static/js/main.js"></script>
|
||||
""")
|
||||
|
||||
ui.page_title('登录 Findreve')
|
||||
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")
|
||||
|
||||
ui.dark_mode(True)
|
||||
|
||||
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)'):
|
||||
async with frame(page='session'):
|
||||
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")
|
||||
|
||||
ui.button(icon='menu').props('flat color=white round')
|
||||
appBar_appName = ui.button(text="HeyPress").classes('text-lg').props('flat color=white no-caps')
|
||||
|
||||
# 创建一个绝对中心的登录卡片
|
||||
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')
|
||||
# 创建一个绝对中心的登录卡片
|
||||
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')
|
||||
|
||||
@@ -6,22 +6,41 @@ LastEditTime: 2024-11-29 20:04:24
|
||||
FilePath: /Findreve/main_page.py
|
||||
Description: Findreve 个人主页 main_page
|
||||
|
||||
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
|
||||
Copyright (c) 2018-2025 by 于小丘Yuerchu, All Rights Reserved.
|
||||
'''
|
||||
|
||||
from nicegui import ui
|
||||
from fastapi import Request
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
import model
|
||||
|
||||
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>
|
||||
.fade-in {
|
||||
animation: fadeIn 0.8s ease-in-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
body > div {
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease-in-out forwards;
|
||||
}
|
||||
.transform {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</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'):
|
||||
@@ -30,7 +49,6 @@ def create() -> None:
|
||||
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'):
|
||||
# 下面的这个是Base64格式,你需要改成你自己的头像,支持链接,也可以用Base64本地化
|
||||
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')
|
||||
@@ -56,30 +74,30 @@ def create() -> None:
|
||||
ui.label('开启创造力').classes('text-4xl text-bold -mt-1 right-4')
|
||||
|
||||
with ui.row().classes('items-center'):
|
||||
ui.chip('Python', color='amber-400').classes('p-4').props('floating').tooltip('Python是世界上最好的语言')
|
||||
ui.chip('Kotlin', color='violet-400').classes('p-4').props('floating').tooltip('Kotlin给安卓开发APP')
|
||||
ui.chip('Golang', color='sky-400').classes('p-4').props('floating').tooltip('Golang写后端')
|
||||
ui.chip('Lua', color='blue-900').classes('p-4').props('floating').tooltip('用aLua给安卓开发,给罗技鼠标写鼠标宏')
|
||||
ui.chip('c', color='red-400').classes('p-4').props('floating').tooltip('C写嵌入式开发')
|
||||
ui.chip('FL Studio', color='orange-600').classes('p-4').props('floating').tooltip('FL Studio是世界上最好的宿主')
|
||||
ui.chip('Photoshop', color='blue-950').classes('p-4').props('floating').tooltip('修图/抠图/画画一站通')
|
||||
ui.chip('Premiere', color='indigo-900').classes('p-4').props('floating').tooltip('剪视频比较顺手,但是一开风扇狂转')
|
||||
ui.chip('After Effects', color='indigo-950').classes('p-4').props('floating').tooltip('制作特效,电脑太烂了做不了太花的')
|
||||
ui.chip('Audition', color='purple-900').classes('p-4').props('floating').tooltip('写歌做母带挺好用的')
|
||||
ui.chip('Illustrator', color='amber-800').classes('p-4').props('floating').tooltip('自制字体和画动态SVG')
|
||||
ui.chip('HTML', color='red-900').classes('p-4').props('floating').tooltip('前端入门三件套,不学这玩意其他学了没用')
|
||||
ui.chip('CSS3', color='cyan-900').classes('p-4').props('floating').tooltip('. window{ show: none; }')
|
||||
ui.chip('JavaScript', color='lime-900').classes('p-4').props('floating').tooltip('还在努力学习中,只会一些简单的')
|
||||
ui.chip('git', color='amber-700').classes('p-4').props('floating').tooltip('版本管理是真好用')
|
||||
ui.chip('Docker', color='sky-600').classes('p-4').props('floating').tooltip('容器化部署')
|
||||
ui.chip('chatGPT', color='emerald-600').classes('p-4').props('floating').tooltip('文本助驾,写代码/写文章/写论文')
|
||||
ui.chip('SAI2', color='gray-950').classes('p-4').props('floating').tooltip('入门绘画')
|
||||
ui.chip('ips Draw', color='gray-900').classes('p-4').props('floating').tooltip('自认为是iOS端最佳绘画软件')
|
||||
ui.chip('AutoCAD', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
|
||||
ui.chip('SolidWorks', color='gray-900').classes('p-4').props('floating').tooltip('画图/绘制3D模型')
|
||||
ui.chip('EasyEDA', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
|
||||
ui.chip('KiCad', color='gray-900').classes('p-4').props('floating').tooltip('画图/绘制电路图')
|
||||
ui.chip('Altium Designer', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user