fix: 修复 EXE 数据库 schema 不匹配导致 500 错误

- ISS 移除 onlyifdoesntexist 确保安装目录 db.sqlite3 始终更新

- .gitignore 忽略所有 sqlite3 文件

- 前端翻页组件和样式更新
This commit is contained in:
Chunyu
2026-08-29 13:49:23 +08:00
parent d7553520ce
commit ffe44f4cb1
6 changed files with 102 additions and 23 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ frontend/node_modules/
frontend/dist/
# Data
backend/db.sqlite3
*.sqlite3
backend/staticfiles/
# Logs
+14 -2
View File
@@ -1,4 +1,4 @@
@echo off
@echo off
chcp 65001 >nul
echo ==========================================
echo 学校订餐菜单生成器 - 一键打包安装包
@@ -40,6 +40,8 @@ if exist "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" (
set "ISCC=C:\Program Files\Inno Setup 6\ISCC.exe"
) else if exist "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" (
set "ISCC=C:\Program Files (x86)\Inno Setup 5\ISCC.exe"
) else if exist "C:\Program Files\Inno Setup 5\ISCC.exe" (
set "ISCC=C:\Program Files\Inno Setup 5\ISCC.exe"
) else (
where iscc >nul 2>&1
if not errorlevel 1 (
@@ -60,6 +62,16 @@ if "%ISCC%"=="" (
)
echo 找到 Inno Setup: %ISCC%
echo.
echo [3.5/4] 验证语言文件...
for %%I in ("%ISCC%") do set "ISDIR=%%~dpI"
if not exist "%ISDIR%Languages\ChineseSimplified.isl" (
echo.
echo 警告: 未找到中文语言文件 ChineseSimplified.isl
echo 安装向导将使用英文界面。
echo 如需中文界面,请确保 Inno Setup 完整安装。
)
echo.
echo [4/4] 编译安装包...
"%ISCC%" school-meal-setup.iss
@@ -78,4 +90,4 @@ echo 输出目录: installer\
echo 安装包: installer\学校订餐菜单生成器-安装包.exe
echo ==========================================
echo.
pause
pause
+30 -11
View File
@@ -20,7 +20,8 @@ const TYPE_LABELS = { meats: '荤菜', vegs: '素菜', soup: '营养汤', staple
const TYPE_TO_TAB = { meats: 'meat', vegs: 'veg', soup: 'soup', staple: 'staple' }
const TYPE_ALL_LABELS = { meat: '荤菜', veg: '素菜', soup: '营养汤', staple: '主食' }
const DEFAULT_SCHOOL = '学校菜单'
const MODAL_PAGE_SIZE = 20
const DEFAULT_PAGE_SIZE = 20
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
/** Compute ISO week Monday/Friday as Date objects */
const weekDates = (weekNo) => {
@@ -63,7 +64,7 @@ const slotKey = (menu, day, slot, idx) => `${menu}|${day}|${slot}|${idx}`
/* ============================
Modern Pagination Component
============================ */
function Pagination({ page, total, pageSize, onChange }) {
function Pagination({ page, total, pageSize, onPageSizeChange, onChange }) {
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const currentPage = Math.min(Math.max(1, page), totalPages)
if (totalPages <= 1) return null
@@ -149,6 +150,21 @@ function Pagination({ page, total, pageSize, onChange }) {
/>
页
</span>
{onPageSizeChange && (
<span className="pg-size">
每页
<select
className="pg-size-select"
value={pageSize}
onChange={(e) => { onPageSizeChange(Number(e.target.value)); onChange(1) }}
>
{PAGE_SIZE_OPTIONS.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
条
</span>
)}
</div>
</div>
)
@@ -158,6 +174,7 @@ function Pagination({ page, total, pageSize, onChange }) {
FlatDishList — paginated dish grid for modals
============================ */
function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem }) {
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE)
const [page, setPage] = useState(1)
// Reset page when items change (e.g. search/filter)
@@ -167,8 +184,8 @@ function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem
// Use effect-less pattern: we'll reset via key prop
}
const startIdx = (page - 1) * MODAL_PAGE_SIZE
const pageItems = items.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
const startIdx = (page - 1) * pageSize
const pageItems = items.slice(startIdx, startIdx + pageSize)
return (
<div className="flat-dish-list">
@@ -189,13 +206,14 @@ function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={page} total={items.length} pageSize={MODAL_PAGE_SIZE} onChange={setPage} />
<Pagination page={page} total={items.length} pageSize={pageSize} onPageSizeChange={setPageSize} onChange={setPage} />
</div>
)
}
function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, setSwapKeyword, setSwapType, onReplace, onClose }) {
const [hover, setHover] = useState(null)
const [swapPageSize, setSwapPageSize] = useState(DEFAULT_PAGE_SIZE)
const [swapPage, setSwapPage] = useState(1)
// Reset page on filter/search change
@@ -229,8 +247,8 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
return result
}, [swapType, kw, dishes])
const startIdx = (swapPage - 1) * MODAL_PAGE_SIZE
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
const startIdx = (swapPage - 1) * swapPageSize
const pageItems = flatList.slice(startIdx, startIdx + swapPageSize)
return (
<div
@@ -284,7 +302,7 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={swapPage} total={flatList.length} pageSize={MODAL_PAGE_SIZE} onChange={setSwapPage} />
<Pagination page={swapPage} total={flatList.length} pageSize={swapPageSize} onPageSizeChange={setSwapPageSize} onChange={setSwapPage} />
</div>
</div>
</div>
@@ -295,6 +313,7 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeyword, setPickType, onChoose, onClose }) {
const [hover, setHover] = useState(null)
const [pickPageSize, setPickPageSize] = useState(DEFAULT_PAGE_SIZE)
const [pickPage, setPickPage] = useState(1)
// Reset page on filter/search change
@@ -324,8 +343,8 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
return result
}, [pickType, kw, dishes])
const startIdx = (pickPage - 1) * MODAL_PAGE_SIZE
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
const startIdx = (pickPage - 1) * pickPageSize
const pageItems = flatList.slice(startIdx, startIdx + pickPageSize)
return (
<div
@@ -381,7 +400,7 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={pickPage} total={flatList.length} pageSize={MODAL_PAGE_SIZE} onChange={setPickPage} />
<Pagination page={pickPage} total={flatList.length} pageSize={pickPageSize} onPageSizeChange={setPickPageSize} onChange={setPickPage} />
</div>
</div>
</div>
+31 -1
View File
@@ -1,4 +1,4 @@
/* ============================================
/* ============================================
学校订餐菜单生成器 — Modern Design System
Based on 2025-2026 mainstream web trends
============================================ */
@@ -1072,6 +1072,36 @@ table.excel-like.nutri-table td {
box-shadow: 0 0 0 3px var(--c-accent-light);
}
/* Page size selector */
.pg-size {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 10px;
font-size: 12px;
color: var(--c-text-muted);
white-space: nowrap;
}
.pg-size-select {
height: 30px;
padding: 0 6px;
border: 1.5px solid var(--c-border);
border-radius: var(--radius-sm);
font-size: 12px;
background: var(--c-surface);
color: var(--c-text);
outline: none;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
font-family: inherit;
}
.pg-size-select:focus {
border-color: var(--c-accent);
box-shadow: 0 0 0 3px var(--c-accent-light);
}
/* Pagination responsive */
@media (max-width: 600px) {
.pagination {
+3 -1
View File
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
@@ -8,6 +8,8 @@ export default defineConfig({
port: 5173,
proxy: {
'/api': 'http://127.0.0.1:8000',
'/admin': 'http://127.0.0.1:8000',
'/static': 'http://127.0.0.1:8000',
},
},
})
+23 -7
View File
@@ -1,4 +1,4 @@
; Inno Setup script for 学校订餐菜单生成器
; Inno Setup script for 学校订餐菜单生成器
; Build with: iscc school-meal-setup.iss
; Prerequisites: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
;
@@ -11,7 +11,7 @@ AppName=学校订餐菜单生成器
AppVersion=1.0.0
AppPublisher=学校食堂管理
AppPublisherURL=
DefaultDirName={autopf}\学校订餐菜单生成器
DefaultDirName={autopf}\SchoolMeal
DefaultGroupName=学校订餐菜单生成器
OutputDir=installer
OutputBaseFilename=学校订餐菜单生成器-安装包
@@ -25,9 +25,18 @@ ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
CloseApplications=force
RestartApplications=no
DisableProgramGroupPage=yes
UninstallDisplayIcon={app}\app.ico
LicenseFile=
VersionInfoVersion=1.0.0.0
VersionInfoCompany=学校食堂管理
VersionInfoDescription=学校订餐菜单生成器安装程序
VersionInfoProductName=学校订餐菜单生成器
VersionInfoProductVersion=1.0.0
MinVersion=10.0.17763
SetupLogging=yes
UninstallLogMode=append
UsePreviousAppDir=no
UsePreviousGroup=no
UsePreviousSetupType=no
[Languages]
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
@@ -35,6 +44,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加选项:"; Flags: checkedonce
Name: "startupicon"; Description: "开机自动启动"; GroupDescription: "附加选项:"; Flags: unchecked
[Files]
; Main executable
@@ -47,9 +57,10 @@ Source: "dist\school-meal\_internal\*"; DestDir: "{app}\_internal"; Flags: ignor
Source: "app.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; IconFilename: "{app}\app.ico"
Name: "{group}\卸载 学校订餐菜单生成器"; Filename: "{uninstallexe}"; IconFilename: "{app}\app.ico"
Name: "{autodesktop}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; Tasks: desktopicon; IconFilename: "{app}\app.ico"
Name: "{group}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; WorkingDir: "{app}"; IconFilename: "{app}\app.ico"
Name: "{group}\卸载 学校订餐菜单生成器"; Filename: "{uninstallexe}"; WorkingDir: "{app}"; IconFilename: "{app}\app.ico"
Name: "{autodesktop}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; WorkingDir: "{app}"; Tasks: desktopicon; IconFilename: "{app}\app.ico"
Name: "{userstartup}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; WorkingDir: "{app}"; Tasks: startupicon; IconFilename: "{app}\app.ico"
[Run]
Filename: "{app}\school-meal.exe"; Description: "启动 菜单生成器"; Flags: nowait postinstall skipifsilent
@@ -79,3 +90,8 @@ begin
Result := '';
Exec('taskkill', '/F /IM school-meal.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
end;