普通用户也支持新建群聊

This commit is contained in:
2026-07-28 19:02:23 +08:00
parent d4a512bc6e
commit 2e9da6c1ac
6 changed files with 103 additions and 174 deletions

View File

@@ -4,13 +4,12 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
.hidden { display: none !important; }
.header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; min-height: 48px; }
.header-left { display: flex; align-items: center; gap: 8px; }
.header-left { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.header-left h2 { font-size: 18px; font-weight: 500; }
.icon-btn { background: none; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: 50%; }
.icon-btn svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
#create-room-btn { visibility: hidden; }
#create-room-btn.visible { visibility: visible; }
.chat-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
.chat-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; cursor: pointer; }
@@ -58,6 +57,10 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
.purchase-panel-body { flex: 1; overflow-y: auto; padding: 12px; }
.purchase-month-group { margin-bottom: 20px; }
.purchase-month-group summary { list-style: none; cursor: pointer; }
.purchase-month-group summary::-webkit-details-marker { display: none; }
.purchase-month-group summary::after { content: '▼'; font-size: 11px; margin-left: 8px; color: #888; }
.purchase-month-group[open] summary::after { content: '▲'; }
.purchase-month-header { background: #f5f5f5; font-size: 16px; font-weight: 600; padding: 10px 8px; border-radius: 4px; display: flex; justify-content: space-between; }
.purchase-item { padding: 12px 8px; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
.purchase-item:active { background: #f9f9f9; }

View File

@@ -20,7 +20,7 @@
<input type="password" id="login-pass" placeholder="密码">
<button class="primary-btn" onclick="login()" style="width:100%;">登录</button>
<p id="login-error" style="color:red; margin-top:8px;"></p>
<div class="version">v2.7</div>
<div class="version" id="version-text"></div>
</div>
</div>
@@ -29,19 +29,16 @@
<!-- 群聊列表页 -->
<div id="list-page" style="display:flex; flex-direction:column; height:100%;">
<div class="header">
<div class="header-left">
<div class="header-left" onclick="Store.logout()">
<svg width="24" height="24" viewBox="0 0 24 24" stroke="#fff" fill="none" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="3"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="12" y2="17"/>
</svg>
<h2>小财记账</h2>
</div>
<div style="display:flex; gap:2px;">
<button class="icon-btn" id="create-room-btn" onclick="openCreateRoom()" title="新建群聊">
<button class="icon-btn visible" id="create-room-btn" onclick="openCreateRoom()" title="新建群聊">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
<button class="icon-btn" id="logout-btn" onclick="Store.logout()" title="注销登录">
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</div>
</div>
<div class="chat-list" id="chat-list-container"></div>
@@ -238,6 +235,12 @@
<script src="/js/purchases.js"></script>
<script src="/js/auth.js"></script>
<script>
// 版本号
(function() {
var d = new Date();
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
document.getElementById('version-text').textContent = 'v2.8-' + (d.getFullYear() % 100) + pad(d.getMonth() + 1) + pad(d.getDate());
})();
document.addEventListener('input', function(e) {
if (e.target.id === 'msg-input' || e.target.id === 'purchase-msg-input') {
e.target.style.height = 'auto';

View File

@@ -3,16 +3,10 @@ async function init() {
const token = Store.getToken();
if (token) {
try {
const res = await fetch('/api/user', { headers: { 'Authorization': `Bearer ${token}` } });
const res = await fetch('/api/user', { headers: { 'Authorization': 'Bearer ' + token } });
if (res.ok) {
const user = await res.json();
document.getElementById('login-page').classList.add('hidden');
document.getElementById('main-page').classList.remove('hidden');
if (localStorage.getItem('isAdmin') === 'true') {
document.getElementById('create-room-btn').classList.add('visible');
} else {
document.getElementById('create-room-btn').classList.remove('visible');
}
initWebSocket();
loadRooms();
updateSummaryPreview();
@@ -38,17 +32,12 @@ async function login() {
Store.saveAuth(data);
document.getElementById('login-page').classList.add('hidden');
document.getElementById('main-page').classList.remove('hidden');
if (data.isAdmin) {
document.getElementById('create-room-btn').classList.add('visible');
} else {
document.getElementById('create-room-btn').classList.remove('visible');
}
initWebSocket();
loadRooms();
updateSummaryPreview();
initSwipeGestures();
} else {
const err = await res.json();
document.getElementById('login-error').textContent = err.error || '登录失败';
document.getElementById('login-error').textContent = err.error || '\u767b\u5f55\u5931\u8d25';
}
}

View File

@@ -22,7 +22,7 @@ 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 += '<div class="purchase-month-group"><div class="purchase-month-header"><span>' + month + '</span><span>' + monthTotal + '</span></div>';
html += '<details class="purchase-month-group" open><summary class="purchase-month-header"><span>' + month + '</span><span>' + monthTotal + '</span></summary>';
monthItems.forEach(item => {
const timeShort = item.created_at.split(' ')[1]?.substring(0,5) || '';
const dateShort = item.created_at.split(' ')[0]?.substring(5) || '';
@@ -31,12 +31,12 @@ function renderPurchasePanel(items) {
<div class="item-main"><span class="item-name">${escapeHtml(item.item)}</span><span class="item-amount">${item.amount}</span></div>
<div class="item-meta">
<span>${dateShort} ${timeShort}</span>
<span>${item.quantity} ${item.invoice_type !== '无票' ? '\u00b7 ' + item.invoice_type : ''}</span>
<span>x${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
</div>
</div>`;
});
html += '</div>';
html += '</details>';
});
container.innerHTML = html;
}
@@ -279,12 +279,17 @@ async function openSummary() {
function closeSummaryModal() { document.getElementById('summary-modal').classList.add('hidden'); }
function openCreateRoom() { document.getElementById('create-modal').classList.remove('hidden'); }
function openCreateRoom() {
document.getElementById('create-modal').classList.remove('hidden');
const 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('\u8bf7\u8f93\u5165\u540d\u79f0');
const whiteList = document.getElementById('white-list').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 }) });
closeCreateModal();
@@ -294,9 +299,11 @@ async function confirmCreateRoom() {
function openManageRoom(roomId) {
const room = Store.rooms.find(r => r.id === roomId);
if (!room) return;
document.getElementById('manage-title').textContent = '\u7f16\u8f91\u7fa4\u804a';
const isAdmin = localStorage.getItem('isAdmin') === 'true';
document.getElementById('manage-title').textContent = '编辑群聊';
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';
document.getElementById('manage-modal').dataset.roomId = roomId;
document.getElementById('manage-modal').classList.remove('hidden');
}
@@ -306,15 +313,16 @@ function closeManageModal() { document.getElementById('manage-modal').classList.
async function saveRoom() {
const roomId = document.getElementById('manage-modal').dataset.roomId;
const name = document.getElementById('manage-room-name').value.trim();
const whiteList = document.getElementById('manage-white-list').value.trim();
if (!name) return alert('\u8bf7\u8f93\u5165\u7fa4\u804a\u540d\u79f0');
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, {
method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, whiteList })
});
if (res.ok) { closeManageModal(); loadRooms(); }
else { const err = await res.json(); alert(err.error || '\u66f4\u65b0\u5931\u8d25'); }
else { const err = await res.json(); alert(err.error || '更新失败'); }
}
async function deleteRoom() {