fix:添加niceGui库美化页面
This commit is contained in:
110
.idea/workspace.xml
generated
110
.idea/workspace.xml
generated
@@ -4,25 +4,21 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/script/setup.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/growth_report.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/growth_report.iml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||
<list default="true" id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="fix:修复一些BUG">
|
||||
<change afterPath="$PROJECT_DIR$/main_nicegui.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/script/setup_nicegui.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/assets/icon.ico" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/assets/style.css" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/core/logger.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/core/state.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/core/task_runner.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/views/home_page.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config.env.toml" beforeDir="false" afterPath="$PROJECT_DIR$/config.env.toml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/names.xlsx" beforeDir="false" afterPath="$PROJECT_DIR$/data/names.xlsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.pyw" beforeDir="false" afterPath="$PROJECT_DIR$/main.pyw" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/old/main.py" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/public/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/public/icon.ico" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/app_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/app_window.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/font_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/font_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/generate_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/generate_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/growt_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/growt_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/pdf_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/pdf_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/script/setup.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/setup.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/file_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/file_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -51,12 +47,14 @@
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"Python.flet_ui_main.pyw.executor": "Run",
|
||||
"Python.main.pyw.executor": "Run",
|
||||
"Python.main_nicegui.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"last_opened_file_path": "D:/working/tools/growth_report/public",
|
||||
"last_opened_file_path": "D:/working/tools/growth_report",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
@@ -68,10 +66,63 @@
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="D:\working\tools\growth_report" />
|
||||
<recent name="D:\working\tools\growth_report\data" />
|
||||
<recent name="D:\working\tools\growth_report\script\dist" />
|
||||
<recent name="D:\working\tools\growth_report\ui\assets" />
|
||||
<recent name="D:\working\tools\growth_report\public" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="D:\working\tools\growth_report\script\dist\data" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<component name="RunManager" selected="Python.main_nicegui">
|
||||
<configuration name="flet_ui_main.pyw" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="growth_report" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/flet_ui_main.pyw.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="main_nicegui" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="growth_report" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main_nicegui.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="main.pyw" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="growth_report" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
@@ -97,6 +148,8 @@
|
||||
</configuration>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="Python.main_nicegui" />
|
||||
<item itemvalue="Python.flet_ui_main.pyw" />
|
||||
<item itemvalue="Python.main.pyw" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
@@ -110,14 +163,29 @@
|
||||
<updated>1765613055475</updated>
|
||||
<workItem from="1765613057798" duration="372000" />
|
||||
<workItem from="1765613448098" duration="48000" />
|
||||
<workItem from="1765613503892" duration="10202000" />
|
||||
<workItem from="1765613503892" duration="15577000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="fix:修复一些BUG">
|
||||
<option name="closed" value="true" />
|
||||
<created>1765626269402</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1765626269402</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="2" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="fix:修复一些BUG" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix:修复一些BUG" />
|
||||
</component>
|
||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||
<SUITE FILE_PATH="coverage/growth_report$main_pyw.coverage" NAME="main.pyw 覆盖结果" MODIFIED="1765626060279" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/growth_report$main_pyw.coverage" NAME="main.pyw 覆盖结果" MODIFIED="1765626787983" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/growth_report$flet_ui_main_pyw.coverage" NAME="flet_ui_main.pyw 覆盖结果" MODIFIED="1765627407579" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/growth_report$main_nicegui.coverage" NAME="main_nicegui 覆盖结果" MODIFIED="1765631173809" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
</component>
|
||||
</project>
|
||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
64
main_nicegui.py
Normal file
64
main_nicegui.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from nicegui import ui, app, run
|
||||
from loguru import logger
|
||||
|
||||
# 导入我们的模块
|
||||
from config.config import load_config
|
||||
from ui.core.logger import setup_logger
|
||||
from utils.font_utils import install_fonts_from_directory
|
||||
from ui.views.home_page import create_page
|
||||
|
||||
# 1. 初始化配置
|
||||
config = load_config("config.toml")
|
||||
setup_logger()
|
||||
|
||||
|
||||
# === 关键修改:定义一个获取路径的通用函数 ===
|
||||
def get_path(relative_path):
|
||||
"""
|
||||
获取资源的绝对路径。
|
||||
兼容:开发环境(直接运行) 和 生产环境(打包成exe后解压的临时目录)
|
||||
"""
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# 开发环境当前目录
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
# 1. 挂载静态资源 (CSS/图片)
|
||||
# 注意:这里使用 get_path 确保打包后能找到
|
||||
static_dir = get_path(os.path.join("ui", "assets"))
|
||||
app.add_static_files('/assets', static_dir)
|
||||
|
||||
|
||||
# 3. 页面路由
|
||||
@ui.page('/')
|
||||
def index():
|
||||
create_page()
|
||||
|
||||
|
||||
# 4. 启动时钩子
|
||||
async def startup_check():
|
||||
try:
|
||||
logger.info("系统启动: 初始化资源...")
|
||||
await run.io_bound(install_fonts_from_directory, config["fonts_dir"])
|
||||
os.makedirs(config["output_folder"], exist_ok=True)
|
||||
logger.success("资源初始化完成")
|
||||
except Exception as e:
|
||||
logger.error(f"初始化失败: {e}")
|
||||
|
||||
|
||||
app.on_startup(startup_check)
|
||||
|
||||
if __name__ in {"__main__", "__mp_main__"}:
|
||||
ui.run(
|
||||
title="尚城幼儿园成长报告助手",
|
||||
native=True,
|
||||
window_size=(900, 900),
|
||||
reload=False
|
||||
)
|
||||
@@ -9,12 +9,14 @@ dependencies = [
|
||||
"langchain>=1.1.3",
|
||||
"langchain-openai>=1.1.1",
|
||||
"loguru>=0.7.3",
|
||||
"nicegui>=3.4.0",
|
||||
"openpyxl>=3.1.5",
|
||||
"pandas>=2.3.3",
|
||||
"pandas-stubs==2.3.3.251201",
|
||||
"pillow>=12.0.0",
|
||||
"pyinstaller>=6.17.0",
|
||||
"python-pptx>=1.0.2",
|
||||
"pywebview>=6.1",
|
||||
"pywin32>=311",
|
||||
"rich>=14.2.0",
|
||||
"tomli>=2.3.0",
|
||||
|
||||
@@ -4,6 +4,8 @@ import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
MAIN_FILE = "main_nicegui.py"
|
||||
|
||||
|
||||
def copy_resources():
|
||||
"""
|
||||
@@ -33,6 +35,7 @@ def copy_resources():
|
||||
("data", "data"), # 复制 data 文件夹 到 dist/data
|
||||
("templates", "templates"), # 复制 templates 文件夹 到 dist/templates
|
||||
("public", "public"), # 复制 public 文件夹 到 dist/public
|
||||
('ui/assets', 'ui/assets'), # 复制 ui/assets 文件夹 到 dist/ui/assets
|
||||
]
|
||||
|
||||
for src_name, dest_name in resources_to_copy:
|
||||
@@ -79,7 +82,7 @@ def build_exe():
|
||||
"--windowed", # 调试阶段建议先注释掉,确认无误后再开启
|
||||
"--name=尚城幼儿园幼儿学期发展报告",
|
||||
"--icon=../public/icon.ico",
|
||||
"../main.pyw"
|
||||
"../" + MAIN_FILE
|
||||
]
|
||||
|
||||
# 添加资源参数
|
||||
@@ -106,4 +109,4 @@ def build_exe():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_exe()
|
||||
build_exe()
|
||||
|
||||
100
script/setup_nicegui.py
Normal file
100
script/setup_nicegui.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
|
||||
MAIN_FILE = "main_nicegui.py"
|
||||
|
||||
def copy_resources():
|
||||
"""
|
||||
将资源文件从项目根目录复制到 dist 文件夹中,
|
||||
以便用户可以直接在 exe 旁边修改这些文件。
|
||||
"""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(current_dir)
|
||||
dist_dir = os.path.join(current_dir, "dist")
|
||||
|
||||
print(f"\n--- 正在复制外部资源到 {dist_dir} ---")
|
||||
|
||||
if not os.path.exists(dist_dir):
|
||||
print("错误: dist 文件夹不存在,请先运行打包。")
|
||||
return
|
||||
|
||||
# 这里的列表是【给用户看/改的】,不用把 ui/assets 放这里,除非你希望用户改CSS
|
||||
resources_to_copy = [
|
||||
("config.toml", ""),
|
||||
("fonts", "fonts"),
|
||||
("data", "data"),
|
||||
("templates", "templates"),
|
||||
("public", "public"),
|
||||
# ui/assets 通常不需要用户改,所以这里可以不复制到外部,只打在包里即可
|
||||
# 但如果你希望用户能自定义 logo,也可以复制出来
|
||||
]
|
||||
|
||||
for src_name, dest_name in resources_to_copy:
|
||||
src_path = os.path.join(project_root, src_name)
|
||||
dest_path = os.path.join(dist_dir, dest_name)
|
||||
|
||||
try:
|
||||
if os.path.isfile(src_path):
|
||||
shutil.copy2(src_path, dest_path)
|
||||
print(f"✅ 已复制文件: {src_name}")
|
||||
elif os.path.isdir(src_path):
|
||||
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
|
||||
print(f"✅ 已复制目录: {src_name}")
|
||||
else:
|
||||
print(f"⚠️ 警告: 源文件不存在,跳过: {src_path}")
|
||||
except Exception as e:
|
||||
print(f"❌ 复制失败 {src_name}: {e}")
|
||||
|
||||
def build_exe():
|
||||
"""使用 PyInstaller 打包"""
|
||||
|
||||
# 1. 确定当前系统的分隔符 (Windows用';', Linux/Mac用':')
|
||||
sep = ';' if platform.system() == "Windows" else ':'
|
||||
|
||||
# 2. 定义内部资源 (打入 exe 肚子里的)
|
||||
# 格式: "源路径{sep}目标路径"
|
||||
resource_paths = [
|
||||
f"../config.toml{sep}.", # 默认配置
|
||||
f"../fonts{sep}fonts", # 字体 (程序可能需要内部路径)
|
||||
f"../templates{sep}templates", # 模板
|
||||
f"../ui/assets{sep}ui/assets", # <--- 关键修复:添加 UI 静态资源
|
||||
# public 和 data 如果体积太大且只在运行时读取,可以不打入包内,只保留外部复制
|
||||
]
|
||||
|
||||
try:
|
||||
command = [
|
||||
sys.executable, "-m", "PyInstaller",
|
||||
"--onefile",
|
||||
"--windowed", # 建议:先注释掉这行,打包出来先看黑框有没有报错,没问题了再开启
|
||||
"--name=尚城幼儿园幼儿学期发展报告",
|
||||
"--clean", # 清理缓存,避免旧文件干扰
|
||||
"--distpath=./dist", # 明确输出目录
|
||||
"--workpath=./build",
|
||||
"--icon=../public/icon.ico", # 确保你真有这个图标,否则会报错
|
||||
"../" + MAIN_FILE
|
||||
]
|
||||
|
||||
# 添加 --add-data 参数
|
||||
for res in resource_paths:
|
||||
command.append(f"--add-data={res}")
|
||||
|
||||
# 添加 hidden-import (NiceGUI 常见缺失)
|
||||
command.extend(["--hidden-import=nicegui", "--hidden-import=uvicorn"])
|
||||
|
||||
print("--- 开始打包 (PyInstaller) ---")
|
||||
subprocess.run(command, check=True, cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||
print("\n--- PyInstaller 打包完成!---")
|
||||
|
||||
copy_resources()
|
||||
print(f"\n🎉 全部完成!请查看 'dist' 文件夹。")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n❌ 打包失败: {e}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_exe()
|
||||
BIN
ui/assets/icon.ico
Normal file
BIN
ui/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
55
ui/assets/style.css
Normal file
55
ui/assets/style.css
Normal file
@@ -0,0 +1,55 @@
|
||||
/* assets/style.css */
|
||||
|
||||
/* 全局字体 */
|
||||
body {
|
||||
font-family: "微软雅黑", "Microsoft YaHei", sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.app-header {
|
||||
background-color: #2E8B57; /* SeaGreen */
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 卡片通用样式 */
|
||||
.func-card {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* 核心功能区顶部边框 */
|
||||
.card-core {
|
||||
border-top: 4px solid #16a34a; /* green-600 */
|
||||
}
|
||||
|
||||
/* 数据管理区顶部边框 */
|
||||
.card-data {
|
||||
border-top: 4px solid #3b82f6; /* blue-500 */
|
||||
}
|
||||
|
||||
/* 系统操作区顶部边框 */
|
||||
.card-system {
|
||||
border-top: 4px solid #ef4444; /* red-500 */
|
||||
}
|
||||
|
||||
.card-logging {
|
||||
border-top: 4px solid #9c1be0;
|
||||
}
|
||||
|
||||
/* 标题文字 */
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 绿色标题 */
|
||||
.text-green { color: #166534; }
|
||||
/* 蓝色标题 */
|
||||
.text-blue { color: #1e40af; }
|
||||
/* 红色标题 */
|
||||
.text-red { color: #991b1b; }
|
||||
15
ui/core/logger.py
Normal file
15
ui/core/logger.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import sys
|
||||
from loguru import logger
|
||||
from ui.core.state import app_state
|
||||
|
||||
class GuiLogger:
|
||||
def write(self, message):
|
||||
if app_state.log_element:
|
||||
app_state.log_element.push(message.strip())
|
||||
|
||||
def setup_logger():
|
||||
logger.remove()
|
||||
# 控制台输出
|
||||
logger.add(sys.stderr, format="{time:HH:mm:ss} | {level} | {message}")
|
||||
# GUI 输出
|
||||
logger.add(GuiLogger(), format="{time:HH:mm:ss} | {level} | {message}", level="INFO")
|
||||
13
ui/core/state.py
Normal file
13
ui/core/state.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import threading
|
||||
|
||||
class AppState:
|
||||
def __init__(self):
|
||||
self.stop_event = threading.Event()
|
||||
self.is_running = False
|
||||
# 这些 UI 元素的引用将在 UI 初始化时被赋值
|
||||
self.progress_bar = None
|
||||
self.progress_label = None
|
||||
self.log_element = None
|
||||
|
||||
# 创建全局单例
|
||||
app_state = AppState()
|
||||
73
ui/core/task_runner.py
Normal file
73
ui/core/task_runner.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from nicegui import ui, run
|
||||
from loguru import logger
|
||||
|
||||
from ui.core.state import app_state
|
||||
|
||||
|
||||
async def select_folder():
|
||||
"""在 Native 模式下弹窗选择文件夹"""
|
||||
|
||||
def _pick():
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes("-topmost", True)
|
||||
path = filedialog.askdirectory()
|
||||
root.destroy()
|
||||
return path
|
||||
|
||||
return await run.io_bound(_pick)
|
||||
|
||||
|
||||
async def run_task(func, *args, **kwargs):
|
||||
"""通用任务执行器"""
|
||||
if app_state.is_running:
|
||||
ui.notify("当前有任务正在运行,请稍候...", type="warning")
|
||||
return
|
||||
|
||||
# 1. 状态重置
|
||||
app_state.is_running = True
|
||||
app_state.stop_event.clear()
|
||||
if app_state.progress_bar: app_state.progress_bar.set_value(0)
|
||||
if app_state.progress_label: app_state.progress_label.set_text("🚀 正在启动任务...")
|
||||
|
||||
# 2. 定义进度条回调
|
||||
def progress_callback(current, total, task_name="任务"):
|
||||
if total <= 0:
|
||||
pct = 0
|
||||
text = f"{task_name}: 准备中..."
|
||||
else:
|
||||
pct = current / total
|
||||
text = f"{task_name}: {current}/{total} ({int(pct * 100)}%)"
|
||||
|
||||
# 更新 UI
|
||||
if app_state.progress_bar: app_state.progress_bar.set_value(pct)
|
||||
if app_state.progress_label: app_state.progress_label.set_text(text)
|
||||
|
||||
# 3. 组装参数
|
||||
kwargs['progress_callback'] = progress_callback
|
||||
kwargs['stop_event'] = app_state.stop_event
|
||||
|
||||
# 4. 执行
|
||||
try:
|
||||
# 适配器:检查函数是否接受 stop_event
|
||||
def _exec():
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except TypeError:
|
||||
kwargs.pop('stop_event', None)
|
||||
func(*args, **kwargs)
|
||||
|
||||
await run.io_bound(_exec)
|
||||
|
||||
if app_state.progress_label: app_state.progress_label.set_text("✅ 任务完成")
|
||||
ui.notify("任务执行成功!", type="positive")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"任务出错: {e}")
|
||||
if app_state.progress_label: app_state.progress_label.set_text(f"❌ 错误: {str(e)}")
|
||||
ui.notify(f"任务失败: {e}", type="negative")
|
||||
finally:
|
||||
app_state.is_running = False
|
||||
if app_state.progress_bar: app_state.progress_bar.set_value(0)
|
||||
94
ui/views/home_page.py
Normal file
94
ui/views/home_page.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from nicegui import ui, app
|
||||
from config.config import load_config
|
||||
from ui.core.state import app_state
|
||||
from ui.core.task_runner import run_task, select_folder
|
||||
|
||||
# 导入业务函数
|
||||
from utils.generate_utils import (
|
||||
generate_template, generate_comment_all,
|
||||
batch_convert_folder, generate_report, generate_zodiac
|
||||
)
|
||||
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
def create_header():
|
||||
with ui.header().classes('app-header items-center justify-between shadow-md'):
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain')
|
||||
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold')
|
||||
ui.label('By 寒寒 | 这里的每一份评语都充满爱意').classes('text-xs opacity-90')
|
||||
|
||||
|
||||
def create_page():
|
||||
# 1. 引入外部 CSS
|
||||
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||
|
||||
create_header()
|
||||
|
||||
# 主容器
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-4 gap-4'):
|
||||
|
||||
# === 进度条区域 ===
|
||||
with ui.card().classes('func-card'):
|
||||
app_state.progress_label = ui.label('⛳ 任务进度: 待命').classes('font-bold text-gray-700 mb-1')
|
||||
# 使用 NiceGUI 原生属性配合 CSS 类
|
||||
app_state.progress_bar = ui.linear_progress(value=0, show_value=False).classes('h-4 rounded')
|
||||
app_state.progress_bar.props('color=positive') # 使用 Quasar 颜色变量
|
||||
|
||||
# === 核心功能区 ===
|
||||
with ui.card().classes('func-card card-core'):
|
||||
ui.label('🛠️ 核心功能').classes('section-title text-green')
|
||||
|
||||
with ui.grid(columns=3).classes('w-full gap-3'):
|
||||
# 辅助函数:快速创建按钮
|
||||
def func_btn(text, icon, func):
|
||||
ui.button(text, on_click=lambda: run_task(func)).props(f'outline').classes('w-full')
|
||||
|
||||
func_btn('📁 生成图片路径', 'image', generate_template)
|
||||
func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all)
|
||||
func_btn('📊 生成报告 (PPT)', 'analytics', generate_report)
|
||||
|
||||
# 特殊处理带参数的
|
||||
async def run_convert():
|
||||
await run_task(batch_convert_folder, config.get("output_folder"))
|
||||
|
||||
ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline')
|
||||
|
||||
func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac)
|
||||
|
||||
# === 下方双栏布局 ===
|
||||
with ui.grid(columns=2).classes('w-full gap-4'):
|
||||
|
||||
# 数据管理
|
||||
with ui.card().classes('func-card card-data'):
|
||||
ui.label('📦 数据管理').classes('section-title text-blue')
|
||||
with ui.row().classes('w-full'):
|
||||
async def do_export(func):
|
||||
path = await select_folder()
|
||||
if path: await run_task(func, path)
|
||||
|
||||
ui.button('📦 导出模板', on_click=lambda: do_export(export_templates_folder)).props(f'outline')
|
||||
ui.button('📤 导出备份', on_click=lambda: do_export(export_data)).props(f'outline')
|
||||
|
||||
# 系统操作
|
||||
with ui.card().classes('func-card card-system'):
|
||||
ui.label('⚙️ 系统操作').classes('section-title text-red')
|
||||
with ui.row().classes('w-full'):
|
||||
def stop_now():
|
||||
if app_state.is_running:
|
||||
app_state.stop_event.set()
|
||||
ui.notify("发送停止信号...", type="warning")
|
||||
|
||||
ui.button('⛔ 停止', on_click=stop_now).props('color=negative').classes('flex-1')
|
||||
|
||||
async def reset_sys():
|
||||
await run_task(initialize_project)
|
||||
|
||||
ui.button('⚠️ 初始化', on_click=reset_sys).props('outline color=warning').classes('flex-1')
|
||||
|
||||
# === 日志区 ===
|
||||
with ui.card().classes('func-card card-logging'):
|
||||
with ui.expansion('📝 系统实时日志',value=True).classes('w-full bg-white shadow-sm rounded'):
|
||||
app_state.log_element = ui.log(max_lines=200).classes('w-full h-40 font-mono text-xs bg-gray-100 p-2')
|
||||
@@ -5,13 +5,15 @@ from loguru import logger
|
||||
import zipfile
|
||||
|
||||
|
||||
def export_templates_folder(output_folder="backup"):
|
||||
def export_templates_folder(output_folder, stop_event, progress_callback=None):
|
||||
"""
|
||||
将指定文件夹压缩为 zip 包
|
||||
:param source_folder: 要压缩的文件夹路径 (默认 'data')
|
||||
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
||||
:param stop_event: 停止事件
|
||||
:param progress_callback : 进度条回调
|
||||
"""
|
||||
source_folder = "data"
|
||||
output_folder = output_folder if output_folder else "backup"
|
||||
try:
|
||||
# 1. 检查源文件夹是否存在
|
||||
if not os.path.exists(source_folder):
|
||||
@@ -55,11 +57,12 @@ def export_templates_folder(output_folder="backup"):
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def export_data(save_dir, root_dir="."):
|
||||
def export_data(save_dir, root_dir=".", progress_callback=None):
|
||||
"""
|
||||
导出 data 和 output 两个文件夹到同一个 zip 包中
|
||||
:param save_dir: 用户在 GUI 弹窗中选择的保存目录 (例如: D:/Backup)
|
||||
:param root_dir: 项目根目录 (用于找到 data 和 output)
|
||||
:param progress_callback: 进度条回调函数,接收一个 float (0.0~1.0)
|
||||
"""
|
||||
|
||||
# 1. 定义要打包的目标文件夹
|
||||
@@ -68,7 +71,18 @@ def export_data(save_dir, root_dir="."):
|
||||
# 2. 检查保存目录
|
||||
if not os.path.exists(save_dir):
|
||||
logger.error(f"保存目录不存在: {save_dir}")
|
||||
return
|
||||
return None
|
||||
|
||||
# --- 【新增步骤 A】预先计算文件总数 ---
|
||||
total_files = 0
|
||||
for target in targets:
|
||||
target_abs_path = os.path.join(root_dir, target)
|
||||
if os.path.exists(target_abs_path):
|
||||
for _, _, files in os.walk(target_abs_path):
|
||||
total_files += len(files)
|
||||
|
||||
logger.info(f"待压缩文件总数: {total_files}")
|
||||
# ------------------------------------
|
||||
|
||||
# 3. 生成压缩包路径
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
@@ -78,15 +92,15 @@ def export_data(save_dir, root_dir="."):
|
||||
logger.info(f"开始备份,目标文件: {zip_path}")
|
||||
|
||||
try:
|
||||
# 4. 创建压缩包 (使用 'w' 写入模式,ZIP_DEFLATED 表示压缩)
|
||||
# 4. 创建压缩包
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
|
||||
processed_count = 0 # 当前处理的文件数
|
||||
has_files = False # 标记是否真的压缩了文件
|
||||
|
||||
for target in targets:
|
||||
target_abs_path = os.path.join(root_dir, target)
|
||||
|
||||
# 检查 data 或 output 是否存在
|
||||
if not os.path.exists(target_abs_path):
|
||||
logger.warning(f"⚠️ 跳过: 找不到文件夹 '{target}'")
|
||||
continue
|
||||
@@ -94,26 +108,30 @@ def export_data(save_dir, root_dir="."):
|
||||
logger.info(f"正在压缩: {target} ...")
|
||||
|
||||
# 5. 遍历文件夹写入 ZIP
|
||||
# os.walk 会递归遍历子文件夹
|
||||
for root, dirs, files in os.walk(target_abs_path):
|
||||
for file in files:
|
||||
# 获取文件的绝对路径
|
||||
file_abs_path = os.path.join(root, file)
|
||||
|
||||
# 【关键】计算在压缩包里的相对路径
|
||||
# 例如: D:/Project/data/images/1.jpg -> data/images/1.jpg
|
||||
# 计算相对路径
|
||||
arcname = os.path.relpath(file_abs_path, root_dir)
|
||||
|
||||
# 写入压缩包
|
||||
zf.write(file_abs_path, arcname)
|
||||
has_files = True
|
||||
# 更新进度条
|
||||
if progress_callback:
|
||||
progress_callback(processed_count + 1, total_files, "导出数据中...")
|
||||
|
||||
if has_files:
|
||||
# 确保进度条最后能走到 100%
|
||||
if progress_callback:
|
||||
progress_callback(total_files, total_files, "导出数据成功")
|
||||
|
||||
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
|
||||
return zip_path
|
||||
else:
|
||||
logger.error("❌ 备份失败: data 和 output 文件夹均为空或不存在。")
|
||||
# 如果生成了空文件,建议删除
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
return None
|
||||
@@ -121,14 +139,15 @@ def export_data(save_dir, root_dir="."):
|
||||
except Exception as e:
|
||||
logger.error(f"导出过程出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
def initialize_project(root_dir="."):
|
||||
def initialize_project(root_dir=".", progress_callback=None):
|
||||
"""
|
||||
初始化项目:清空 data,重建目录,复制模板
|
||||
:param root_dir: 项目根目录
|
||||
:param progress_callback : 进度条回调
|
||||
"""
|
||||
# 定义路径
|
||||
data_dir = os.path.join(root_dir, "data")
|
||||
@@ -192,13 +211,9 @@ def initialize_project(root_dir="."):
|
||||
f"⚠️ 警告: 模板文件不存在 ({src_excel}),data 文件夹内将没有 Excel 文件。"
|
||||
)
|
||||
|
||||
|
||||
def check_file_exists(file_path):
|
||||
"""
|
||||
判断文件是否存在
|
||||
"""
|
||||
if (file_path and isinstance(file_path, str) and os.path.exists(file_path)):
|
||||
logger.info(f"✅ 文件存在: {file_path}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"❌ 文件不存在: {file_path}")
|
||||
return False
|
||||
return file_path and isinstance(file_path, str) and os.path.exists(file_path)
|
||||
|
||||
Reference in New Issue
Block a user