feat: 聚会人员动态管理与排行榜美化
- 支持聚会创建后随时增减参与人员(进行中/已完成均可) - 美化参与次数排行榜界面,采用渐变进度条风格 - 新上传图片改用相对路径,避免端口变化导致图片无法加载 - 修复外网访问的 ALLOWED_HOSTS 配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0a50c09dba
commit
f1994c869d
+272
-20
@@ -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 @@
|
||||
<i class="bi bi-people-fill"></i>
|
||||
参与人员
|
||||
</h3>
|
||||
<div id="order-detail-participants"></div>
|
||||
<div class="participant-edit-wrapper">
|
||||
<div class="input-group participant-edit-input">
|
||||
<input type="text" class="form-control" id="detail-participant-input" placeholder="输入人名后回车添加">
|
||||
</div>
|
||||
<div id="detail-participants-tags" class="d-flex flex-wrap gap-2 mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-detail-section" id="order-ingredients-section">
|
||||
<h3 class="order-detail-section-title">
|
||||
@@ -5767,15 +5927,13 @@
|
||||
ingContainer.innerHTML = '<p class="detail-empty-note">暂时还没有汇总出食材,等选好菜后这里会自动整理。</p>';
|
||||
}
|
||||
|
||||
// Render participants
|
||||
const participantsContainer = document.getElementById('order-detail-participants');
|
||||
if (order.participants && order.participants.length > 0) {
|
||||
participantsContainer.innerHTML = `<div class="participants-wrap">${order.participants.map(name => `
|
||||
<span class="participant-chip">${name}</span>
|
||||
`).join('')}</div>`;
|
||||
} else {
|
||||
participantsContainer.innerHTML = '<p class="detail-empty-note">这次还没记录参与人。</p>';
|
||||
}
|
||||
// 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 => `
|
||||
<span class="participant-tag">
|
||||
${name}
|
||||
<button type="button" class="participant-remove-btn" onclick="removeDetailParticipant('${name}')">×</button>
|
||||
</span>
|
||||
`).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 = '<span class="badge bg-warning text-dark ms-2">🥇</span>';
|
||||
else if (index === 1) rankBadge = '<span class="badge bg-secondary ms-2">🥈</span>';
|
||||
else if (index === 2) rankBadge = '<span class="badge bg-brown ms-2">🥉</span>';
|
||||
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 `
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-medium">${index + 1}. ${item.name} ${rankBadge}</span>
|
||||
<span class="text-muted">${item.count}次</span>
|
||||
<div class="ranking-item ${rankClass}">
|
||||
<div class="ranking-header">
|
||||
<span class="ranking-rank">${index + 1}</span>
|
||||
<span class="ranking-name">${item.name}</span>
|
||||
<span class="ranking-count">${item.count}次</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 12px; border-radius: 6px;">
|
||||
<div class="progress-bar" style="width: ${percentage}%; background: linear-gradient(90deg, #4f46e5, #7c3aed); border-radius: 6px;"></div>
|
||||
<div class="ranking-progress">
|
||||
<div class="ranking-bar" style="width: ${percentage}%; background: ${gradient};"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user