feat: 现代化翻页组件 + 完整项目更新

- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航
- PickModal 和 SwapModal 集成分页功能,每页20项
- 搜索/筛选切换自动回到第1页
- 现代化CSS样式:hover浮起动效、主色高亮、移动端适配
- 新增 dashboard、admin 管理模板
- 菜系(cuisine)模型和迁移
- 构建脚本 build.py 支持 PyInstaller 打包
- 前端资源重新构建
This commit is contained in:
Chunyu
2026-08-28 13:44:21 +08:00
parent 65b52b052f
commit d7553520ce
51 changed files with 4318 additions and 441 deletions
+6
View File
@@ -2,7 +2,13 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="512x512" href="/icon_512.png" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="512x512" href="/icon_512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="学校订餐菜单生成器 - 自动生成营养均衡的周菜单" />
<meta name="theme-color" content="#6366f1" />
<title>学校订餐菜单生成器</title>
</head>
<body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="55%" stop-color="#5457e5"/>
<stop offset="100%" stop-color="#4338ca"/>
</linearGradient>
<linearGradient id="bowlGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#dbeafe"/>
</linearGradient>
<linearGradient id="bowlRim" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#eef2ff"/>
</linearGradient>
<linearGradient id="chopstick" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#fbbf24"/>
<stop offset="100%" stop-color="#f59e0b"/>
</linearGradient>
<linearGradient id="brothGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#fde68a"/>
<stop offset="100%" stop-color="#fcd34d"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"/>
<feOffset dx="0" dy="6" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.15"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Rounded square background -->
<rect width="512" height="512" rx="112" fill="url(#bgGrad)"/>
<!-- Subtle decorative circle (menu/cycle concept) -->
<circle cx="256" cy="256" r="200" fill="none" stroke="#818cf8" stroke-width="3" stroke-dasharray="2 14" opacity="0.25"/>
<!-- Steam lines (smooth waves) -->
<path d="M196 154 C188 134 204 114 196 94 C188 74 204 54 196 40" stroke="#c7d2fe" stroke-width="13" stroke-linecap="round" fill="none" opacity="0.65"/>
<path d="M256 154 C248 134 264 114 256 94 C248 74 264 54 256 40" stroke="#c7d2fe" stroke-width="13" stroke-linecap="round" fill="none" opacity="0.65"/>
<path d="M316 154 C308 134 324 114 316 94 C308 74 324 54 316 40" stroke="#c7d2fe" stroke-width="13" stroke-linecap="round" fill="none" opacity="0.65"/>
<!-- Chopsticks (pair, angled) -->
<rect x="296" y="118" width="15" height="175" rx="7.5" fill="url(#chopstick)" transform="rotate(22 303.5 205.5)"/>
<rect x="322" y="128" width="15" height="170" rx="7.5" fill="url(#chopstick)" transform="rotate(28 329.5 213)"/>
<!-- Bowl body -->
<path d="M120 242 L392 242 Q376 350 256 370 Q136 350 120 242 Z" fill="url(#bowlGrad)" filter="url(#shadow)"/>
<!-- Bowl rim (ellipse on top) -->
<ellipse cx="256" cy="242" rx="136" ry="24" fill="url(#bowlRim)"/>
<!-- Bowl inner area (filled broth) -->
<ellipse cx="256" cy="242" rx="120" ry="18" fill="url(#brothGrad)" opacity="0.7"/>
<!-- Food items in bowl -->
<!-- Rice / grains -->
<ellipse cx="210" cy="240" rx="24" ry="8" fill="#fef3c7" opacity="0.9"/>
<ellipse cx="256" cy="236" rx="28" ry="9" fill="#fef3c7" opacity="0.9"/>
<ellipse cx="300" cy="240" rx="20" ry="7" fill="#fef3c7" opacity="0.85"/>
<!-- Small vegetable / herb accents -->
<circle cx="224" cy="234" r="6" fill="#86efac" opacity="0.85"/>
<circle cx="288" cy="234" r="6" fill="#86efac" opacity="0.85"/>
<circle cx="256" cy="228" r="5" fill="#fb923c" opacity="0.85"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+308 -79
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { pinyin } from 'pinyin-pro'
const DAYS = ['周一', '周二', '周三', '周四', '周五']
@@ -18,7 +18,34 @@ const TYPE_GROUPS = [
]
const TYPE_LABELS = { meats: '荤菜', vegs: '素菜', soup: '营养汤', staple: '主食' }
const TYPE_TO_TAB = { meats: 'meat', vegs: 'veg', soup: 'soup', staple: 'staple' }
const DEFAULT_SCHOOL = '上海外国语大学附属宝山双语学校'
const TYPE_ALL_LABELS = { meat: '荤菜', veg: '素菜', soup: '营养汤', staple: '主食' }
const DEFAULT_SCHOOL = '学校菜单'
const MODAL_PAGE_SIZE = 20
/** Compute ISO week Monday/Friday as Date objects */
const weekDates = (weekNo) => {
const now = new Date()
const isoYear = now.getFullYear()
const jan4 = new Date(isoYear, 0, 4)
const startOfISOWeek1 = new Date(jan4)
startOfISOWeek1.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7))
const monday = new Date(startOfISOWeek1)
monday.setDate(startOfISOWeek1.getDate() + (weekNo - 1) * 7)
const friday = new Date(monday)
friday.setDate(monday.getDate() + 4)
return { monday, friday }
}
/** Format Date as M.D for title */
const fmtDateShort = (d) => `${d.getMonth() + 1}.${d.getDate()}`
/** Format Date as YYYY-MM-DD for date input */
const fmtDateISO = (d) => {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const currentIsoWeek = () => {
const now = new Date()
@@ -33,8 +60,147 @@ const currentIsoWeek = () => {
const slotKey = (menu, day, slot, idx) => `${menu}|${day}|${slot}|${idx}`
/* ============================
Modern Pagination Component
============================ */
function Pagination({ page, total, pageSize, onChange }) {
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const currentPage = Math.min(Math.max(1, page), totalPages)
if (totalPages <= 1) return null
const go = (p) => onChange(Math.min(Math.max(1, p), totalPages))
// Build page number list with ellipsis
const pages = []
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 1 && i <= currentPage + 1)) {
pages.push(i)
} else if (pages.length > 0 && pages[pages.length - 1] !== '...') {
pages.push('...')
}
}
const startItem = (currentPage - 1) * pageSize + 1
const endItem = Math.min(currentPage * pageSize, total)
return (
<div className="pagination">
<div className="pg-info">
显示 {startItem}-{endItem} / 共 {total} 项
</div>
<div className="pg-controls">
<button
className="pg-btn pg-arrow"
disabled={currentPage <= 1}
onClick={() => go(1)}
title="首页"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>
</button>
<button
className="pg-btn pg-arrow"
disabled={currentPage <= 1}
onClick={() => go(currentPage - 1)}
title="上一页"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
{pages.map((p, i) =>
p === '...' ? (
<span key={`e${i}`} className="pg-ellipsis">&hellip;</span>
) : (
<button
key={p}
className={`pg-btn pg-num ${p === currentPage ? 'active' : ''}`}
onClick={() => go(p)}
>
{p}
</button>
),
)}
<button
className="pg-btn pg-arrow"
disabled={currentPage >= totalPages}
onClick={() => go(currentPage + 1)}
title="下一页"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
<button
className="pg-btn pg-arrow"
disabled={currentPage >= totalPages}
onClick={() => go(totalPages)}
title="末页"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>
</button>
<span className="pg-jump">
跳至
<input
type="number"
min="1"
max={totalPages}
value={currentPage}
onChange={(e) => {
const v = parseInt(e.target.value)
if (v >= 1 && v <= totalPages) go(v)
}}
className="pg-jump-input"
/>
页
</span>
</div>
</div>
)
}
/* ============================
FlatDishList — paginated dish grid for modals
============================ */
function FlatDishList({ items, currentId, hover, setHover, onSelect, renderItem }) {
const [page, setPage] = useState(1)
// Reset page when items change (e.g. search/filter)
const itemsKey = items.map(d => d.id).join(',')
const prevItemsKey = useMemo(() => itemsKey, [])
if (itemsKey !== prevItemsKey) {
// 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)
return (
<div className="flat-dish-list">
<div className="modal-items-grid">
{pageItems.map((d) => (
<button
key={d.id}
className={`modal-item ${d.id === currentId ? 'current' : ''}`}
onClick={() => onSelect(d)}
onMouseEnter={(e) => setHover({ dish: d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) => setHover((h) => (h && h.dish.id === d.id ? { dish: d, x: e.clientX, y: e.clientY } : h))}
onMouseLeave={() => setHover(null)}
>
<span className="modal-item-badge">{TYPE_ALL_LABELS[d.dish_type] || d.dish_type}</span>
<span className="item-name">{d.name}</span>
{d.ingredient_detail && <span className="item-ing">{d.ingredient_detail}</span>}
</button>
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={page} total={items.length} pageSize={MODAL_PAGE_SIZE} onChange={setPage} />
</div>
)
}
function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, setSwapKeyword, setSwapType, onReplace, onClose }) {
const [hover, setHover] = useState(null)
const [swapPage, setSwapPage] = useState(1)
// Reset page on filter/search change
useEffect(() => { setSwapPage(1) }, [swapKeyword, swapType])
if (!editing || !menus) return null
const { menuKey, day, type, idx } = editing
@@ -51,9 +217,20 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
const p = pinyinMap[d.id]
return d.name.toLowerCase().includes(kw) || (p && (p.full.includes(kw) || p.initials.includes(kw)))
}
const groups = (swapType === 'all' ? TYPE_GROUPS : TYPE_GROUPS.filter(([t]) => t === swapType))
.map(([t, label]) => [t, label, dishes[t].filter(match)])
.filter(([, , pool]) => pool.length)
const flatList = useMemo(() => {
const result = []
const groups = (swapType === 'all' ? TYPE_GROUPS : TYPE_GROUPS.filter(([t]) => t === swapType))
for (const [t] of groups) {
for (const d of dishes[t]) {
if (match(d)) result.push(d)
}
}
return result
}, [swapType, kw, dishes])
const startIdx = (swapPage - 1) * MODAL_PAGE_SIZE
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
return (
<div
@@ -65,11 +242,11 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-head">
<h3>
替换{menuLabel}·{day}「{slotLabel}」
替换{menuLabel}&middot;{day}&laquo;{slotLabel}&raquo;
{current ? `(当前:${current.name})` : ''}
</h3>
<button className="modal-close" onClick={onClose}>
×
&times;
</button>
</div>
<div className="modal-toolbar">
@@ -83,43 +260,32 @@ function SwapModal({ editing, menus, dishes, pinyinMap, swapKeyword, swapType, s
</div>
<input
className="pick-search"
placeholder="搜索菜名…"
placeholder="搜索菜名或拼音..."
value={swapKeyword}
onChange={(e) => setSwapKeyword(e.target.value)}
/>
</div>
<div className="modal-body">
{groups.map(([t, label, pool]) => {
const cuisines = [...new Set(pool.map((d) => d.cuisine || '未分类'))]
return (
<div className="modal-group" key={t}>
<div className="modal-group-label">{label}</div>
{cuisines.map((g) => (
<div key={g} className="modal-subgroup">
<div className="modal-subgroup-label">{g}</div>
<div className="modal-items">
{pool
.filter((d) => (d.cuisine || '未分类') === g)
.map((d) => (
<button
key={d.id}
className={`modal-item ${d.id === current?.id ? 'current' : ''}`}
onClick={() => onReplace(menuKey, day, type, idx, d.id)}
onMouseEnter={(e) => setHover({ dish: d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) => setHover((h) => (h && h.dish.id === d.id ? { dish: d, x: e.clientX, y: e.clientY } : h))}
onMouseLeave={() => setHover(null)}
>
<span className="item-name">{d.name}</span>
{d.ingredient_detail && <span className="item-ing">({d.ingredient_detail})</span>}
</button>
))}
</div>
</div>
))}
</div>
)
})}
{!groups.length && <p className="hint warn">没有匹配的菜品</p>}
<div className="flat-dish-list">
<div className="modal-items-grid">
{pageItems.map((d) => (
<button
key={d.id}
className={`modal-item ${d.id === current?.id ? 'current' : ''}`}
onClick={() => onReplace(menuKey, day, type, idx, d.id)}
onMouseEnter={(e) => setHover({ dish: d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) => setHover((h) => (h && h.dish.id === d.id ? { dish: d, x: e.clientX, y: e.clientY } : h))}
onMouseLeave={() => setHover(null)}
>
<span className="modal-item-badge">{TYPE_ALL_LABELS[d.dish_type] || d.dish_type}</span>
<span className="item-name">{d.name}</span>
{d.ingredient_detail && <span className="item-ing">{d.ingredient_detail}</span>}
</button>
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={swapPage} total={flatList.length} pageSize={MODAL_PAGE_SIZE} onChange={setSwapPage} />
</div>
</div>
</div>
{hover && <DishTooltip dish={hover.dish} x={hover.x} y={hover.y} />}
@@ -129,6 +295,11 @@ 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 [pickPage, setPickPage] = useState(1)
// Reset page on filter/search change
useEffect(() => { setPickPage(1) }, [keyword, pickType])
if (!pick) return null
const { menu, day, slot, idx } = pick
const key = slotKey(menu, day, slot, idx)
@@ -141,9 +312,20 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
const p = pinyinMap[d.id]
return d.name.toLowerCase().includes(kw) || (p && (p.full.includes(kw) || p.initials.includes(kw)))
}
const groups = (pickType === 'all' ? TYPE_GROUPS : TYPE_GROUPS.filter(([t]) => t === pickType))
.map(([t, label]) => [t, label, dishes[t].filter(match)])
.filter(([, , pool]) => pool.length)
const flatList = useMemo(() => {
const result = []
const groups = (pickType === 'all' ? TYPE_GROUPS : TYPE_GROUPS.filter(([t]) => t === pickType))
for (const [t] of groups) {
for (const d of dishes[t]) {
if (match(d)) result.push(d)
}
}
return result
}, [pickType, kw, dishes])
const startIdx = (pickPage - 1) * MODAL_PAGE_SIZE
const pageItems = flatList.slice(startIdx, startIdx + MODAL_PAGE_SIZE)
return (
<div
@@ -155,11 +337,11 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-head">
<h3>
自选{menuLabel}·{day}「{slotLabel}」
自选{menuLabel}&middot;{day}&laquo;{slotLabel}&raquo;
{current ? `(当前:${current.name})` : '(未选择,可留空自动生成)'}
</h3>
<button className="modal-close" onClick={onClose}>
×
&times;
</button>
</div>
<div className="modal-toolbar">
@@ -175,43 +357,32 @@ function PickModal({ pick, slots, dishes, pinyinMap, keyword, pickType, setKeywo
</div>
<input
className="pick-search"
placeholder="搜索菜名…"
placeholder="搜索菜名或拼音..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
<div className="modal-body">
{groups.map(([t, label, pool]) => {
const cuisines = [...new Set(pool.map((d) => d.cuisine || '未分类'))]
return (
<div className="modal-group" key={t}>
<div className="modal-group-label">{label}</div>
{cuisines.map((g) => (
<div key={g} className="modal-subgroup">
<div className="modal-subgroup-label">{g}</div>
<div className="modal-items">
{pool
.filter((d) => (d.cuisine || '未分类') === g)
.map((d) => (
<button
key={d.id}
className={`modal-item ${d.id === current?.id ? 'current' : ''}`}
onClick={() => onChoose(menu, day, slot, idx, d)}
onMouseEnter={(e) => setHover({ dish: d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) => setHover((h) => (h && h.dish.id === d.id ? { dish: d, x: e.clientX, y: e.clientY } : h))}
onMouseLeave={() => setHover(null)}
>
<span className="item-name">{d.name}</span>
{d.ingredient_detail && <span className="item-ing">({d.ingredient_detail})</span>}
</button>
))}
</div>
</div>
))}
</div>
)
})}
{!groups.length && <p className="hint warn">没有匹配的菜品</p>}
<div className="flat-dish-list">
<div className="modal-items-grid">
{pageItems.map((d) => (
<button
key={d.id}
className={`modal-item ${d.id === current?.id ? 'current' : ''}`}
onClick={() => onChoose(menu, day, slot, idx, d)}
onMouseEnter={(e) => setHover({ dish: d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) => setHover((h) => (h && h.dish.id === d.id ? { dish: d, x: e.clientX, y: e.clientY } : h))}
onMouseLeave={() => setHover(null)}
>
<span className="modal-item-badge">{TYPE_ALL_LABELS[d.dish_type] || d.dish_type}</span>
<span className="item-name">{d.name}</span>
{d.ingredient_detail && <span className="item-ing">{d.ingredient_detail}</span>}
</button>
))}
{pageItems.length === 0 && <p className="hint warn">没有匹配的菜品</p>}
</div>
<Pagination page={pickPage} total={flatList.length} pageSize={MODAL_PAGE_SIZE} onChange={setPickPage} />
</div>
</div>
</div>
{hover && <DishTooltip dish={hover.dish} x={hover.x} y={hover.y} />}
@@ -248,6 +419,12 @@ function App() {
const [error, setError] = useState('')
const [schoolName, setSchoolName] = useState(DEFAULT_SCHOOL)
const [weekNo, setWeekNo] = useState(String(currentIsoWeek()))
const initDates = weekDates(Number(currentIsoWeek()))
const [dateStart, setDateStart] = useState(fmtDateISO(initDates.monday))
const [dateEnd, setDateEnd] = useState(fmtDateISO(initDates.friday))
const [pinyinMap, setPinyinMap] = useState({})
useEffect(() => {
@@ -262,6 +439,14 @@ function App() {
setPinyinMap(map)
}, [dishes])
useEffect(() => {
if (weekNo) {
const d = weekDates(Number(weekNo))
setDateStart(fmtDateISO(d.monday))
setDateEnd(fmtDateISO(d.friday))
}
}, [weekNo])
useEffect(() => {
fetch('/api/dishes/')
.then((r) => r.json())
@@ -294,6 +479,8 @@ function App() {
selections,
school_name: schoolName.trim() || DEFAULT_SCHOOL,
week_no: Number(weekNo),
date_start: dateStart || undefined,
date_end: dateEnd || undefined,
}),
})
const data = await res.json()
@@ -479,7 +666,7 @@ function App() {
}
const nutritionText = `营养分析:蛋白质≥${Math.min(...proteins).toFixed(1)}g 脂肪≤${Math.max(...fats).toFixed(1)}g 热量≥${Math.round(Math.min(...calories))}kcal`
return (
<section className="panel table-wrap" style={{ width: '920px' }}>
<section className="panel table-wrap menu-block">
<div className="panel-head">
<h2>{idx === 0 ? '营养A菜单' : '营养B菜单'}</h2>
<span className="tip">
@@ -602,8 +789,50 @@ function App() {
</button>
</div>
</div>
<div className="title-date-fields">
<div className="title-field date-field">
<label className="label">{`菜单日期范围`}</label>
<div className="date-range-input">
<input
type="date"
value={dateStart}
onChange={(e) => setDateStart(e.target.value)}
/>
<span className="date-sep">{'—'}</span>
<input
type="date"
value={dateEnd}
onChange={(e) => setDateEnd(e.target.value)}
/>
</div>
</div>
</div>
<p className="hint title-preview">
标题预览:{schoolName.trim() || DEFAULT_SCHOOL}[第{weekNo || '?'}周]日期营养X菜单
{`标题预览:${schoolName.trim() || DEFAULT_SCHOOL}[第${weekNo || '?'}周]${dateStart && dateEnd ? `${fmtDateShort(new Date(dateStart))}--${fmtDateShort(new Date(dateEnd))}` : `日期`}营养A菜单`}
<br />
{`${schoolName.trim() || DEFAULT_SCHOOL}[第${weekNo || '?'}周]${dateStart && dateEnd ? `${fmtDateShort(new Date(dateStart))}--${fmtDateShort(new Date(dateEnd))}` : `日期`}营养B菜单`}
</p>
</div>
<div className="field">
@@ -674,4 +903,4 @@ function App() {
)
}
export default App
export default App
+684 -248
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: 'https://qnycdn.mymoyu.top/xx/web/',
base: '/',
server: {
port: 5173,
proxy: {