营养校准、后台美化、静态走对象存储 CDN、标题可编辑、admin 拼音搜索

- 菜品营养按食物成分表+烹饪方式校准(calibrate_nutrition 命令)
- 后台接入 jazzmin 主题美化,移除 Google Fonts(模板覆盖)
- 前端/后台静态资源指向 qnycdn 对象存储 CDN(vite base + DJANGO_STATIC_URL)
- 菜单标题学校名称/周数可编辑,日期随周数计算
- admin 搜索支持拼音首字母/全拼(Dish.pinyin 索引,pypinyin)
- Dockerfile 使用清华 pip 镜像源
This commit is contained in:
root
2026-08-06 21:11:14 +08:00
parent 75f214dc40
commit 65b52b052f
16 changed files with 805 additions and 16 deletions
+18 -1
View File
@@ -4,7 +4,7 @@ upstream django_backend {
server {
listen 80;
server_name xx.mymoyu.top _;
server_name xx.mymoyu.top qnycdn.mymoyu.top _;
client_max_body_size 20m;
gzip on;
@@ -30,6 +30,23 @@ server {
expires 7d;
}
location /xx/web/ {
alias /usr/share/nginx/html/;
index index.html;
expires 7d;
}
location /xx/web/assets/ {
alias /usr/share/nginx/html/assets/;
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
}
location /xx/admin/ {
alias /static/;
expires 7d;
}
location / {
root /usr/share/nginx/html;
index index.html;
+49 -1
View File
@@ -18,6 +18,18 @@ 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 currentIsoWeek = () => {
const now = new Date()
const target = new Date(now.valueOf())
const dayNr = (now.getDay() + 6) % 7
target.setDate(target.getDate() - dayNr + 3)
const firstThursday = new Date(target.getFullYear(), 0, 4)
const firstDayNr = (firstThursday.getDay() + 6) % 7
firstThursday.setDate(firstThursday.getDate() - firstDayNr + 3)
return 1 + Math.round((target - firstThursday) / (7 * 24 * 3600 * 1000))
}
const slotKey = (menu, day, slot, idx) => `${menu}|${day}|${slot}|${idx}`
@@ -234,6 +246,8 @@ function App() {
const [swapKeyword, setSwapKeyword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [schoolName, setSchoolName] = useState(DEFAULT_SCHOOL)
const [weekNo, setWeekNo] = useState(String(currentIsoWeek()))
const [pinyinMap, setPinyinMap] = useState({})
useEffect(() => {
@@ -275,7 +289,12 @@ function App() {
const res = await fetch('/api/menu/generate/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ partial: true, selections }),
body: JSON.stringify({
partial: true,
selections,
school_name: schoolName.trim() || DEFAULT_SCHOOL,
week_no: Number(weekNo),
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.detail || '生成失败')
@@ -558,6 +577,35 @@ function App() {
</header>
<section className="panel">
<div className="field title-fields">
<div className="title-field">
<label className="label">菜单标题 · 学校名称</label>
<input
className="pick-search"
value={schoolName}
onChange={(e) => setSchoolName(e.target.value)}
placeholder="学校名称"
/>
</div>
<div className="title-field week-field">
<label className="label">周数</label>
<div className="week-input">
<input
type="number"
min="1"
max="53"
value={weekNo}
onChange={(e) => setWeekNo(e.target.value)}
/>
<button className="btn small" onClick={() => setWeekNo(String(currentIsoWeek()))}>
当前周
</button>
</div>
</div>
<p className="hint title-preview">
标题预览:{schoolName.trim() || DEFAULT_SCHOOL}[第{weekNo || '?'}周]日期营养X菜单
</p>
</div>
<div className="field">
<div className="label-row">
<label className="label">自选菜品(A / B 两套分别指定,已选 {selectedCount} 道)</label>
+42
View File
@@ -426,6 +426,48 @@ td:hover .swap-btn,
align-items: start;
}
.title-fields {
display: flex;
align-items: flex-end;
gap: 24px;
flex-wrap: wrap;
background: #f7f9fc;
border: 1px solid #e3e9f2;
border-radius: 12px;
padding: 16px 18px;
margin-bottom: 20px;
}
.title-field .label {
margin-bottom: 6px;
font-size: 13px;
}
.title-field input.pick-search {
min-width: 320px;
}
.title-field.week-field input[type='number'] {
width: 90px;
padding: 9px 12px;
border: 1.5px solid #d5deea;
border-radius: 8px;
font-size: 14px;
}
.week-input {
display: flex;
gap: 10px;
align-items: center;
}
.title-preview {
width: 100%;
margin: 8px 0 0;
color: #2f5597;
font-weight: 600;
}
.slot-panel {
border: 1px solid #e3e9f2;
border-radius: 12px;
+1
View File
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: 'https://qnycdn.mymoyu.top/xx/web/',
server: {
port: 5173,
proxy: {