-
+ if (options.zIndex) overlay.style.zIndex = options.zIndex;
+ const modalClass = options.modalClass || 'modal';
+ const headerHtml = options.hideHeader ? '' : ``;
+ const footerSection = footerHtml ? `` : '';
+ overlay.innerHTML = `
+ ${headerHtml}
${bodyHtml}
+ ${footerSection}
`;
overlay.addEventListener('click', e => { if (e.target === overlay) Modal.close(); });
document.body.appendChild(overlay);
},
close() {
- const o = document.querySelector('.modal-overlay');
- if (!o) return;
- o.classList.add('closing');
- setTimeout(() => o.remove(), 200);
+ const overlays = document.querySelectorAll('.modal-overlay');
+ if (!overlays.length) return;
+ const top = overlays[overlays.length - 1];
+ top.classList.add('closing');
+ setTimeout(() => top.remove(), 200);
+ },
+ closeAll() {
+ const overlays = document.querySelectorAll('.modal-overlay');
+ if (!overlays.length) return;
+ overlays.forEach(o => {
+ o.classList.add('closing');
+ setTimeout(() => o.remove(), 200);
+ });
}
};
-/* ========== 消费数据 ========== */
+/* ========== 金额格式化 ========== */
+function formatMoney(n) {
+ return '¥' + Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+/* ========== Toast 提示 ========== */
+function showToast(msg, type) {
+ const colors = { success:'#52C41A', danger:'#FF4D4F', warning:'#FAAD14', info:'#1890FF' };
+ const c = colors[type] || colors.info;
+ const t = document.createElement('div');
+ t.style.cssText = `position:fixed;top:80px;left:50%;transform:translateX(-50%);background:${c};color:#fff;padding:10px 20px;border-radius:4px;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:2000;animation:modalSlideIn .25s ease;`;
+ t.textContent = msg;
+ document.body.appendChild(t);
+ setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity .2s'; setTimeout(() => t.remove(), 200); }, 2200);
+}
+
+/* ========== 消费数据 ==========
+ * refundable: 是否可退款(消费成功且未完全退款)
+ * refundedAmount: 已退金额(元),默认 0
+ * refundMethod: 最近一次退款方式 'full'|'partial'|null
+ * amountNum: 消费金额(元,数值型,用于退款计算)
+ */
const consumeData = {
- 'TX-20260421-001': { txId:'TX-20260421-001', user:'张伟(EMP001)', dept:'总部机关/研发部', source:'点餐', mealTime:'午餐', type:'消费', pay:'余额支付', amount:'¥22.00', actualPay:'¥22.00', time:'2026-04-21 11:02:35', status:'成功', score:'88分', reconcile:'已对账', abnormal:'—', preBal:'¥256.50', postBal:'¥234.50', orderId:'ORD-20260421-001', mealId:'MEAL-20260421-001', operator:'系统自动', remark:'—',
+ 'TX-20260421-001': { txId:'TX-20260421-001', user:'张伟(EMP001)', dept:'总部机关/研发部', source:'点餐', mealTime:'午餐', type:'消费', pay:'余额支付', amount:'¥22.00', amountNum:22.00, actualPay:'¥22.00', time:'2026-04-21 11:02:35', status:'成功', score:'88分', reconcile:'已对账', abnormal:'—', preBal:'¥256.50', postBal:'¥234.50', orderId:'ORD-20260421-001', mealId:'MEAL-20260421-001', operator:'系统自动', remark:'—', refundable:true, refundedAmount:0, refundMethod:null,
dishes:[
{ code:'DISH-001', name:'宫保鸡丁', weight:'0.18kg', price:'¥14/份', total:'¥14.00' },
{ code:'DISH-012', name:'米饭', weight:'0.25kg', price:'¥2/份', total:'¥2.00' },
{ code:'DISH-008', name:'紫菜蛋花汤', weight:'0.30kg', price:'¥6/份', total:'¥6.00' },
]},
- 'TX-20260421-002': { txId:'TX-20260421-002', user:'李娜(EMP002)', dept:'总部机关/市场部', source:'点餐', mealTime:'午餐', type:'消费', pay:'余额支付', amount:'¥18.00', actualPay:'¥18.00', time:'2026-04-21 11:08:12', status:'成功', score:'92分', reconcile:'已对账', abnormal:'—', preBal:'¥104.20', postBal:'¥86.20', orderId:'ORD-20260421-002', mealId:'MEAL-20260421-002', operator:'系统自动', remark:'—',
+ 'TX-20260421-002': { txId:'TX-20260421-002', user:'李娜(EMP002)', dept:'总部机关/市场部', source:'点餐', mealTime:'午餐', type:'消费', pay:'余额支付', amount:'¥18.00', amountNum:18.00, actualPay:'¥18.00', time:'2026-04-21 11:08:12', status:'成功', score:'92分', reconcile:'已对账', abnormal:'—', preBal:'¥104.20', postBal:'¥86.20', orderId:'ORD-20260421-002', mealId:'MEAL-20260421-002', operator:'系统自动', remark:'—', refundable:true, refundedAmount:0, refundMethod:null,
dishes:[
{ code:'DISH-003', name:'清蒸鱼', weight:'0.15kg', price:'¥12/份', total:'¥12.00' },
{ code:'DISH-012', name:'米饭', weight:'0.25kg', price:'¥2/份', total:'¥2.00' },
{ code:'DISH-015', name:'炒时蔬', weight:'0.20kg', price:'¥4/份', total:'¥4.00' },
]},
- 'TX-20260421-003': { txId:'TX-20260421-003', user:'王磊(EMP003)', dept:'总部机关/运营部', source:'餐线', mealTime:'午餐', type:'消费', pay:'微信支付', amount:'¥28.00', actualPay:'¥25.20', time:'2026-04-21 11:15:48', status:'成功', score:'68分', reconcile:'待对账', abnormal:'短时多笔', preBal:'—', postBal:'—', orderId:'ORD-20260421-003', mealId:'MEAL-20260421-003', operator:'系统自动', remark:'—',
+ 'TX-20260421-003': { txId:'TX-20260421-003', user:'王磊(EMP003)', dept:'总部机关/运营部', source:'餐线', mealTime:'午餐', type:'消费', pay:'微信支付', amount:'¥28.00', amountNum:28.00, actualPay:'¥25.20', time:'2026-04-21 11:15:48', status:'成功', score:'68分', reconcile:'待对账', abnormal:'短时多笔', preBal:'—', postBal:'—', orderId:'ORD-20260421-003', mealId:'MEAL-20260421-003', operator:'系统自动', remark:'—', refundable:true, refundedAmount:8.00, refundMethod:'partial',
dishes:[
{ code:'DISH-005', name:'红烧肉', weight:'0.22kg', price:'¥16/份', total:'¥16.00' },
{ code:'DISH-012', name:'米饭', weight:'0.25kg', price:'¥2/份', total:'¥2.00' },
{ code:'DISH-009', name:'番茄炒蛋', weight:'0.18kg', price:'¥8/份', total:'¥8.00' },
{ code:'DISH-012', name:'酸辣汤', weight:'0.30kg', price:'¥2/份', total:'¥2.00' },
]},
- 'TX-20260421-004': { txId:'TX-20260421-004', user:'赵敏(EMP004)', dept:'总部机关/财务部', source:'账户', mealTime:'—', type:'充值', pay:'微信支付', amount:'+¥100.00', actualPay:'¥100.00', time:'2026-04-21 09:30:22', status:'成功', score:'—', reconcile:'已对账', abnormal:'—', preBal:'¥256.80', postBal:'¥356.80', orderId:'—', mealId:'—', operator:'系统自动', remark:'—', dishes:[] },
- 'TX-20260421-005': { txId:'TX-20260421-005', user:'陈刚(EMP005)', dept:'总部机关/人事部', source:'餐线', mealTime:'早餐', type:'消费', pay:'余额支付', amount:'¥12.00', actualPay:'¥12.00', time:'2026-04-21 07:45:10', status:'成功', score:'85分', reconcile:'已对账', abnormal:'—', preBal:'¥154.60', postBal:'¥142.60', orderId:'ORD-20260421-005', mealId:'MEAL-20260421-005', operator:'系统自动', remark:'—',
+ 'TX-20260421-004': { txId:'TX-20260421-004', user:'赵敏(EMP004)', dept:'总部机关/财务部', source:'账户', mealTime:'—', type:'充值', pay:'微信支付', amount:'+¥100.00', amountNum:100.00, actualPay:'¥100.00', time:'2026-04-21 09:30:22', status:'成功', score:'—', reconcile:'已对账', abnormal:'—', preBal:'¥256.80', postBal:'¥356.80', orderId:'—', mealId:'—', operator:'系统自动', remark:'—', refundable:false, refundedAmount:0, refundMethod:null, dishes:[] },
+ 'TX-20260421-005': { txId:'TX-20260421-005', user:'陈刚(EMP005)', dept:'总部机关/人事部', source:'餐线', mealTime:'早餐', type:'消费', pay:'余额支付', amount:'¥12.00', amountNum:12.00, actualPay:'¥12.00', time:'2026-04-21 07:45:10', status:'成功', score:'85分', reconcile:'已对账', abnormal:'—', preBal:'¥154.60', postBal:'¥142.60', orderId:'ORD-20260421-005', mealId:'MEAL-20260421-005', operator:'系统自动', remark:'—', refundable:true, refundedAmount:0, refundMethod:null,
dishes:[
{ code:'DISH-020', name:'小米粥', weight:'0.35kg', price:'¥5/份', total:'¥5.00' },
{ code:'DISH-022', name:'煎蛋', weight:'0.08kg', price:'¥3/份', total:'¥3.00' },
{ code:'DISH-025', name:'凉拌黄瓜', weight:'0.15kg', price:'¥4/份', total:'¥4.00' },
]},
- 'TX-20260421-006': { txId:'TX-20260421-006', user:'刘芳(EMP006)', dept:'总部机关/市场部', source:'点餐', mealTime:'午餐', type:'退款', pay:'原路退回', amount:'+¥18.00', actualPay:'¥18.00', time:'2026-04-21 10:20:05', status:'已退款', score:'76分', reconcile:'差异', abnormal:'退款频繁', preBal:'¥50.40', postBal:'¥68.40', orderId:'ORD-20260421-006', mealId:'MEAL-20260421-006', operator:'客服张丽', remark:'用户申请退款',
+ 'TX-20260421-006': { txId:'TX-20260421-006', user:'刘芳(EMP006)', dept:'总部机关/市场部', source:'点餐', mealTime:'午餐', type:'退款', pay:'原路退回', amount:'+¥18.00', amountNum:18.00, actualPay:'¥18.00', time:'2026-04-21 10:20:05', status:'已退款', score:'76分', reconcile:'差异', abnormal:'退款频繁', preBal:'¥50.40', postBal:'¥68.40', orderId:'ORD-20260421-006', mealId:'MEAL-20260421-006', operator:'客服张丽', remark:'用户申请退款', refundable:false, refundedAmount:0, refundMethod:null,
dishes:[
{ code:'DISH-003', name:'清蒸鱼', weight:'0.15kg', price:'¥12/份', total:'¥12.00' },
{ code:'DISH-015', name:'炒时蔬', weight:'0.20kg', price:'¥4/份', total:'¥4.00' },
@@ -401,6 +500,9 @@ const consumeData = {
]},
};
+/* 退款原因字典 */
+const REFUND_REASONS = ['用户申请','餐品质量问题','操作失误','系统故障','口味不符','其他'];
+
/* ========== 餐品明细弹窗 ========== */
function showDishDetail(txId) {
const d = consumeData[txId];
@@ -419,14 +521,73 @@ function showDishDetail(txId) {
Modal.open('餐品明细 — ' + txId, infoHtml + tableHtml);
}
-/* ========== 交易明细弹窗 ========== */
+/* ========== 交易明细弹窗(双视图:明细视图 / 退款视图) ==========
+ * 明细视图:展示退款信息区 + 详情网格,footer 显示「退款」按钮
+ * 退款视图:展示退款信息区 + 退款表单,footer 显示「返回 / 确认退款」
+ * 仅在消费类型=消费 且 支付状态=成功(或部分退款) 时启用退款视图
+ * 部分退款的订单可继续退款,直到 refundedAmount >= amountNum
+ */
function showTransDetail(txId) {
const d = consumeData[txId];
if (!d) return;
- const statusTag = d.status === '成功' ? '
成功 ' : '
已退款 ';
+
+ // 计算可退款状态
+ const isConsumption = d.type === '消费';
+ const isRefundable = !!d.refundable && isConsumption && d.refundedAmount < d.amountNum;
+ const isFullyRefunded = isConsumption && d.refundedAmount >= d.amountNum && d.amountNum > 0;
+ const isPartiallyRefunded = d.refundedAmount > 0 && d.refundedAmount < d.amountNum;
+
+ // 状态标签
+ let statusTag;
+ if (isFullyRefunded) statusTag = '
已退款 ';
+ else if (isPartiallyRefunded) statusTag = '
部分退款 ';
+ else statusTag = '
成功 ';
+
const typeMap = {'消费':'tag-green','充值':'tag-blue','退款':'tag-red'};
const typeTag = `
${d.type} `;
- const html = `
+
+ // 退款信息区(可退款或有退款记录时显示)
+ let refundInfoHtml = '';
+ if (isConsumption && (d.refundedAmount > 0 || isRefundable)) {
+ const refundableAmount = Math.max(0, d.amountNum - d.refundedAmount).toFixed(2);
+ let methodTag = '';
+ if (d.refundMethod === 'full') methodTag = '
全额退款 ';
+ else if (d.refundMethod === 'partial') methodTag = '
部分退款 ';
+ refundInfoHtml = `
+
+ 原始消费金额
+ ${formatMoney(d.amountNum)}
+
+
+
+ 已退金额
+ ${formatMoney(d.refundedAmount)}
+
+
+
+ 可退金额${methodTag}
+ ${formatMoney(refundableAmount)}
+
+
`;
+ }
+
+ // 完全退款提示
+ if (isFullyRefunded) {
+ refundInfoHtml = `
+
+ 退款状态
+ 已全额退款
+
+
+
+ 累计退款金额
+ ${formatMoney(d.refundedAmount)}
+
+
`;
+ }
+
+ // 明细视图 HTML(始终构建)
+ const detailViewHtml = `${refundInfoHtml}
流水号 ${d.txId}
姓名/编号 ${d.user}
单位/部门 ${d.dept}
@@ -448,7 +609,295 @@ function showTransDetail(txId) {
操作员 ${d.operator}
备注 ${d.remark}
`;
- Modal.open('交易明细 — ' + txId, html);
+
+ // 退款视图 HTML(仅可退款时构建,初始隐藏)
+ const refundViewHtml = isRefundable ? buildRefundView(txId) : '';
+ const refundViewWrapper = isRefundable ? `
${refundViewHtml}
` : '';
+
+ // 整体 body:明细视图 + 退款视图(切换显示)
+ const bodyHtml = `
${detailViewHtml}
${refundViewWrapper}`;
+
+ // 底部按钮区:可退款时显示退款按钮
+ let footerHtml;
+ if (isRefundable) {
+ footerHtml = `
+ `;
+ } else {
+ footerHtml = `
+ `;
+ }
+
+ currentRefundTx = txId;
+ Modal.open('交易明细 — ' + txId, bodyHtml, footerHtml);
+}
+
+/* 构建退款视图 HTML(嵌入交易明细弹窗内,不单独打开弹窗) */
+function buildRefundView(txId) {
+ const d = consumeData[txId];
+ if (!d) return '';
+ const refundableAmount = (d.amountNum - d.refundedAmount).toFixed(2);
+ const reasonOptions = REFUND_REASONS.map(r => `
${r} `).join('');
+
+ return `
`;
+}
+
+/* 视图切换:明细视图 → 退款视图 */
+function switchToRefundView(txId) {
+ const detailView = document.getElementById('detailView');
+ const refundView = document.getElementById('refundView');
+ if (!detailView || !refundView) return;
+
+ detailView.style.display = 'none';
+ refundView.style.display = 'block';
+
+ // 更新 modal-title
+ const titleEl = document.querySelector('.modal-title');
+ if (titleEl) titleEl.textContent = '退款 — ' + txId;
+
+ // 更新 footer
+ const footer = document.querySelector('.modal-footer');
+ if (footer) {
+ footer.innerHTML = `
+ `;
+ }
+
+ // 绑定金额输入实时更新汇总
+ const amountInput = document.getElementById('refundAmount');
+ if (amountInput) amountInput.addEventListener('input', updateRefundSummary);
+}
+
+/* 视图切换:退款视图 → 明细视图 */
+function switchToDetailView(txId) {
+ const detailView = document.getElementById('detailView');
+ const refundView = document.getElementById('refundView');
+ if (!detailView || !refundView) return;
+
+ refundView.style.display = 'none';
+ detailView.style.display = 'block';
+
+ // 更新 modal-title
+ const titleEl = document.querySelector('.modal-title');
+ if (titleEl) titleEl.textContent = '交易明细 — ' + txId;
+
+ // 更新 footer
+ const footer = document.querySelector('.modal-footer');
+ if (footer) {
+ footer.innerHTML = `
+ `;
+ }
+}
+
+/* 切换退款方式(全额/部分) */
+function switchRefundMethod(method) {
+ const tabs = document.querySelectorAll('#refundTabs .refund-tab');
+ tabs.forEach(t => t.classList.toggle('active', t.dataset.method === method));
+
+ const amountInput = document.getElementById('refundAmount');
+ const quickFillBtn = document.getElementById('quickFillBtn');
+ const tip = document.getElementById('amountTip');
+ const d = currentRefundTx ? consumeData[currentRefundTx] : null;
+ const refundableAmount = d ? (d.amountNum - d.refundedAmount).toFixed(2) : '0.00';
+
+ if (method === 'full') {
+ amountInput.value = refundableAmount;
+ amountInput.disabled = true;
+ quickFillBtn.style.display = 'none';
+ tip.textContent = `全额退款模式:自动填入剩余可退金额 ${formatMoney(refundableAmount)}`;
+ } else {
+ amountInput.disabled = false;
+ amountInput.value = '';
+ amountInput.placeholder = `请输入退款金额(最多 ${formatMoney(refundableAmount)})`;
+ quickFillBtn.style.display = 'inline-flex';
+ tip.textContent = `部分退款模式:可输入小于等于 ${formatMoney(refundableAmount)} 的金额,支持多次退款`;
+ }
+ updateRefundSummary();
+}
+
+/* 填入最大可退金额(部分退款快捷按钮) */
+function fillMaxAmount() {
+ const d = consumeData[currentRefundTx];
+ if (!d) return;
+ const refundableAmount = (d.amountNum - d.refundedAmount).toFixed(2);
+ document.getElementById('refundAmount').value = refundableAmount;
+ updateRefundSummary();
+}
+
+/* 更新退款汇总 */
+function updateRefundSummary() {
+ const amountInput = document.getElementById('refundAmount');
+ const summary = document.getElementById('refundSummary');
+ if (!amountInput || !summary) return;
+ const val = parseFloat(amountInput.value) || 0;
+ summary.textContent = formatMoney(val);
+}
+
+/* 当前退款操作对应的流水号 */
+var currentRefundTx = null;
+
+/* 确认退款 → 弹出独立二次确认小弹窗(叠加在退款视图之上) */
+function confirmRefund(txId) {
+ const d = consumeData[txId];
+ if (!d) return;
+ currentRefundTx = txId;
+
+ const activeTab = document.querySelector('#refundTabs .refund-tab.active');
+ const method = activeTab ? activeTab.dataset.method : 'full';
+ const amount = parseFloat(document.getElementById('refundAmount').value) || 0;
+ const reason = document.getElementById('refundReason').value;
+ const remark = document.getElementById('refundRemark').value.trim();
+ const refundableAmount = d.amountNum - d.refundedAmount;
+
+ // 校验
+ if (method === 'partial' && amount <= 0) {
+ showToast('请输入退款金额', 'warning');
+ return;
+ }
+ if (amount > refundableAmount + 0.001) {
+ showToast(`退款金额不能超过可退金额 ${formatMoney(refundableAmount)}`, 'warning');
+ return;
+ }
+ if (method === 'partial' && amount >= refundableAmount - 0.001) {
+ showToast('退款金额等于剩余可退,建议选择全额退款', 'warning');
+ return;
+ }
+
+ // 二次确认弹窗(叠加在退款视图之上,不关闭交易明细弹窗)
+ const methodText = method === 'full' ? '全额退款' : '部分退款';
+ const newRefundedTotal = (d.refundedAmount + amount).toFixed(2);
+ const remaining = (refundableAmount - amount).toFixed(2);
+ const isThisFull = method === 'full' || (method === 'partial' && amount >= refundableAmount - 0.001);
+
+ const bodyHtml = `
+
⚠️
+
确认发起${methodText}?
+
退款后金额将原路返回至用户账户,该操作不可撤销
+
+
流水号 ${d.txId}
+
用户 ${d.user}
+
退款方式 ${methodText}
+
本次退款金额 ${formatMoney(amount)}
+
累计已退金额 ${formatMoney(newRefundedTotal)}
+
剩余可退金额 ${formatMoney(remaining)}
+ ${reason ? `
退款原因 ${reason}
` : ''}
+ ${remark ? `
备注 ${remark}
` : ''}
+
+ ${(!isThisFull && refundableAmount - amount > 0.001) ? `
本次退款后,该订单仍可继续退款 ${formatMoney(remaining)}
` : ''}
+ ${isThisFull ? `
本次退款后,该订单将完成全部退款,不可再发起退款
` : ''}
+
`;
+
+ const footerHtml = `
+ `;
+
+ // 叠加模式:保留交易明细弹窗,二次确认弹窗在其之上
+ Modal.open('确认退款', bodyHtml, footerHtml, { keepExisting: true, zIndex: 1100, modalClass: 'confirm-modal', hideHeader: true });
+}
+
+/* 处理退款提交 → 关闭所有弹窗 + 刷新表格 + Toast */
+function processRefund(txId, method, amount, reason, remark) {
+ const d = consumeData[txId];
+ if (!d) return;
+
+ // 更新数据
+ d.refundedAmount = Number((d.refundedAmount + amount).toFixed(2));
+ const isFullNow = d.refundedAmount >= d.amountNum - 0.001;
+ d.refundMethod = isFullNow ? 'full' : 'partial';
+ if (isFullNow) {
+ d.refundedAmount = d.amountNum; // 修正精度
+ }
+
+ // 关闭所有弹窗(交易明细 + 二次确认)
+ Modal.closeAll();
+
+ // 刷新表格行
+ refreshTableRow(txId);
+
+ // Toast 提示
+ const msg = isFullNow ? '退款成功,已完成全额退款' : `部分退款成功,本次退款 ${formatMoney(amount)}`;
+ setTimeout(() => showToast(msg, 'success'), 220);
+}
+
+/* 刷新表格行(仅更新支付状态列,不再修改消费类型列) */
+function refreshTableRow(txId) {
+ const d = consumeData[txId];
+ if (!d) return;
+ const rows = document.querySelectorAll('tbody tr');
+ rows.forEach(row => {
+ const cells = row.querySelectorAll('td');
+ if (cells.length < 16) return;
+ if (cells[0].textContent.trim() !== txId) return;
+
+ // 仅更新支付状态列(index 9)
+ const statusCell = cells[9];
+ if (d.refundedAmount >= d.amountNum - 0.001) {
+ statusCell.innerHTML = '
已退款 ';
+ } else if (d.refundedAmount > 0) {
+ statusCell.innerHTML = '
部分退款 ';
+ }
+ });
}
/* ========== 级联选择器 ========== */
diff --git a/web-admin/nutrition/doc/CHANGELOG.md b/web-admin/nutrition/doc/CHANGELOG.md
index 5fc9fc6..6900a53 100644
--- a/web-admin/nutrition/doc/CHANGELOG.md
+++ b/web-admin/nutrition/doc/CHANGELOG.md
@@ -1,5 +1,81 @@
# 营养用餐管理端 CHANGELOG
+## [2026-08-11] feat: 采供管理新增「待确认」状态 + 立即确认补全入口
+
+### 变更背景
+后台部分采购单由系统通过食谱/库存预警等自动生成,这类订单生成时缺供应商与食材单价,需要用户二次确认补全后才能流转到正常采购流程。原采供管理(`farm-purchase.html`)状态机只有 5 种(待接单/待发货/待收货/已完成/已取消),无法承接系统自动生成订单的初始态。
+
+### 新增(`farm-purchase.html`)
+- 新增「待确认」订单状态:紫色 tag(`.tag-purple` 配色 `#F9F0FF/#722ED1`)
+- 新增「系统生成」采购单来源:复用紫色 tag,与状态同色强化"自动生成"语义
+- 新增 mock 订单 `PO-20260416-006`(待确认状态,供应商为空,食材单价为 null,总额显示橙色"待核算")
+- 操作列新增「立即确认」按钮(紫色链接 `#722ED1`),仅对"待确认"状态订单显示
+- 编辑弹窗顶部新增橙色补全提示条 `#confirmHintBar`("该采购单由系统自动生成,请补全食材单价和供应商信息后提交"),仅"立即确认"入口显示
+- 新增 `confirmOrder(idx)` 函数:调用 `openEditModal` 并启用提示条 + 修改提交按钮文案为"确认并提交采购单"
+- 新增 `setSubmitBtnText(text)` 工具函数,统一修改弹窗提交按钮文案
+- 详情抽屉 `statusMap` 补充"待确认 → tag-purple"映射
+- 详情抽屉对"待确认"订单:总额显示橙色"待核算" + 追加"提示"项(⚠️ 该订单由系统自动生成,请补全食材单价与供应商信息)
+- 详情抽屉供应商为空时显示"⚠️ 待补充"(红色),联系人/电话默认"—"
+- 详情抽屉食材明细单价为 null 时显示"待补充"(红色)
+- 编辑弹窗单价输入框对 null 值回填为空,边框橙色提醒用户必填
+- 筛选栏"订单状态"下拉新增"待确认"选项(位置:全部之后、待接单之前)
+
+### 调整
+- 统计卡片"采购单总数" 68 → 69,"进行中" 5 → 6(待确认计入进行中)
+- 分页"显示 5 条,共 68 条" → "显示 6 条,共 69 条"
+- `openCreateModal` / `openEditModal` 重置提示条 `display:none` + 调用 `setSubmitBtnText('提交采购单')` 还原按钮文案,避免上一状态残留
+
+### 设计约束
+- **状态流转路径**:系统自动生成 → 待确认(用户立即确认 → 补全供应商与单价 → 提交) → 待接单(进入正常采购流程)
+- **配色梯度**:待确认(紫)与待接单(橙)视觉可区分,避免与已完成(绿)/已取消(红)/待发货(蓝)冲突
+- **补全校验**:单价 null 时输入框为空 + 橙色边框提醒,placeholder="0";本原型仅做视觉提醒,提交前校验由后续阶段补齐
+- **提示条触发**:仅"立即确认"按钮入口启用提示条,新建/普通编辑入口均隐藏
+- **mock 数据约定**:待确认订单 `supplier=''` + `supplierContact=''` + `supplierPhone=''` + 食材 `price=null` + `subtotal='—'`,`total` 字段为字符串"待核算"
+
+---
+
+## [2026-08-11] feat: 消费管理交易明细弹窗新增退款功能(全额/部分退款)
+
+### 变更背景
+原消费管理页(`din-data-consume.html`)的交易明细弹窗仅展示字段信息,缺少退款入口。当出现用户申请退款、餐品质量问题、操作失误等场景时,运营人员需切换到其他模块操作,流程断裂。同时业务方明确要求支持「部分退款可继续退款」的灵活模式(如分批退、试退),仅靠单一全额退款无法满足现场实际需求。
+
+### 新增(`din-data-consume.html`)
+- 交易明细弹窗底部新增「退款」按钮(红色主按钮),仅对消费类型=消费 且 支付状态=成功(或部分退款) 的记录显示
+- 弹窗顶部新增「退款信息区」(黄色警示卡),展示原始消费金额、已退金额、可退金额三栏
+- 已全额退款的记录显示绿色「已全额退款」信息卡,且不展示退款按钮
+- 交易明细弹窗内嵌双视图:明细视图(默认)/退款视图(点击退款切换),弹窗本身不关闭
+- 退款视图支持「全额退款」与「部分退款」两种方式 Tab 切换
+- 退款金额输入框:全额模式下自动填入剩余可退金额(禁用编辑),部分模式下可手动输入 + 「填入最大可退」快捷按钮
+- 退款原因下拉(选填):用户申请/餐品质量问题/操作失误/系统故障/口味不符/其他
+- 备注文本域(选填,最多 200 字)
+- 退款汇总区:实时显示本次退款金额(红色大字)
+- 独立二次确认小弹窗(叠加在退款视图之上):展示流水号/用户/退款方式/本次退款金额/累计已退/剩余可退/原因/备注,并提示「本次退款后是否仍可继续退款」
+- 提交后 Toast 提示 + 自动刷新表格行的支付状态标签(成功→部分退款 / 已退款)
+- 新增 `consumeData[].refundable / refundedAmount / refundMethod / amountNum` 字段;新增 `REFUND_REASONS` 字典常量
+- 新增工具函数:`formatMoney(n)`(金额千分位格式化)、`showToast(msg, type)`(全局 Toast)
+- 演示数据 `TX-20260421-003 王磊` 预置 `refundedAmount: 8.00` + 表格行支付状态初始为「部分退款」,用于演示「部分退款后继续退款」场景
+
+### 调整
+- `Modal.open` 方法签名扩展为 `open(title, bodyHtml, footerHtml, options)`,支持 `keepExisting`/`zIndex`/`modalClass`/`hideHeader` 选项;新增 `close()` 关闭最上层弹窗 + `closeAll()` 关闭所有弹窗(用于叠加二次确认场景)
+- `showTransDetail` 重构:构建 `detailView`(明细视图) + `refundView`(退款视图)双视图,可退款时通过 `switchToRefundView` 切换,不打开新弹窗
+- `openRefundDialog` 重构为 `buildRefundView`(返回 HTML 字符串,嵌入交易明细弹窗内)
+- 新增 `switchToRefundView` / `switchToDetailView` 视图切换函数,切换时联动更新 modal-title 和 modal-footer
+- `confirmRefund` 改造为弹出独立二次确认小弹窗(`keepExisting: true, zIndex: 1100, modalClass: 'confirm-modal'`),叠加在退款视图之上
+- `processRefund` 改造为调用 `Modal.closeAll()` 关闭所有弹窗(交易明细 + 二次确认)
+- `refreshTableRow` 仅更新支付状态列,不再在消费类型列追加「部分退」「已全额退」徽标(状态信息已在支付状态列体现)
+- 表格初始渲染 `TX-20260421-003` 行的支付状态为「部分退款」,消费类型保持纯「消费」标签
+
+### 设计约束
+- **退款方式校验**:部分退款金额等于剩余可退时,系统提示建议改用全额退款(但不阻塞)
+- **可退款判断**:`refundable === true && type === '消费' && refundedAmount < amountNum` 三条件同时满足
+- **不可退款场景**:充值记录、退款记录、已全额退款的消费记录,均隐藏退款按钮并在 footer 显示原因说明
+- **重复退款支持**:部分退款提交后 `refundedAmount` 累加,弹窗重新打开时重新计算可退金额,直至累计退款金额 = 消费金额即转为全额退款
+- **精度处理**:`processRefund` 在 `refundedAmount >= amountNum - 0.001` 时修正为完整 `amountNum`,避免浮点精度误差
+- **双视图切换**:明细视图与退款视图共享同一弹窗,切换时不打开新弹窗,仅切换 modal-body 内的 div display 状态
+- **二次确认叠加**:使用 `keepExisting: true` 保留交易明细弹窗,二次确认弹窗以 `z-index: 1100` 叠加在上方,取消时仅关闭最上层弹窗,保留退款视图状态
+
+---
+
## [2026-08-06] feat: 由食谱快速创建订单支持餐次级人数 + 新增「生成中」状态
### 变更背景
diff --git a/web-admin/nutrition/farm-purchase.html b/web-admin/nutrition/farm-purchase.html
index c9a15b4..675a3a6 100644
--- a/web-admin/nutrition/farm-purchase.html
+++ b/web-admin/nutrition/farm-purchase.html
@@ -82,6 +82,7 @@ tbody tr:hover{background:#F8FBFF;}
.tag-orange{background:#FFF7E6;color:var(--warning);}
.tag-red{background:#FFF1F0;color:var(--danger);}
.tag-gray{background:#F5F5F5;color:#999;}
+.tag-purple{background:#F9F0FF;color:#722ED1;}
.link{color:var(--primary);cursor:pointer;font-size:var(--font-sm);}.link:hover{color:#40a9ff;}
.pagination{display:flex;align-items:center;justify-content:flex-end;padding:16px 24px;gap:8px;border-top:1px solid var(--border);}
.pagination span{font-size:var(--font-sm);color:var(--text-secondary);}
@@ -210,11 +211,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
✅
@@ -247,7 +248,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
订单状态
- 全部 待接单 待发货 待收货 已完成 已取消
+ 全部 待确认 待接单 待发货 待收货 已完成 已取消
@@ -423,11 +424,37 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
+
+
+ ▶
+ PO-20260416-006
+ 一号场所
+ ⚠️ 待补充
+ 2 种
+ 待核算
+ 2026-04-16
+ 2026-04-17
+ 系统生成
+ 待确认
+ 详情 立即确认
+
+
+
+
+ 溯源码 食材名称 食材分类 采购数量 规格 单价 小计金额 验收数量 备注
+
+ ST-EX-LY01-VG-XLHU-080K-20260416-007 西兰花 蔬菜类 80 散装(kg) 待补充 — — —
+ ST-EX-LY01-VG-QCAI-120K-20260416-008 芹菜 蔬菜类 120 散装(kg) 待补充 — — —
+
+
+
+
+