营养校准、后台美化、静态走对象存储 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:
+18
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user