From 2c4dc19654cc0861f277cfa0acee356bdedc181f Mon Sep 17 00:00:00 2001 From: kicer Date: Tue, 28 Jul 2026 19:51:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=85=8D=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/public/css/style.css | 9 +- server/public/index.html | 2 +- server/public/js/purchases.js | 231 ++++++++++++++++------------------ server/public/js/ws.js | 4 +- 4 files changed, 120 insertions(+), 126 deletions(-) diff --git a/server/public/css/style.css b/server/public/css/style.css index 49b6664..582baed 100644 --- a/server/public/css/style.css +++ b/server/public/css/style.css @@ -24,7 +24,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- .edit-room-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 2px; } .edit-room-btn svg { width: 16px; height: 16px; stroke: #888; fill: none; stroke-width: 2; } -.summary-card { background: #f0f7ff; margin: 8px; border-radius: 12px; padding: 14px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; } +.summary-card { background: linear-gradient(135deg, #e8f0fe 0%, #d4e4fc 100%); margin: 8px; border-radius: 12px; padding: 14px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border: 1px solid #b8d4f8; } .summary-card h4 { font-size: 16px; font-weight: 500; } .summary-card .summary-preview { font-size: 14px; color: #555; } @@ -70,8 +70,11 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- .purchase-item .item-name { font-size: 16px; font-weight: 500; } .purchase-item .item-amount { font-size: 16px; font-weight: 600; } .purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; flex-wrap: wrap; gap: 4px; } -.status-badge { background: #fef3c7; padding: 1px 6px; border-radius: 8px; font-size: 12px; } -.status-badge.done { background: #d1fae5; } +.status-badge { padding: 1px 6px; border-radius: 8px; font-size: 12px; color: #fff; } +.status-badge.待付款 { background: #f59e0b; } +.status-badge.已付款 { background: #3b82f6; } +.status-badge.已收货 { background: #8b5cf6; } +.status-badge.已完成 { background: #10b981; } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 100; } .modal { background: #fff; width: 90%; max-width: 400px; max-height: 80vh; border-radius: 12px; overflow: auto; padding: 16px; position: relative; } diff --git a/server/public/index.html b/server/public/index.html index b69ff4d..215d065 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -57,7 +57,7 @@ - 采购清单 + 采购清单 diff --git a/server/public/js/purchases.js b/server/public/js/purchases.js index db33948..8b3e5d4 100644 --- a/server/public/js/purchases.js +++ b/server/public/js/purchases.js @@ -1,5 +1,5 @@ // 采购相关功能 -let editPid = null; // 当前正在编辑的采购ID +let editPid = null; async function loadPurchases() { if (!Store.currentRoom) return; @@ -11,7 +11,7 @@ async function loadPurchases() { function renderPurchasePanel(items) { const container = document.getElementById('purchase-panel-body'); - if (items.length === 0) { container.innerHTML = '

暂无采购记录

'; return; } + if (items.length === 0) { container.innerHTML = '

\u6682\u65e0\u91c7\u8d2d\u8bb0\u5f55

'; return; } const groups = {}; items.forEach(item => { const month = formatMonth(item.created_at); @@ -22,19 +22,20 @@ function renderPurchasePanel(items) { Object.keys(groups).sort().reverse().forEach(month => { const monthItems = groups[month]; const monthTotal = monthItems.reduce((s, i) => s + (i.amount||0), 0); - html += '
' + month + '¥' + monthTotal + ''; + html += '
' + month + '\uffe5' + monthTotal + ''; monthItems.forEach(item => { - const timeShort = item.created_at.split(' ')[1]?.substring(0,5) || ''; - const dateShort = item.created_at.split(' ')[0]?.substring(5) || ''; - html += ` -
-
${escapeHtml(item.item)}${item.amount}
-
- ${dateShort} ${timeShort} - x${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''} - ${item.status} -
-
`; + var t = item.created_at.split(' ')[1]; + var timeShort = t ? t.substring(0,5) : ''; + var datePart = item.created_at.split(' ')[0]; + var dateShort = datePart ? datePart.substring(5) : ''; + html += '
' + + '
' + escapeHtml(item.item) + '\uffe5' + item.amount + '
' + + '
' + + '' + dateShort + ' ' + timeShort + '' + + 'x' + item.quantity + (item.invoice_type !== '\u65e0\u7968' ? ' \u00b7 ' + item.invoice_type : '') + '' + + '' + item.status + '' + + '
' + + '
'; }); html += '
'; }); @@ -42,18 +43,16 @@ function renderPurchasePanel(items) { } function formatMonth(dateStr) { - const clean = dateStr.replace(/-/g, '/'); - const parts = clean.split('/'); + var clean = dateStr.replace(/-/g, '/'); + var parts = clean.split('/'); if (parts.length >= 2) return parts[0] + '\u5e74' + parseInt(parts[1], 10) + '\u6708'; return dateStr.substring(0,7); } -// ============ 可编辑详情弹窗 ============ - async function openDetail(pid) { - const token = Store.getToken(); - const res = await fetch('/api/purchases/' + pid, { headers: { 'Authorization': 'Bearer ' + token } }); - const p = await res.json(); + var token = Store.getToken(); + var res = await fetch('/api/purchases/' + pid, { headers: { 'Authorization': 'Bearer ' + token } }); + var p = await res.json(); editPid = pid; document.getElementById('edit-item').value = p.item || ''; @@ -69,62 +68,58 @@ async function openDetail(pid) { document.getElementById('edit-time').value = p.created_at || ''; document.getElementById('edit-remarks').value = p.remarks || ''; - // 附件 renderEditAttachments(p.attachments || []); - // 操作历史 - const historyHtml = (p.history || []).map(h => - '
  • ' + h.timestamp + ' ' + escapeHtml(h.user) + ': ' + escapeHtml(h.action) + '
  • ' - ).join(''); - document.getElementById('edit-history').innerHTML = historyHtml || '
  • 暂无操作记录
  • '; + var historyHtml = (p.history || []).map(function(h) { + return '
  • ' + h.timestamp + ' ' + escapeHtml(h.user) + ': ' + escapeHtml(h.action) + '
  • '; + }).join(''); + document.getElementById('edit-history').innerHTML = historyHtml || '
  • \u6682\u65e0\u64cd\u4f5c\u8bb0\u5f55
  • '; document.getElementById('edit-history-section').open = false; - // 删除按钮 document.getElementById('delete-purchase-btn').style.display = ''; document.getElementById('delete-purchase-btn').style.marginLeft = ''; - // 折叠所有编辑组 - document.querySelectorAll('.edit-group-detail').forEach(el => el.classList.add('hidden')); - document.querySelectorAll('.edit-group-summary').forEach(el => el.classList.remove('hidden')); + document.querySelectorAll('.edit-group-detail').forEach(function(el) { el.classList.add('hidden'); }); + document.querySelectorAll('.edit-group-summary').forEach(function(el) { el.classList.remove('hidden'); }); document.getElementById('detail-modal').classList.remove('hidden'); } function renderEditAttachments(attachments) { - const container = document.getElementById('edit-attachments'); + var container = document.getElementById('edit-attachments'); if (!attachments || attachments.length === 0) { - container.innerHTML = '暂无附件'; + container.innerHTML = '\u6682\u65e0\u9644\u4ef6'; return; } - container.innerHTML = attachments.map(a => - '
    ' + + container.innerHTML = attachments.map(function(a) { + return '
    ' + '' + '' + - '
    ' - ).join(''); + '
    '; + }).join(''); } function updateAmountSummary() { - const q = parseFloat(document.getElementById('edit-quantity').value) || 0; - const up = parseFloat(document.getElementById('edit-unit-price').value) || 0; - const f = parseFloat(document.getElementById('edit-freight').value) || 0; - const total = (q * up + f).toFixed(2); + var q = parseFloat(document.getElementById('edit-quantity').value) || 0; + var up = parseFloat(document.getElementById('edit-unit-price').value) || 0; + var f = parseFloat(document.getElementById('edit-freight').value) || 0; + var total = (q * up + f).toFixed(2); document.getElementById('amount-summary').textContent = total + ', \u6570\u91cf' + q + ', \u5355\u4ef7' + up + ', \u90ae\u8d39' + f; } function updateStatusSummary() { - const pm = document.getElementById('edit-payment-method').value; - const iv = document.getElementById('edit-invoice-type').value; - const st = document.getElementById('edit-status').value; + var pm = document.getElementById('edit-payment-method').value; + var iv = document.getElementById('edit-invoice-type').value; + var st = document.getElementById('edit-status').value; document.getElementById('status-summary').textContent = st + ', ' + pm + ', ' + iv; } function toggleEditGroup(groupId) { - const detail = document.getElementById(groupId); - const summary = detail.previousElementSibling; - const isOpen = !detail.classList.contains('hidden'); + var detail = document.getElementById(groupId); + var summary = detail.previousElementSibling; + var isOpen = !detail.classList.contains('hidden'); if (isOpen) { detail.classList.add('hidden'); summary.classList.remove('hidden'); @@ -136,8 +131,8 @@ function toggleEditGroup(groupId) { async function savePurchase() { if (!editPid) return; - const token = Store.getToken(); - const body = { + var token = Store.getToken(); + var body = { item: document.getElementById('edit-item').value, quantity: parseFloat(document.getElementById('edit-quantity').value) || 0, unit_price: parseFloat(document.getElementById('edit-unit-price').value) || 0, @@ -149,12 +144,12 @@ async function savePurchase() { remarks: document.getElementById('edit-remarks').value, created_at: document.getElementById('edit-time').value }; - const res = await fetch('/api/purchases/' + editPid, { + var res = await fetch('/api/purchases/' + editPid, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); - const result = await res.json(); + var result = await res.json(); if (result.success) { closeDetailModal(); loadPurchases(); @@ -166,8 +161,8 @@ async function savePurchase() { async function deletePurchaseDetail() { if (!editPid) return; if (!confirm('\u786e\u5b9a\u8981\u5220\u9664\u6b64\u91c7\u8d2d\u8bb0\u5f55\u5417\uff1f')) return; - const token = Store.getToken(); - const res = await fetch('/api/purchases/' + editPid, { + var token = Store.getToken(); + var res = await fetch('/api/purchases/' + editPid, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); @@ -175,38 +170,37 @@ async function deletePurchaseDetail() { closeDetailModal(); loadPurchases(); } else { - const err = await res.json(); + var err = await res.json(); alert('\u5220\u9664\u5931\u8d25\uff1a' + (err.error || '\u672a\u77e5\u9519\u8bef')); } } async function deleteAttachment(event, encodedPath) { event.stopPropagation(); - const filePath = decodeURIComponent(encodedPath); + var filePath = decodeURIComponent(encodedPath); if (!confirm('\u786e\u5b9a\u5220\u9664\u6b64\u9644\u4ef6\u5417\uff1f')) return; - const token = Store.getToken(); - const res = await fetch('/api/purchases/' + editPid + '/attachments', { + var token = Store.getToken(); + var res = await fetch('/api/purchases/' + editPid + '/attachments', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ file_path: filePath }) }); - if (res.ok) { - openDetail(editPid); - } + if (res.ok) openDetail(editPid); } async function handleDetailFileUpload(event) { - const files = event.target.files; + var files = event.target.files; if (!files.length) return; - const token = Store.getToken(); - const paths = []; + var token = Store.getToken(); + var paths = []; try { - for (let file of files) { - let blob = file; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var blob = file; if (file.size > 1 * 1024 * 1024) blob = await compressImage(file); - const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg'); - const res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); - const data = await res.json(); + var formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg'); + var res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); + var data = await res.json(); if (data.path) paths.push(data.path); } if (paths.length) { @@ -225,44 +219,41 @@ function closeDetailModal() { editPid = null; } -// ============ 采购页面输入区 ============ - async function handlePurchaseFileSelect(event) { - const files = event.target.files; + var files = event.target.files; if (!files.length) return; - const token = Store.getToken(); + var token = Store.getToken(); try { - for (let file of files) { - let blob = file; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var blob = file; if (file.size > 1 * 1024 * 1024) blob = await compressImage(file); - const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg'); - const res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); - const data = await res.json(); + var formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg'); + var res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); + var data = await res.json(); if (data.path) Store.pendingUploads.push(data.path); } } catch(e) { alert('\u56fe\u7247\u4e0a\u4f20\u5931\u8d25'); } } function sendFromPurchase() { - const input = document.getElementById('purchase-msg-input'); - const text = input.value.trim(); + var input = document.getElementById('purchase-msg-input'); + var text = input.value.trim(); if (!text && Store.pendingUploads.length === 0) return; Store.pendingPurchaseText = text; input.value = ''; openChatFromPurchase(); } -// ============ 汇总 & 群聊管理 ============ - async function openSummary() { - const token = Store.getToken(); - const res = await fetch('/api/summary', { headers: { 'Authorization': 'Bearer ' + token } }); - const summary = await res.json(); - let html = ''; + var token = Store.getToken(); + var res = await fetch('/api/summary', { headers: { 'Authorization': 'Bearer ' + token } }); + var summary = await res.json(); + var html = ''; if (summary.length === 0) html = '

    \u6682\u65e0\u91c7\u8d2d\u8bb0\u5f55

    '; else { - summary.forEach(s => { - html += '

    ' + escapeHtml(s.room_name) + '

    ' + s.purchase_count + ' \u6761\uff0c\u5408\u8ba1 ' + s.total_amount + '

    '; + summary.forEach(function(s) { + html += '

    ' + escapeHtml(s.room_name) + '

    ' + s.purchase_count + ' \u6761\uff0c\u5408\u8ba1 \uffe5' + s.total_amount + '

    '; }); } document.getElementById('summary-title').textContent = '\u91c7\u8d2d\u6c47\u603b'; @@ -274,26 +265,26 @@ function closeSummaryModal() { document.getElementById('summary-modal').classLis function openCreateRoom() { document.getElementById('create-modal').classList.remove('hidden'); - const isAdmin = localStorage.getItem('isAdmin') === 'true'; + var isAdmin = localStorage.getItem('isAdmin') === 'true'; document.getElementById('white-list').style.display = isAdmin ? '' : 'none'; } function closeCreateModal() { document.getElementById('create-modal').classList.add('hidden'); } async function confirmCreateRoom() { - const name = document.getElementById('new-room-name').value.trim(); - if (!name) return alert('请输入名称'); - const isAdmin = localStorage.getItem('isAdmin') === 'true'; - const whiteList = isAdmin ? document.getElementById('white-list').value.trim() : localStorage.getItem('currentUser'); - const token = Store.getToken(); - await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) }); + var name = document.getElementById('new-room-name').value.trim(); + if (!name) return alert('\u8bf7\u8f93\u5165\u540d\u79f0'); + var isAdmin = localStorage.getItem('isAdmin') === 'true'; + var whiteList = isAdmin ? document.getElementById('white-list').value.trim() : localStorage.getItem('currentUser'); + var token = Store.getToken(); + await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, whiteList: whiteList }) }); closeCreateModal(); loadRooms(); } function openManageRoom(roomId) { - const room = Store.rooms.find(r => r.id === roomId); + var room = Store.rooms.find(function(r) { return r.id === roomId; }); if (!room) return; - const isAdmin = localStorage.getItem('isAdmin') === 'true'; - document.getElementById('manage-title').textContent = '编辑群聊'; + var isAdmin = localStorage.getItem('isAdmin') === 'true'; + document.getElementById('manage-title').textContent = '\u7f16\u8f91\u7fa4\u804a'; document.getElementById('manage-room-name').value = room.name; document.getElementById('manage-white-list').value = room.white_list || ''; document.getElementById('manage-white-list').style.display = isAdmin ? '' : 'none'; @@ -304,43 +295,41 @@ function openManageRoom(roomId) { function closeManageModal() { document.getElementById('manage-modal').classList.add('hidden'); } async function saveRoom() { - const roomId = document.getElementById('manage-modal').dataset.roomId; - const name = document.getElementById('manage-room-name').value.trim(); - const isAdmin = localStorage.getItem('isAdmin') === 'true'; - const whiteList = isAdmin ? document.getElementById('manage-white-list').value.trim() : undefined; - if (!name) return alert('请输入群聊名称'); - const token = Store.getToken(); - const res = await fetch('/api/rooms/' + roomId, { + var roomId = document.getElementById('manage-modal').dataset.roomId; + var name = document.getElementById('manage-room-name').value.trim(); + var isAdmin = localStorage.getItem('isAdmin') === 'true'; + var whiteList = isAdmin ? document.getElementById('manage-white-list').value.trim() : undefined; + if (!name) return alert('\u8bf7\u8f93\u5165\u7fa4\u804a\u540d\u79f0'); + var token = Store.getToken(); + var res = await fetch('/api/rooms/' + roomId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, whiteList }) + body: JSON.stringify({ name: name, whiteList: whiteList }) }); if (res.ok) { closeManageModal(); loadRooms(); } - else { const err = await res.json(); alert(err.error || '更新失败'); } + else { var err = await res.json(); alert(err.error || '\u66f4\u65b0\u5931\u8d25'); } } async function deleteRoom() { if (!confirm('\u786e\u5b9a\u8981\u5220\u9664\u8be5\u7fa4\u804a\u5417\uff1f\u6240\u6709\u6d88\u606f\u548c\u91c7\u8d2d\u6570\u636e\u5c06\u88ab\u6c38\u4e45\u5220\u9664\u3002')) return; - const roomId = document.getElementById('manage-modal').dataset.roomId; - const token = Store.getToken(); - const res = await fetch('/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); + var roomId = document.getElementById('manage-modal').dataset.roomId; + var token = Store.getToken(); + var res = await fetch('/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); if (res.ok) { closeManageModal(); loadRooms(); if (Store.currentRoom === roomId) showList(); - } else { const err = await res.json(); alert(err.error || '\u5220\u9664\u5931\u8d25'); } + } else { var err = await res.json(); alert(err.error || '\u5220\u9664\u5931\u8d25'); } } -// ============ 滑动手势 ============ - function initSwipeGestures() { - const purchasePage = document.getElementById('purchase-page'); - const chatPage = document.getElementById('chat-page'); - let startX = 0; + var purchasePage = document.getElementById('purchase-page'); + var chatPage = document.getElementById('chat-page'); + var startX = 0; - purchasePage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true }); - purchasePage.addEventListener('touchend', (e) => { + purchasePage.addEventListener('touchstart', function(e) { startX = e.touches[0].clientX; }, { passive: true }); + purchasePage.addEventListener('touchend', function(e) { if (!startX) return; - const diffX = e.changedTouches[0].clientX - startX; + var diffX = e.changedTouches[0].clientX - startX; if (Math.abs(diffX) > 50) { if (diffX > 30) showList(); else if (diffX < -30) openChatFromPurchase(); @@ -348,10 +337,10 @@ function initSwipeGestures() { startX = 0; }, { passive: true }); - chatPage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true }); - chatPage.addEventListener('touchend', (e) => { + chatPage.addEventListener('touchstart', function(e) { startX = e.touches[0].clientX; }, { passive: true }); + chatPage.addEventListener('touchend', function(e) { if (!startX) return; - const diffX = e.changedTouches[0].clientX - startX; + var diffX = e.changedTouches[0].clientX - startX; if (Math.abs(diffX) > 50) backToPurchase(); startX = 0; }, { passive: true }); diff --git a/server/public/js/ws.js b/server/public/js/ws.js index d5c931f..047318b 100644 --- a/server/public/js/ws.js +++ b/server/public/js/ws.js @@ -107,6 +107,8 @@ async function openPurchasePage(roomId) { document.getElementById('chat-page').classList.add('hidden'); document.getElementById('purchase-page').classList.remove('hidden'); document.getElementById('purchase-panel-body').innerHTML = '

    加载中...

    '; + var room = Store.rooms.find(function(r) { return r.id === roomId; }); + document.getElementById('purchase-page-title').textContent = (room ? room.name : '') + '(清单)'; loadPurchases(); } @@ -129,7 +131,7 @@ async function openChat(roomId, fromPurchase = false) { document.getElementById('purchase-page').classList.add('hidden'); document.getElementById('chat-page').classList.remove('hidden'); const room = Store.rooms.find(r => r.id === roomId); - document.getElementById('current-chat-name').textContent = room?.name || ''; + document.getElementById('current-chat-name').textContent = (room ? room.name : '') + '(聊天)'; const token = Store.getToken(); const res = await fetch('/api/rooms/' + roomId + '/messages', { headers: { 'Authorization': 'Bearer ' + token } }); const msgs = await res.json();