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 += '';
+ html += '';
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();