fix: 修复 EXE 数据库 schema 不匹配导致 500 错误
- ISS 移除 onlyifdoesntexist 确保安装目录 db.sqlite3 始终更新 - .gitignore 忽略所有 sqlite3 文件 - 前端翻页组件和样式更新
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ frontend/node_modules/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
backend/db.sqlite3
|
*.sqlite3
|
||||||
backend/staticfiles/
|
backend/staticfiles/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
+13
-1
@@ -1,4 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
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"
|
set "ISCC=C:\Program Files\Inno Setup 6\ISCC.exe"
|
||||||
) else if exist "C:\Program Files (x86)\Inno Setup 5\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"
|
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 (
|
) else (
|
||||||
where iscc >nul 2>&1
|
where iscc >nul 2>&1
|
||||||
if not errorlevel 1 (
|
if not errorlevel 1 (
|
||||||
@@ -60,6 +62,16 @@ if "%ISCC%"=="" (
|
|||||||
)
|
)
|
||||||
echo 找到 Inno Setup: %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.
|
||||||
echo [4/4] 编译安装包...
|
echo [4/4] 编译安装包...
|
||||||
"%ISCC%" school-meal-setup.iss
|
"%ISCC%" school-meal-setup.iss
|
||||||
|
|||||||
+30
-11
@@ -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_TO_TAB = { meats: 'meat', vegs: 'veg', soup: 'soup', staple: 'staple' }
|
||||||
const TYPE_ALL_LABELS = { meat: '荤菜', veg: '素菜', soup: '营养汤', staple: '主食' }
|
const TYPE_ALL_LABELS = { meat: '荤菜', veg: '素菜', soup: '营养汤', staple: '主食' }
|
||||||
const DEFAULT_SCHOOL = '学校菜单'
|
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 */
|
/** Compute ISO week Monday/Friday as Date objects */
|
||||||
const weekDates = (weekNo) => {
|
const weekDates = (weekNo) => {
|
||||||
@@ -63,7 +64,7 @@ const slotKey = (menu, day, slot, idx) => `${menu}|${day}|${slot}|${idx}`
|
|||||||
/* ============================
|
/* ============================
|
||||||
Modern Pagination Component
|
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 totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||||
const currentPage = Math.min(Math.max(1, page), totalPages)
|
const currentPage = Math.min(Math.max(1, page), totalPages)
|
||||||
if (totalPages <= 1) return null
|
if (totalPages <= 1) return null
|
||||||
@@ -149,6 +150,21 @@ function Pagination({ page, total, pageSize, onChange }) {
|
|||||||
/>
|
/>
|
||||||
页
|
页
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -158,6 +174,7 @@ function Pagination({ page, total, pageSize, onChange }) {
|
|||||||
FlatDishList — paginated dish grid for modals
|
FlatDishList — paginated dish grid for modals
|
||||||
============================ */
|
============================ */
|
||||||
function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem }) {
|
function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem }) {
|
||||||
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
// Reset page when items change (e.g. search/filter)
|
// 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
|
// Use effect-less pattern: we'll reset via key prop
|
||||||
}
|
}
|
||||||
|
|
||||||
const startIdx = (page - 1) * MODAL_PAGE_SIZE
|
const startIdx = (page - 1) * pageSize
|
||||||
const pageItems = items.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
|
const pageItems = items.slice(startIdx, startIdx + pageSize)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flat-dish-list">
|
<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>}
|
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, setSwapKeyword, setSwapType, onReplace, onClose }) {
|
function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, setSwapKeyword, setSwapType, onReplace, onClose }) {
|
||||||
const [hover, setHover] = useState(null)
|
const [hover, setHover] = useState(null)
|
||||||
|
const [swapPageSize, setSwapPageSize] = useState(DEFAULT_PAGE_SIZE)
|
||||||
const [swapPage, setSwapPage] = useState(1)
|
const [swapPage, setSwapPage] = useState(1)
|
||||||
|
|
||||||
// Reset page on filter/search change
|
// Reset page on filter/search change
|
||||||
@@ -229,8 +247,8 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
|
|||||||
return result
|
return result
|
||||||
}, [swapType, kw, dishes])
|
}, [swapType, kw, dishes])
|
||||||
|
|
||||||
const startIdx = (swapPage - 1) * MODAL_PAGE_SIZE
|
const startIdx = (swapPage - 1) * swapPageSize
|
||||||
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
|
const pageItems = flatList.slice(startIdx, startIdx + swapPageSize)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -284,7 +302,7 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
|
|||||||
))}
|
))}
|
||||||
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
|
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
|
||||||
</div>
|
</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>
|
</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 }) {
|
function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeyword, setPickType, onChoose, onClose }) {
|
||||||
const [hover, setHover] = useState(null)
|
const [hover, setHover] = useState(null)
|
||||||
|
const [pickPageSize, setPickPageSize] = useState(DEFAULT_PAGE_SIZE)
|
||||||
const [pickPage, setPickPage] = useState(1)
|
const [pickPage, setPickPage] = useState(1)
|
||||||
|
|
||||||
// Reset page on filter/search change
|
// Reset page on filter/search change
|
||||||
@@ -324,8 +343,8 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
|
|||||||
return result
|
return result
|
||||||
}, [pickType, kw, dishes])
|
}, [pickType, kw, dishes])
|
||||||
|
|
||||||
const startIdx = (pickPage - 1) * MODAL_PAGE_SIZE
|
const startIdx = (pickPage - 1) * pickPageSize
|
||||||
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
|
const pageItems = flatList.slice(startIdx, startIdx + pickPageSize)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -381,7 +400,7 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
|
|||||||
))}
|
))}
|
||||||
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
|
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+31
-1
@@ -1,4 +1,4 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
学校订餐菜单生成器 — Modern Design System
|
学校订餐菜单生成器 — Modern Design System
|
||||||
Based on 2025-2026 mainstream web trends
|
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);
|
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 */
|
/* Pagination responsive */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.pagination {
|
.pagination {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -8,6 +8,8 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://127.0.0.1:8000',
|
'/api': 'http://127.0.0.1:8000',
|
||||||
|
'/admin': 'http://127.0.0.1:8000',
|
||||||
|
'/static': 'http://127.0.0.1:8000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+23
-7
@@ -1,4 +1,4 @@
|
|||||||
; Inno Setup script for 学校订餐菜单生成器
|
; Inno Setup script for 学校订餐菜单生成器
|
||||||
; Build with: iscc school-meal-setup.iss
|
; Build with: iscc school-meal-setup.iss
|
||||||
; Prerequisites: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
; Prerequisites: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
||||||
;
|
;
|
||||||
@@ -11,7 +11,7 @@ AppName=学校订餐菜单生成器
|
|||||||
AppVersion=1.0.0
|
AppVersion=1.0.0
|
||||||
AppPublisher=学校食堂管理
|
AppPublisher=学校食堂管理
|
||||||
AppPublisherURL=
|
AppPublisherURL=
|
||||||
DefaultDirName={autopf}\学校订餐菜单生成器
|
DefaultDirName={autopf}\SchoolMeal
|
||||||
DefaultGroupName=学校订餐菜单生成器
|
DefaultGroupName=学校订餐菜单生成器
|
||||||
OutputDir=installer
|
OutputDir=installer
|
||||||
OutputBaseFilename=学校订餐菜单生成器-安装包
|
OutputBaseFilename=学校订餐菜单生成器-安装包
|
||||||
@@ -25,9 +25,18 @@ ArchitecturesAllowed=x64compatible
|
|||||||
ArchitecturesInstallIn64BitMode=x64compatible
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
CloseApplications=force
|
CloseApplications=force
|
||||||
RestartApplications=no
|
RestartApplications=no
|
||||||
DisableProgramGroupPage=yes
|
|
||||||
UninstallDisplayIcon={app}\app.ico
|
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]
|
[Languages]
|
||||||
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
||||||
@@ -35,6 +44,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
|||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加选项:"; Flags: checkedonce
|
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加选项:"; Flags: checkedonce
|
||||||
|
Name: "startupicon"; Description: "开机自动启动"; GroupDescription: "附加选项:"; Flags: unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
; Main executable
|
; Main executable
|
||||||
@@ -47,9 +57,10 @@ Source: "dist\school-meal\_internal\*"; DestDir: "{app}\_internal"; Flags: ignor
|
|||||||
Source: "app.ico"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "app.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; IconFilename: "{app}\app.ico"
|
Name: "{group}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; WorkingDir: "{app}"; IconFilename: "{app}\app.ico"
|
||||||
Name: "{group}\卸载 学校订餐菜单生成器"; Filename: "{uninstallexe}"; IconFilename: "{app}\app.ico"
|
Name: "{group}\卸载 学校订餐菜单生成器"; Filename: "{uninstallexe}"; WorkingDir: "{app}"; IconFilename: "{app}\app.ico"
|
||||||
Name: "{autodesktop}\学校订餐菜单生成器"; Filename: "{app}\school-meal.exe"; Tasks: desktopicon; 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]
|
[Run]
|
||||||
Filename: "{app}\school-meal.exe"; Description: "启动 菜单生成器"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\school-meal.exe"; Description: "启动 菜单生成器"; Flags: nowait postinstall skipifsilent
|
||||||
@@ -79,3 +90,8 @@ begin
|
|||||||
Result := '';
|
Result := '';
|
||||||
Exec('taskkill', '/F /IM school-meal.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
Exec('taskkill', '/F /IM school-meal.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
end;
|
||||||
Reference in New Issue
Block a user