diff --git a/README.md b/README.md new file mode 100644 index 0000000..511efbc --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Zhangmenu 项目 + +聚会菜单管理应用 + +## 启动服务 + +```bash +python3 manage.py runserver 0.0.0.0:9300 +``` + +访问地址: http://你的IP:9300 + +## 技术栈 + +- Django + Django REST Framework +- Bootstrap 5 +- SQLite diff --git a/menu/views.py b/menu/views.py index a05c6d0..4fa657d 100644 --- a/menu/views.py +++ b/menu/views.py @@ -48,7 +48,7 @@ def upload_image(request): media_path = str(saved_path).lstrip('/') media_url = urljoin(settings.MEDIA_URL, media_path) - return JsonResponse({'url': request.build_absolute_uri(media_url)}) + return JsonResponse({'url': media_url}) class DishCategoryViewSet(viewsets.ModelViewSet): diff --git a/templates/index.html b/templates/index.html index 26db34e..011bfd1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3575,6 +3575,161 @@ gap: 8px; } + /* Participant editing in detail modal */ + .participant-edit-wrapper { + margin-top: 8px; + } + + .participant-edit-input .form-control { + background: rgba(255,255,255,0.9); + border: 1px solid #e5d3c3; + border-radius: 20px; + padding: 8px 16px; + font-size: 14px; + } + + .participant-edit-input .form-control:focus { + background: #fff; + border-color: #c9a86c; + box-shadow: 0 0 0 3px rgba(201,168,108,0.15); + } + + .participant-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: linear-gradient(135deg, #f8f4ed 0%, #efe6d8 100%); + border: 1px solid #e5d3c3; + border-radius: 20px; + font-size: 14px; + color: #6b5344; + animation: tagAppear 0.2s ease-out; + } + + @keyframes tagAppear { + from { opacity: 0; transform: scale(0.8); } + to { opacity: 1; transform: scale(1); } + } + + .participant-remove-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + border: none; + background: rgba(180,140,100,0.2); + border-radius: 50%; + font-size: 14px; + line-height: 1; + color: #8b6f5a; + cursor: pointer; + transition: all 0.15s; + } + + .participant-remove-btn:hover { + background: #e74c3c; + color: white; + } + + /* Ranking styles */ + .ranking-item { + margin-bottom: 16px; + padding: 12px 14px; + background: rgba(255,255,255,0.5); + border-radius: 12px; + border: 1px solid rgba(0,0,0,0.04); + transition: transform 0.2s, box-shadow 0.2s; + } + + .ranking-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.08); + } + + .ranking-item.rank-gold { + background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); + border-color: #fbbf24; + } + + .ranking-item.rank-silver { + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); + border-color: #94a3b8; + } + + .ranking-item.rank-bronze { + background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%); + border-color: #d97706; + } + + .ranking-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + } + + .ranking-rank { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; + padding: 0 6px; + background: #e5e7eb; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + color: #6b7280; + } + + .rank-gold .ranking-rank { + background: linear-gradient(135deg, #fbbf24, #f59e0b); + color: #78350f; + } + + .rank-silver .ranking-rank { + background: linear-gradient(135deg, #94a3b8, #64748b); + color: white; + } + + .rank-bronze .ranking-rank { + background: linear-gradient(135deg, #d97706, #b45309); + color: white; + } + + .ranking-name { + flex: 1; + font-size: 15px; + font-weight: 500; + color: #374151; + } + + .ranking-count { + font-size: 14px; + font-weight: 600; + color: #6b7280; + } + + .rank-gold .ranking-count { + color: #b45309; + } + + .ranking-progress { + height: 10px; + background: rgba(0,0,0,0.06); + border-radius: 5px; + overflow: hidden; + } + + .ranking-bar { + height: 100%; + border-radius: 5px; + transition: width 0.5s ease-out; + } + .ingredient-row { margin: 0 0 6px; padding: 12px 10px; @@ -4391,7 +4546,12 @@ 参与人员 -
+
+
+ +
+
+

@@ -5767,15 +5927,13 @@ ingContainer.innerHTML = '

暂时还没有汇总出食材,等选好菜后这里会自动整理。

'; } - // Render participants - const participantsContainer = document.getElementById('order-detail-participants'); - if (order.participants && order.participants.length > 0) { - participantsContainer.innerHTML = `
${order.participants.map(name => ` - ${name} - `).join('')}
`; - } else { - participantsContainer.innerHTML = '

这次还没记录参与人。

'; - } + // Render participants with edit capability + window.detailOrderParticipants = order && order.participants ? [...order.participants] : []; + renderDetailParticipantTags(); + setupDetailParticipantInput(); + + // Update summary to reflect changes + updateDetailSummary(); document.getElementById('btn-complete-order').style.display = order.status === 'in_progress' ? 'block' : 'none'; @@ -5792,6 +5950,89 @@ new bootstrap.Modal(document.getElementById('order-detail-modal')).show(); } + function renderDetailParticipantTags() { + const container = document.getElementById('detail-participants-tags'); + if (!container) return; + container.innerHTML = window.detailOrderParticipants.map(name => ` + + ${name} + + + `).join(''); + } + + function setupDetailParticipantInput() { + const input = document.getElementById('detail-participant-input'); + if (!input) return; + input.onkeypress = function(e) { + if (e.key === 'Enter' && this.value.trim()) { + e.preventDefault(); + const name = this.value.trim(); + if (name && !window.detailOrderParticipants.includes(name)) { + window.detailOrderParticipants.push(name); + renderDetailParticipantTags(); + saveDetailParticipants(); + } + this.value = ''; + } + }; + } + + function removeDetailParticipant(name) { + window.detailOrderParticipants = window.detailOrderParticipants.filter(p => p !== name); + renderDetailParticipantTags(); + saveDetailParticipants(); + } + + async function saveDetailParticipants() { + const modal = document.getElementById('order-detail-modal'); + const orderId = modal.dataset.orderId; + if (!orderId) return; + + try { + const res = await fetch(`${API_BASE}/orders/${orderId}/`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ participants: window.detailOrderParticipants }) + }); + if (res.ok) { + const updatedOrder = await res.json(); + // Update local orders cache + const idx = orders.findIndex(o => o.id == orderId); + if (idx !== -1) { + orders[idx] = updatedOrder; + } + updateDetailSummary(); + // Refresh ranking if exists + if (window.participantRanking) { + calculateParticipantRanking(); + if (document.getElementById('participant-ranking-modal').classList.contains('show')) { + showParticipantRanking(); + } + } + } + } catch (err) { + console.error('Failed to save participants:', err); + } + } + + function updateDetailSummary() { + const modal = document.getElementById('order-detail-modal'); + const orderId = modal.dataset.orderId; + const order = orders.find(o => o.id == orderId); + if (!order) return; + + const dishCount = order.dishes_detail ? order.dishes_detail.length : 0; + const participantCount = window.detailOrderParticipants ? window.detailOrderParticipants.length : 0; + + const summaryEl = document.getElementById('order-detail-summary'); + if (summaryEl) { + summaryEl.textContent = order.status === 'in_progress' + ? `这张备菜清单里有 ${dishCount} 道菜${participantCount > 0 ? `,共 ${participantCount} 位参与人` : ''}。` + : `这次聚会已经完成,留下了 ${dishCount} 道上桌菜品${participantCount > 0 ? `和 ${participantCount} 位参与人` : ''}的记录。`; + } + } + // Render party images function renderOrderImages(order) { @@ -5996,19 +6237,30 @@ const maxCount = Math.max(...ranking.map(r => r.count)); container.innerHTML = ranking.map((item, index) => { const percentage = maxCount > 0 ? (item.count / maxCount) * 100 : 0; - let rankBadge = ''; - if (index === 0) rankBadge = '🥇'; - else if (index === 1) rankBadge = '🥈'; - else if (index === 2) rankBadge = '🥉'; + let rankClass = ''; + let gradient = ''; + if (index === 0) { + rankClass = 'rank-gold'; + gradient = 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #d97706 100%)'; + } else if (index === 1) { + rankClass = 'rank-silver'; + gradient = 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'; + } else if (index === 2) { + rankClass = 'rank-bronze'; + gradient = 'linear-gradient(135deg, #d97706 0%, #b45309 100%)'; + } else { + gradient = 'linear-gradient(90deg, #6366f1 0%, #8b5cf6 100%)'; + } return ` -
-
- ${index + 1}. ${item.name} ${rankBadge} - ${item.count}次 +
+
+ ${index + 1} + ${item.name} + ${item.count}次
-
-
+
+
`; diff --git a/zhangmenu/settings.py b/zhangmenu/settings.py index ed8866c..dc07b42 100644 --- a/zhangmenu/settings.py +++ b/zhangmenu/settings.py @@ -28,7 +28,7 @@ DEBUG = os.getenv('DJANGO_DEBUG', 'true').lower() in {'1', 'true', 'yes', 'on'} ALLOWED_HOSTS = [ host.strip() - for host in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1,testserver').split(',') + for host in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1,testserver,121.41.188.238').split(',') if host.strip() ]