采购详情支持手动修改
This commit is contained in:
@@ -2,63 +2,73 @@
|
||||
function buildSystemPrompt(username, isAdmin) {
|
||||
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
|
||||
const role = `你是智能财务助手"小财"。当前服务器时间:${now}。结合对话历史理解用户意图,只返回JSON。`;
|
||||
const role = '你是智能财务助手"小财"。当前服务器时间:' + now + '。结合对话历史理解用户意图,只返回JSON。';
|
||||
|
||||
const intent = `
|
||||
意图分类:
|
||||
- 采购/付款/发票相关:action="purchase",提取字段:purchase_item, quantity(默认1), unit_price(默认0), freight(默认0), payment_method(淘宝默认支付宝), invoice_type(默认无票), status, applicant, remarks, created_time
|
||||
- 查询汇总:action="query"
|
||||
- 聊天:action="chat",reply简短回复
|
||||
- 删除指定物品:action="delete",delete_item为物品名
|
||||
- 清空所有采购数据:action="clear_all"
|
||||
- 忽略(仅当完全无关):action="ignore"`;
|
||||
const intent = [
|
||||
'意图分类:',
|
||||
'- 采购/付款/发票相关(新建):action="purchase",提取字段:purchase_item, quantity(默认1), unit_price(默认0), freight(默认0), payment_method(淘宝默认支付宝), invoice_type(默认无票), status, applicant, remarks, created_time',
|
||||
'- 查询汇总:action="query"',
|
||||
'- 聊天:action="chat",reply简短回复',
|
||||
'- 删除指定物品:action="delete",delete_item为物品名',
|
||||
'- 清空所有采购数据:action="clear_all"',
|
||||
'- 忽略(仅当完全无关):action="ignore"'
|
||||
].join('\n');
|
||||
|
||||
const permission = `
|
||||
⚠️ 权限规则:
|
||||
- 当前用户${username},管理员状态:${isAdmin}。仅管理员可执行删除/清空操作。
|
||||
- 如果用户要求删除或清空,且 isAdmin 为 false,你必须返回 action="chat" 并说明"仅管理员可操作"。
|
||||
- 如果 isAdmin 为 true,正常返回 delete 或 clear_all。`;
|
||||
const permission = [
|
||||
'⚠️ 权限规则:',
|
||||
'- 当前用户' + username + ',管理员状态:' + isAdmin + '。仅管理员可执行删除/清空操作。',
|
||||
'- 如果用户要求删除或清空,且 isAdmin 为 false,你必须返回 action="chat" 并说明"仅管理员可操作"。',
|
||||
'- 如果 isAdmin 为 true,正常返回 delete 或 clear_all。'
|
||||
].join('\n');
|
||||
|
||||
const deleteRules = `
|
||||
⚠️ 删除规则:
|
||||
- 特殊批量删除:delete_item="__AMOUNT_ZERO__"(金额为0)、"__NULL_NAME__"(空名称)
|
||||
- 精确删除:用户指定数量/金额时同时返回 quantity/amount 字段,只匹配一条
|
||||
- 不要返回 action="ask" 处理删除请求`;
|
||||
const deleteRules = [
|
||||
'⚠️ 删除规则:',
|
||||
'- 特殊批量删除:delete_item="__AMOUNT_ZERO__"(金额为0)、"__NULL_NAME__"(空名称)',
|
||||
'- 精确删除:用户指定数量/金额时同时返回 quantity/amount 字段,只匹配一条',
|
||||
'- 不要返回 action="ask" 处理删除请求'
|
||||
].join('\n');
|
||||
|
||||
const attachmentRules = `
|
||||
⚠️ 附件规则:
|
||||
- 图片+物品名("这是XX的图片")→ 只返回 purchase_item,绝对不要 is_new/quantity/unit_price
|
||||
- 引用历史图片("上面/前面/刚刚那张""存入附件/添加到附件/加到XX的附件/关联到")→ use_recent_image: true,**同时必须提供** purchase_item
|
||||
- 纯图片+物品名不是新建采购,系统自动关联附件`;
|
||||
const attachmentRules = [
|
||||
'⚠️ 附件规则:',
|
||||
'- 图片+物品名("这是XX的图片")→ 只返回 purchase_item,绝对不要 is_new/quantity/unit_price',
|
||||
'- 引用历史图片("上面/前面/刚刚那张""存入附件/添加到附件/加到XX的附件/关联到")→ use_recent_image: true,**同时必须提供** purchase_item',
|
||||
'- 纯图片+物品名不是新建采购,系统自动关联附件'
|
||||
].join('\n');
|
||||
|
||||
const prohibitionRules = `
|
||||
⚠️ 绝对禁止:
|
||||
- 图片+物品名请求 → 只能返回 purchase_item,禁止返回 is_new、quantity、unit_price、amount`;
|
||||
const prohibitionRules = [
|
||||
'⚠️ 绝对禁止:',
|
||||
'- 图片+物品名请求 → 只能返回 purchase_item,禁止返回 is_new、quantity、unit_price、amount'
|
||||
].join('\n');
|
||||
|
||||
const purchaseRules = `
|
||||
⚠️ 采购规则:
|
||||
- 系统会在 Prompt 末尾提供“当前房间采购清单”,你必须根据清单和对话历史判断用户是要更新已有记录还是新建。
|
||||
- **新建采购**(用户首次购买、重复购买、明确说“再买/又/新/另一批/这次”等)→ 必须返回 "is_new": true。
|
||||
- **更新已有记录**(用户要求修改、更正、调整,如“改一下/更新/修改/更正/搞错了/不是XX是YY”)→ 返回 "is_new": false。
|
||||
- **纯图片+物品名**(如“这是xxx的图片”)→ 不是新建采购,不要返回 is_new、quantity、unit_price 等字段,只返回 purchase_item。
|
||||
- 如果无法明确判断新建/更新,优先按新建处理 → 返回 "is_new": true。
|
||||
- 采购状态(status)只能从以下 4 个值中选择,绝不允许自创:
|
||||
* "待付款" — 采购已记录,等待付款
|
||||
* "已付款" — 已完成支付,等待收货
|
||||
* "已收货" — 已收到货物
|
||||
* "已完成" — 发票已收,全部结束
|
||||
- 状态映射:
|
||||
* "付了/已付/付款了" → "已付款"
|
||||
* "到了/收到货了" → "已收货"
|
||||
* "发票到/全搞定/完结" → "已完成"
|
||||
* 新建不指定 → "待付款"
|
||||
- 只有明确消费意图(购买/采购/下单/付款/买了/花了)才返回 action="purchase"
|
||||
- 纯陈述 → action="ignore"
|
||||
- 信息不完整 → action="ask" 追问
|
||||
- amount 由系统自动计算(quantity × unit_price + freight),AI 无需提供
|
||||
- 无法确定物品名 → action="ask" 追问`;
|
||||
const modifyGuide = [
|
||||
'⚠️ 修改引导规则(极其重要!):',
|
||||
'- 聊天窗口仅用于**新建**采购记录和查询汇总。',
|
||||
'- 如果用户要求**修改/更正/调整/改一下/更新/纠正**已有采购记录 → 你必须返回 action="chat",reply引导用户去采购清单页面手动修改。',
|
||||
' 示例回复:"请在采购清单中找到对应记录,点击进入详情页面直接修改。修改后点击保存即可。"',
|
||||
'- 如果用户要求修改,你绝不要尝试去匹配或更新已有记录,只能引导用户手动操作。'
|
||||
].join('\n');
|
||||
|
||||
return [role, intent, permission, deleteRules, attachmentRules, prohibitionRules, purchaseRules].join('\n');
|
||||
const purchaseRules = [
|
||||
'⚠️ 采购规则:',
|
||||
'- 聊天默认为**新建**采购记录。只要用户提到购买/采购/下单/付款/买了/花了,就创建新记录。',
|
||||
'- 采购状态(status)只能从以下 4 个值中选择,绝不允许自创:',
|
||||
' * "待付款" — 采购已记录,等待付款',
|
||||
' * "已付款" — 已完成支付,等待收货',
|
||||
' * "已收货" — 已收到货物',
|
||||
' * "已完成" — 发票已收,全部结束',
|
||||
'- 状态映射:',
|
||||
' * "付了/已付/付款了" → "已付款"',
|
||||
' * "到了/收到货了" → "已收货"',
|
||||
' * "发票到/全搞定/完结" → "已完成"',
|
||||
' * 新建不指定 → "待付款"',
|
||||
'- 只有明确消费意图(购买/采购/下单/付款/买了/花了)才返回 action="purchase"',
|
||||
'- 纯陈述 → action="ignore"',
|
||||
'- 信息不完整 → action="ask" 追问',
|
||||
'- amount 由系统自动计算(quantity × unit_price + freight),AI 无需提供',
|
||||
'- 无法确定物品名 → action="ask" 追问'
|
||||
].join('\n');
|
||||
|
||||
return [role, intent, permission, deleteRules, attachmentRules, prohibitionRules, modifyGuide, purchaseRules].join('\n');
|
||||
}
|
||||
|
||||
module.exports = { buildSystemPrompt };
|
||||
|
||||
@@ -108,6 +108,43 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||
|
||||
.detail-row { margin-bottom: 8px; }
|
||||
|
||||
/* 可编辑详情弹窗 */
|
||||
.detail-modal { max-height: 90vh; overflow-y: auto; padding: 0; }
|
||||
.detail-modal .modal-close-btn { display: none; }
|
||||
.detail-header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 8px 16px; display: flex; align-items: center; gap: 8px; border-radius: 12px 12px 0 0; margin: -16px -16px 0 -16px; }
|
||||
.detail-header .icon-btn { flex-shrink: 0; }
|
||||
.detail-header .icon-btn svg { stroke: #fff; }
|
||||
.detail-title-input { flex: 1; font-size: 18px; font-weight: 600; color: #fff; background: transparent; border: none; outline: none; padding: 4px 0; min-width: 0; }
|
||||
.detail-title-input::placeholder { color: rgba(255,255,255,0.6); }
|
||||
.detail-title-input:focus { border-bottom: 2px solid rgba(255,255,255,0.5); }
|
||||
.edit-form { padding: 12px 0 0 0; }
|
||||
.edit-row { display: flex; align-items: center; margin-bottom: 6px; gap: 8px; }
|
||||
.edit-row label { min-width: 48px; font-size: 13px; color: #777; text-align: right; }
|
||||
.edit-row input, .edit-row select, .edit-row textarea { flex: 1; padding: 6px 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; outline: none; }
|
||||
.edit-row input:focus, .edit-row select:focus, .edit-row textarea:focus { border-color: #3b82f6; }
|
||||
.edit-row select { appearance: auto; }
|
||||
.edit-row textarea { resize: vertical; min-height: 40px; }
|
||||
.amount-row { font-weight: 600; }
|
||||
.amount-row label { color: #333; }
|
||||
.edit-amount { flex: 1; font-size: 17px; color: #e53e3e; font-weight: 700; }
|
||||
.edit-section { margin-top: 8px; border-top: 1px solid #eee; padding-top: 8px; }
|
||||
.edit-section-header { font-size: 13px; font-weight: 600; color: #555; cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 2px 0; }
|
||||
.edit-section-header svg { width: 16px; height: 16px; stroke: #888; fill: none; stroke-width: 2; }
|
||||
.history-summary { list-style: none; }
|
||||
.history-summary::-webkit-details-marker { display: none; }
|
||||
.history-summary::after { content: '展开'; font-size: 12px; color: #3b82f6; font-weight: 400; }
|
||||
details[open] .history-summary::after { content: '收起'; }
|
||||
.attach-gallery { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||||
.attach-thumb-wrap { position: relative; display: inline-block; }
|
||||
.attach-thumb { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; border: 1px solid #eee; }
|
||||
.attach-del-btn { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; border-radius: 50%; background: #e53e3e; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }
|
||||
.attach-del-btn svg { width: 11px; height: 11px; stroke: #fff; fill: none; stroke-width: 2; }
|
||||
.attach-add-btn { cursor: pointer; display: flex; align-items: center; }
|
||||
.attach-add-btn svg { width: 18px; height: 18px; stroke: #3b82f6; fill: none; stroke-width: 2; }
|
||||
.edit-buttons { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.save-btn { flex: 1; background: #3b82f6; color: #fff; border: none; padding: 8px 0; border-radius: 6px; font-size: 15px; cursor: pointer; font-weight: 500; }
|
||||
.danger-btn { background: transparent; color: #e53e3e; border: 1px solid #e53e3e; padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.white-list-tag {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
|
||||
@@ -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.6</div>
|
||||
<div class="version">v2.7</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,17 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="purchase-panel-body" id="purchase-panel-body"></div>
|
||||
<!-- 新建采购输入栏 -->
|
||||
<div class="input-area">
|
||||
<label class="file-upload-btn" title="上传图片">
|
||||
<input type="file" id="purchase-file-input" accept="image/*" multiple style="display:none;" onchange="handlePurchaseFileSelect(event)">
|
||||
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
||||
</label>
|
||||
<textarea id="purchase-msg-input" placeholder="输入采购信息..." rows="1"></textarea>
|
||||
<button class="send-btn" onclick="sendFromPurchase()">
|
||||
<svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
@@ -75,6 +86,9 @@
|
||||
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
<span class="chat-title" id="current-chat-name"></span>
|
||||
<button class="icon-btn" onclick="backToPurchase()" title="关闭聊天">
|
||||
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="messages" id="messages-container"></div>
|
||||
<div class="input-area">
|
||||
@@ -116,13 +130,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="detail-modal" class="modal-overlay hidden">
|
||||
<!-- 采购汇总弹窗 -->
|
||||
<div id="summary-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<button class="modal-close-btn" onclick="closeDetailModal()">
|
||||
<button class="modal-close-btn" onclick="closeSummaryModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
<h3 id="detail-title"></h3>
|
||||
<div id="detail-content"></div>
|
||||
<h3 id="summary-title"></h3>
|
||||
<div id="summary-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 采购详情编辑弹窗 -->
|
||||
<div id="detail-modal" class="modal-overlay hidden">
|
||||
<div class="modal detail-modal">
|
||||
<div class="detail-header">
|
||||
<input type="text" id="edit-item" class="detail-title-input" placeholder="物品名称">
|
||||
<button class="icon-btn" onclick="closeDetailModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edit-form">
|
||||
<div class="edit-row">
|
||||
<label>数量</label><input type="number" id="edit-quantity" inputmode="decimal" min="0" step="1">
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>单价</label><input type="number" id="edit-unit-price" inputmode="decimal" min="0" step="0.01">
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>邮费</label><input type="number" id="edit-freight" inputmode="decimal" min="0" step="0.01">
|
||||
</div>
|
||||
<div class="edit-row amount-row">
|
||||
<label>金额</label><span class="edit-amount" id="edit-amount">0</span>
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>付款</label>
|
||||
<select id="edit-payment-method">
|
||||
<option>未指定</option><option>支付宝</option><option>微信</option><option>银行卡</option><option>现金</option><option>对公转账</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>发票</label>
|
||||
<select id="edit-invoice-type">
|
||||
<option>无票</option><option>普票</option><option>专票</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>状态</label>
|
||||
<select id="edit-status">
|
||||
<option>待付款</option><option>已付款</option><option>已收货</option><option>已完成</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>申请人</label><input type="text" id="edit-applicant">
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>时间</label><input type="text" id="edit-time" placeholder="yyyy/MM/dd HH:mm:ss">
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>备注</label><textarea id="edit-remarks" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 附件 -->
|
||||
<div class="edit-section">
|
||||
<div class="edit-section-header">
|
||||
<span>附件</span>
|
||||
<label class="attach-add-btn" title="添加附件">
|
||||
<input type="file" id="detail-file-input" accept="image/*" multiple style="display:none;" onchange="handleDetailFileUpload(event)">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
<div class="attach-gallery" id="edit-attachments"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作历史(折叠) -->
|
||||
<details class="edit-section" id="edit-history-section">
|
||||
<summary class="edit-section-header history-summary"><span>操作历史</span></summary>
|
||||
<ul class="history-list" id="edit-history"></ul>
|
||||
</details>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="edit-buttons">
|
||||
<button class="save-btn" onclick="savePurchase()">保存</button>
|
||||
<button class="danger-btn" id="delete-purchase-btn" onclick="deletePurchaseDetail()">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,13 +226,20 @@
|
||||
<script src="/js/auth.js"></script>
|
||||
<script>
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.id === 'msg-input') {
|
||||
if (e.target.id === 'msg-input' || e.target.id === 'purchase-msg-input') {
|
||||
e.target.style.height = 'auto';
|
||||
e.target.style.height = e.target.scrollHeight + 'px';
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.id === 'msg-input' && e.key === 'Enter' && !e.ctrlKey) { e.preventDefault(); sendMessage(); }
|
||||
if (e.target.id === 'purchase-msg-input' && e.key === 'Enter' && !e.ctrlKey) { e.preventDefault(); sendFromPurchase(); }
|
||||
});
|
||||
// 实时计算金额
|
||||
document.addEventListener('input', function(e) {
|
||||
if (['edit-quantity','edit-unit-price','edit-freight'].includes(e.target.id)) {
|
||||
updateEditAmount();
|
||||
}
|
||||
});
|
||||
init();
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// 采购相关功能
|
||||
let editPid = null; // 当前正在编辑的采购ID
|
||||
|
||||
async function loadPurchases() {
|
||||
if (!Store.currentRoom) return;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/rooms/' + Store.currentRoom + '/purchases', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/rooms/' + Store.currentRoom + '/purchases', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const items = await res.json();
|
||||
renderPurchasePanel(items);
|
||||
}
|
||||
@@ -20,16 +22,16 @@ 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 += '<div class="purchase-month-group"><div class="purchase-month-header"><span>' + month + '</span><span>' + monthTotal + '</span></div>';
|
||||
monthItems.forEach(item => {
|
||||
const timeShort = item.created_at.split(' ')[1]?.substring(0,5) || '';
|
||||
const dateShort = item.created_at.split(' ')[0]?.substring(5) || '';
|
||||
html += `
|
||||
<div class="purchase-item" onclick="openDetail('${item.id}')">
|
||||
<div class="item-main"><span class="item-name">${escapeHtml(item.item)}</span><span class="item-amount">¥${item.amount}</span></div>
|
||||
<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 !== '无票' ? '· ' + item.invoice_type : ''}</span>
|
||||
<span>${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
|
||||
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -42,54 +44,210 @@ function renderPurchasePanel(items) {
|
||||
function formatMonth(dateStr) {
|
||||
const clean = dateStr.replace(/-/g, '/');
|
||||
const parts = clean.split('/');
|
||||
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
||||
if (parts.length >= 2) return parts[0] + '年' + parseInt(parts[1], 10) + '月';
|
||||
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 res = await fetch('/api/purchases/' + pid, { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const p = await res.json();
|
||||
document.getElementById('detail-title').textContent = p.item;
|
||||
let attachHtml = '';
|
||||
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}" ondblclick="onImgThumbDblClick(this)">`).join('') + '</div>';
|
||||
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
|
||||
document.getElementById('detail-content').innerHTML = `
|
||||
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
|
||||
<div class="detail-row"><strong>单价:</strong>¥${p.unit_price}</div>
|
||||
<div class="detail-row"><strong>邮费:</strong>¥${p.freight}</div>
|
||||
<div class="detail-row"><strong>总金额:</strong>¥${p.amount}</div>
|
||||
<div class="detail-row"><strong>付款方式:</strong>${p.payment_method || '未指定'}</div>
|
||||
<div class="detail-row"><strong>发票:</strong>${p.invoice_type || '无票'}</div>
|
||||
<div class="detail-row"><strong>状态:</strong>${p.status}</div>
|
||||
<div class="detail-row"><strong>申请人:</strong>${p.applicant}</div>
|
||||
<div class="detail-row"><strong>采购时间:</strong>${p.created_at}</div>
|
||||
<div class="detail-row"><strong>备注:</strong>${p.remarks || '无'}</div>
|
||||
${attachHtml}
|
||||
<h4 style="margin-top:12px;">操作历史</h4>
|
||||
<ul class="history-list">${historyHtml}</ul>`;
|
||||
editPid = pid;
|
||||
|
||||
document.getElementById('edit-item').value = p.item || '';
|
||||
document.getElementById('edit-quantity').value = p.quantity || 0;
|
||||
document.getElementById('edit-unit-price').value = p.unit_price || 0;
|
||||
document.getElementById('edit-freight').value = p.freight || 0;
|
||||
updateEditAmount();
|
||||
document.getElementById('edit-payment-method').value = p.payment_method || '未指定';
|
||||
document.getElementById('edit-invoice-type').value = p.invoice_type || '无票';
|
||||
document.getElementById('edit-status').value = p.status || '待付款';
|
||||
document.getElementById('edit-applicant').value = p.applicant || '';
|
||||
document.getElementById('edit-time').value = p.created_at || '';
|
||||
document.getElementById('edit-remarks').value = p.remarks || '';
|
||||
|
||||
// 附件
|
||||
renderEditAttachments(p.attachments || []);
|
||||
|
||||
// 操作历史
|
||||
const historyHtml = (p.history || []).map(h =>
|
||||
'<li><span class="history-time">' + h.timestamp + '</span> ' + escapeHtml(h.user) + ': ' + escapeHtml(h.action) + '</li>'
|
||||
).join('');
|
||||
document.getElementById('edit-history').innerHTML = historyHtml || '<li style="color:#888;">暂无操作记录</li>';
|
||||
document.getElementById('edit-history-section').open = false;
|
||||
|
||||
// 删除按钮仅管理员可见
|
||||
const isAdmin = localStorage.getItem('isAdmin') === 'true';
|
||||
document.getElementById('delete-purchase-btn').style.display = isAdmin ? '' : 'none';
|
||||
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); }
|
||||
function renderEditAttachments(attachments) {
|
||||
const container = document.getElementById('edit-attachments');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<span style="color:#888;font-size:14px;">暂无附件</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(a =>
|
||||
'<div class="attach-thumb-wrap">' +
|
||||
'<img class="attach-thumb" src="' + a.file_path + '" ondblclick="onImgThumbDblClick(this)">' +
|
||||
'<button class="attach-del-btn" onclick="deleteAttachment(event, \'' + encodeURIComponent(a.file_path) + '\')">' +
|
||||
'<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function updateEditAmount() {
|
||||
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;
|
||||
document.getElementById('edit-amount').textContent = (q * up + f).toFixed(2);
|
||||
}
|
||||
|
||||
async function savePurchase() {
|
||||
if (!editPid) return;
|
||||
const token = Store.getToken();
|
||||
const 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,
|
||||
freight: parseFloat(document.getElementById('edit-freight').value) || 0,
|
||||
payment_method: document.getElementById('edit-payment-method').value,
|
||||
invoice_type: document.getElementById('edit-invoice-type').value,
|
||||
status: document.getElementById('edit-status').value,
|
||||
applicant: document.getElementById('edit-applicant').value,
|
||||
remarks: document.getElementById('edit-remarks').value,
|
||||
created_at: document.getElementById('edit-time').value
|
||||
};
|
||||
const 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();
|
||||
if (result.success) {
|
||||
closeDetailModal();
|
||||
loadPurchases();
|
||||
} else {
|
||||
alert('保存失败:' + (result.error || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePurchaseDetail() {
|
||||
if (!editPid) return;
|
||||
if (!confirm('确定要删除此采购记录吗?')) return;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/purchases/' + editPid, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (res.ok) {
|
||||
closeDetailModal();
|
||||
loadPurchases();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('删除失败:' + (err.error || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAttachment(event, encodedPath) {
|
||||
event.stopPropagation();
|
||||
const filePath = decodeURIComponent(encodedPath);
|
||||
if (!confirm('确定删除此附件吗?')) return;
|
||||
const token = Store.getToken();
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDetailFileUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
const token = Store.getToken();
|
||||
const paths = [];
|
||||
try {
|
||||
for (let file of files) {
|
||||
let 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();
|
||||
if (data.path) paths.push(data.path);
|
||||
}
|
||||
if (paths.length) {
|
||||
await fetch('/api/purchases/' + editPid + '/attachments', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_paths: paths })
|
||||
});
|
||||
openDetail(editPid);
|
||||
}
|
||||
} catch(e) { alert('上传失败,请重试'); }
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detail-modal').classList.add('hidden');
|
||||
editPid = null;
|
||||
}
|
||||
|
||||
// ============ 采购页面输入区 ============
|
||||
|
||||
async function handlePurchaseFileSelect(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
const token = Store.getToken();
|
||||
try {
|
||||
for (let file of files) {
|
||||
let 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();
|
||||
if (data.path) Store.pendingUploads.push(data.path);
|
||||
}
|
||||
} catch(e) { alert('图片上传失败'); }
|
||||
}
|
||||
|
||||
function sendFromPurchase() {
|
||||
const input = document.getElementById('purchase-msg-input');
|
||||
const 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 res = await fetch('/api/summary', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const summary = await res.json();
|
||||
let html = '';
|
||||
if (summary.length === 0) html = '<p>暂无采购记录</p>';
|
||||
else {
|
||||
summary.forEach(s => {
|
||||
html += `<div class="summary-card" onclick="event.stopPropagation(); closeDetailModal(); openPurchasePage('${s.room_id}')" style="margin:8px 0;"><h4>${escapeHtml(s.room_name)}</h4><p>${s.purchase_count} 条,合计 ¥${s.total_amount}</p></div>`;
|
||||
html += '<div class="summary-card" onclick="event.stopPropagation(); closeSummaryModal(); openPurchasePage(\'' + s.room_id + '\')" style="margin:8px 0;"><h4>' + escapeHtml(s.room_name) + '</h4><p>' + s.purchase_count + ' 条,合计 ' + s.total_amount + '</p></div>';
|
||||
});
|
||||
}
|
||||
document.getElementById('detail-title').textContent = '采购汇总';
|
||||
document.getElementById('detail-content').innerHTML = html;
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
document.getElementById('summary-title').textContent = '采购汇总';
|
||||
document.getElementById('summary-content').innerHTML = html;
|
||||
document.getElementById('summary-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 群聊管理
|
||||
function closeSummaryModal() { document.getElementById('summary-modal').classList.add('hidden'); }
|
||||
|
||||
function openCreateRoom() { document.getElementById('create-modal').classList.remove('hidden'); }
|
||||
function closeCreateModal() { document.getElementById('create-modal').classList.add('hidden'); }
|
||||
async function confirmCreateRoom() {
|
||||
@@ -97,7 +255,7 @@ async function confirmCreateRoom() {
|
||||
if (!name) return alert('请输入名称');
|
||||
const whiteList = document.getElementById('white-list').value.trim();
|
||||
const token = Store.getToken();
|
||||
await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) });
|
||||
await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) });
|
||||
closeCreateModal();
|
||||
loadRooms();
|
||||
}
|
||||
@@ -121,7 +279,7 @@ async function saveRoom() {
|
||||
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' },
|
||||
method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, whiteList })
|
||||
});
|
||||
if (res.ok) { closeManageModal(); loadRooms(); }
|
||||
@@ -132,7 +290,7 @@ async function deleteRoom() {
|
||||
if (!confirm('确定要删除该群聊吗?所有消息和采购数据将被永久删除。')) 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}` } });
|
||||
const res = await fetch('/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
|
||||
if (res.ok) {
|
||||
closeManageModal();
|
||||
loadRooms();
|
||||
@@ -140,17 +298,31 @@ async function deleteRoom() {
|
||||
} else { const err = await res.json(); alert(err.error || '删除失败'); }
|
||||
}
|
||||
|
||||
// 滑动手势
|
||||
// ============ 滑动手势 ============
|
||||
|
||||
function initSwipeGestures() {
|
||||
const purchasePage = document.getElementById('purchase-page');
|
||||
const chatPage = document.getElementById('chat-page');
|
||||
let startX = 0;
|
||||
|
||||
// 采购页:右滑回列表,左滑进聊天
|
||||
purchasePage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true });
|
||||
purchasePage.addEventListener('touchend', (e) => {
|
||||
if (!startX) return;
|
||||
const diffX = e.changedTouches[0].clientX - startX;
|
||||
if (Math.abs(diffX) > 50) {
|
||||
if (diffX > 30) showList();
|
||||
else if (diffX < -30) openChatFromPurchase();
|
||||
}
|
||||
startX = 0;
|
||||
}, { passive: true });
|
||||
|
||||
// 聊天页:左右滑回采购页
|
||||
chatPage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true });
|
||||
chatPage.addEventListener('touchend', (e) => {
|
||||
if (!startX) return;
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const diffX = endX - startX;
|
||||
if (diffX > 50) backToPurchase();
|
||||
const diffX = e.changedTouches[0].clientX - startX;
|
||||
if (Math.abs(diffX) > 50) backToPurchase();
|
||||
startX = 0;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const Store = {
|
||||
currentRoom: null,
|
||||
rooms: [],
|
||||
pendingUploads: [],
|
||||
pendingPurchaseText: '',
|
||||
messageCache: [],
|
||||
tempBubbleTimer: null,
|
||||
wsReconnectTimer: null,
|
||||
|
||||
@@ -109,7 +109,7 @@ async function openPurchasePage(roomId) {
|
||||
|
||||
async function openChatFromPurchase() {
|
||||
if (!Store.currentRoom) return;
|
||||
openChat(Store.currentRoom);
|
||||
await openChat(Store.currentRoom, true);
|
||||
}
|
||||
|
||||
function backToPurchase() {
|
||||
@@ -119,7 +119,7 @@ function backToPurchase() {
|
||||
loadPurchases();
|
||||
}
|
||||
|
||||
async function openChat(roomId) {
|
||||
async function openChat(roomId, fromPurchase = false) {
|
||||
Store.currentRoom = roomId;
|
||||
clearTempBubble();
|
||||
document.getElementById('list-page').classList.add('hidden');
|
||||
@@ -128,10 +128,20 @@ async function openChat(roomId) {
|
||||
const room = Store.rooms.find(r => r.id === roomId);
|
||||
document.getElementById('current-chat-name').textContent = room?.name || '';
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/rooms/' + roomId + '/messages', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/rooms/' + roomId + '/messages', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const msgs = await res.json();
|
||||
renderAllMessages(msgs);
|
||||
if (Store.ws && Store.ws.readyState === WebSocket.OPEN) Store.ws.send(JSON.stringify({ type: 'join', roomId }));
|
||||
|
||||
// 从采购页跳转过来时,自动发送缓存的文本
|
||||
if (fromPurchase && Store.pendingPurchaseText) {
|
||||
const text = Store.pendingPurchaseText;
|
||||
Store.pendingPurchaseText = '';
|
||||
document.getElementById('msg-input').value = text;
|
||||
document.getElementById('msg-input').style.height = 'auto';
|
||||
document.getElementById('msg-input').style.height = document.getElementById('msg-input').scrollHeight + 'px';
|
||||
setTimeout(() => sendMessage(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
function showList() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// 采购相关路由
|
||||
const express = require('express');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { authMiddleware, adminOnly } = require('../middleware/auth');
|
||||
const { broadcastToRoom } = require('../ws');
|
||||
const db = require('../db');
|
||||
const { timestamp } = require('../utils');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -18,6 +20,90 @@ router.get('/purchases/:id', authMiddleware, (req, res) => {
|
||||
res.json({ ...pur, history, attachments });
|
||||
});
|
||||
|
||||
// 更新采购记录
|
||||
router.put('/purchases/:id', authMiddleware, (req, res) => {
|
||||
const old = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
||||
if (!old) return res.status(404).json({ error: '未找到' });
|
||||
|
||||
const now = timestamp();
|
||||
const username = req.user.username;
|
||||
const { item, quantity, unit_price, freight, payment_method, invoice_type, status, applicant, remarks, created_at } = req.body;
|
||||
|
||||
const newVals = {
|
||||
item: item !== undefined ? String(item) : old.item,
|
||||
quantity: quantity !== undefined ? Number(quantity) : old.quantity,
|
||||
unit_price: unit_price !== undefined ? Number(unit_price) : old.unit_price,
|
||||
freight: freight !== undefined ? Number(freight) : old.freight,
|
||||
payment_method: payment_method !== undefined ? String(payment_method) : old.payment_method,
|
||||
invoice_type: invoice_type !== undefined ? String(invoice_type) : old.invoice_type,
|
||||
status: status !== undefined ? String(status) : old.status,
|
||||
applicant: applicant !== undefined ? String(applicant) : old.applicant,
|
||||
remarks: remarks !== undefined ? String(remarks) : old.remarks,
|
||||
created_at: created_at !== undefined ? String(created_at) : old.created_at
|
||||
};
|
||||
newVals.amount = newVals.quantity * newVals.unit_price + newVals.freight;
|
||||
|
||||
const fieldLabels = {
|
||||
item: '物品', quantity: '数量', unit_price: '单价', freight: '邮费', amount: '金额',
|
||||
payment_method: '付款方式', invoice_type: '发票', status: '状态', applicant: '申请人', remarks: '备注', created_at: '时间'
|
||||
};
|
||||
|
||||
const changes = [];
|
||||
for (const [field, label] of Object.entries(fieldLabels)) {
|
||||
if (String(newVals[field]) !== String(old[field])) {
|
||||
changes.push(label + ': ' + (old[field] || '空') + ' -> ' + newVals[field]);
|
||||
db.prepare('UPDATE purchases SET ' + field + ' = ? WHERE id = ?').run(newVals[field], old.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
db.prepare('UPDATE purchases SET updated_at = ? WHERE id = ?').run(now, old.id);
|
||||
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(old.id, '更新:' + changes.join(';'), username, now);
|
||||
broadcastToRoom(old.room_id, { type: 'purchase_updated' });
|
||||
res.json({ success: true, changes });
|
||||
} else {
|
||||
res.json({ success: true, changes: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除采购记录(管理员)
|
||||
router.delete('/purchases/:id', authMiddleware, adminOnly, (req, res) => {
|
||||
const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
||||
if (!pur) return res.status(404).json({ error: '未找到' });
|
||||
db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?').run(pur.id);
|
||||
db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?').run(pur.id);
|
||||
db.prepare('DELETE FROM purchases WHERE id = ?').run(pur.id);
|
||||
broadcastToRoom(pur.room_id, { type: 'purchase_updated' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// 管理附件
|
||||
router.post('/purchases/:id/attachments', authMiddleware, (req, res) => {
|
||||
const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
||||
if (!pur) return res.status(404).json({ error: '未找到' });
|
||||
const now = timestamp();
|
||||
const username = req.user.username;
|
||||
const { file_paths } = req.body;
|
||||
if (!file_paths || !file_paths.length) return res.status(400).json({ error: '缺少文件路径' });
|
||||
const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)');
|
||||
file_paths.forEach(fp => insertAttach.run(pur.id, fp, username, now));
|
||||
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(pur.id, '添加附件:' + file_paths.length + ' 个', username, now);
|
||||
broadcastToRoom(pur.room_id, { type: 'purchase_updated' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.delete('/purchases/:id/attachments', authMiddleware, (req, res) => {
|
||||
const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
||||
if (!pur) return res.status(404).json({ error: '未找到' });
|
||||
const username = req.user.username;
|
||||
const { file_path } = req.body;
|
||||
if (!file_path) return res.status(400).json({ error: '缺少文件路径' });
|
||||
db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ? AND file_path = ?').run(pur.id, file_path);
|
||||
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(pur.id, '删除附件:' + file_path.split('/').pop(), username, timestamp());
|
||||
broadcastToRoom(pur.room_id, { type: 'purchase_updated' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get('/summary', authMiddleware, (req, res) => {
|
||||
const username = req.user.username;
|
||||
const isAdmin = req.user.isAdmin;
|
||||
@@ -25,7 +111,7 @@ router.get('/summary', authMiddleware, (req, res) => {
|
||||
if (isAdmin) {
|
||||
rooms = db.prepare('SELECT id, name FROM rooms').all();
|
||||
} else {
|
||||
rooms = db.prepare("SELECT id, name FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all(`%,${username},%`);
|
||||
rooms = db.prepare("SELECT id, name FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all('%,' + username + ',%');
|
||||
}
|
||||
const summary = rooms.map(room => {
|
||||
const stats = db.prepare('SELECT COUNT(*) as count, SUM(amount) as total FROM purchases WHERE room_id = ?').get(room.id);
|
||||
@@ -38,10 +124,10 @@ router.get('/rooms/:roomId/purchases/export', authMiddleware, (req, res) => {
|
||||
const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ?').all(req.params.roomId);
|
||||
let csv = '时间,事项,数量,单价,金额,邮费,付款方式,发票类型,状态,申请人,备注\n';
|
||||
purchases.forEach(p => {
|
||||
csv += `${p.created_at},"${p.item||''}",${p.quantity},${p.unit_price},${p.amount},${p.freight},"${p.payment_method||''}","${p.invoice_type||''}","${p.status}","${p.applicant}","${p.remarks||''}"\n`;
|
||||
csv += p.created_at + ',"' + (p.item||'') + '",' + p.quantity + ',' + p.unit_price + ',' + p.amount + ',' + p.freight + ',"' + (p.payment_method||'') + '","' + (p.invoice_type||'') + '","' + p.status + '","' + p.applicant + '","' + (p.remarks||'') + '"\n';
|
||||
});
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="purchases-${req.params.roomId}.csv"`);
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="purchases-' + req.params.roomId + '.csv"');
|
||||
res.send('\uFEFF' + csv);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user