feat: 营养管理模块消费管理退款、净菜订单支持选择内配调料、采供管理加待确认状态
This commit is contained in:
@@ -143,6 +143,62 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
.detail-info{display:flex;gap:24px;padding:12px 16px;background:#FAFAFA;border-radius:var(--radius-sm);margin-bottom:16px;font-size:var(--font-sm);}
|
||||
.detail-info span{color:var(--text-secondary);}
|
||||
.detail-info b{color:var(--text-primary);font-weight:500;}
|
||||
|
||||
/* 弹窗底部按钮区 */
|
||||
.modal-footer{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;border-top:1px solid var(--border);background:#FAFAFA;border-radius:0 0 8px 8px;}
|
||||
.modal-footer__left{display:flex;align-items:center;gap:8px;font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.modal-footer__right{display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* 退款信息区(黄色警示卡) */
|
||||
.refund-info{display:flex;align-items:center;gap:24px;padding:12px 16px;background:#FFF7E6;border:1px solid #FFE58F;border-radius:var(--radius-sm);margin-bottom:16px;font-size:var(--font-sm);}
|
||||
.refund-info__item{display:flex;flex-direction:column;gap:2px;}
|
||||
.refund-info__label{font-size:var(--font-xs);color:var(--text-secondary);}
|
||||
.refund-info__value{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
|
||||
.refund-info__value.danger{color:var(--danger);}
|
||||
.refund-info__value.success{color:var(--success);}
|
||||
.refund-info__divider{width:1px;height:32px;background:#FFE58F;}
|
||||
|
||||
/* 退款表单 */
|
||||
.refund-form{padding:4px 0;}
|
||||
.refund-tabs{display:inline-flex;background:#F5F5F5;border-radius:var(--radius-sm);padding:4px;margin-bottom:20px;gap:4px;}
|
||||
.refund-tab{padding:6px 16px;border-radius:4px;font-size:var(--font-sm);cursor:pointer;color:var(--text-secondary);transition:all .15s;user-select:none;}
|
||||
.refund-tab.active{background:#fff;color:var(--primary);font-weight:500;box-shadow:0 1px 2px rgba(0,0,0,.06);}
|
||||
.form-row{display:flex;align-items:flex-start;gap:12px;margin-bottom:16px;}
|
||||
.form-row__label{width:88px;flex-shrink:0;font-size:var(--font-sm);color:var(--text-secondary);padding-top:8px;}
|
||||
.form-row__label.required::before{content:"* ";color:var(--danger);}
|
||||
.form-row__content{flex:1;}
|
||||
.form-row__content .tip{font-size:var(--font-xs);color:var(--text-placeholder);margin-top:4px;}
|
||||
.form-input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;}
|
||||
.form-input:focus{border-color:var(--primary);}
|
||||
.form-input:disabled{background:#F5F5F5;color:var(--text-secondary);cursor:not-allowed;}
|
||||
.form-textarea{width:100%;min-height:72px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;resize:vertical;font-family:inherit;}
|
||||
.form-textarea:focus{border-color:var(--primary);}
|
||||
.form-select{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 10px center;appearance:none;cursor:pointer;}
|
||||
.amount-input-group{display:flex;align-items:center;gap:8px;}
|
||||
.amount-input-group .form-input{flex:1;}
|
||||
.amount-input-group .quick-fill{height:36px;padding:0 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;color:var(--primary);font-size:var(--font-xs);cursor:pointer;white-space:nowrap;}
|
||||
.amount-input-group .quick-fill:hover{border-color:var(--primary);background:var(--primary-light);}
|
||||
|
||||
/* 退款汇总区 */
|
||||
.refund-summary{padding:12px 16px;background:#FFF1F0;border:1px solid #FFCCC7;border-radius:var(--radius-sm);margin-top:16px;display:flex;align-items:center;justify-content:space-between;}
|
||||
.refund-summary__text{font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
.refund-summary__amount{font-size:var(--font-xl);font-weight:700;color:var(--danger);}
|
||||
|
||||
/* 二次确认弹窗 */
|
||||
.confirm-modal{background:#fff;border-radius:8px;width:480px;max-width:90vw;box-shadow:0 12px 48px rgba(0,0,0,.15);animation:modalSlideIn .25s ease;}
|
||||
.confirm-modal .modal-body{padding:24px;}
|
||||
.confirm-icon{width:48px;height:48px;border-radius:50%;background:#FFF1F0;display:flex;align-items:center;justify-content:center;font-size:24px;margin:0 auto 16px;}
|
||||
.confirm-title{font-size:var(--font-lg);font-weight:600;text-align:center;margin-bottom:12px;}
|
||||
.confirm-text{font-size:var(--font-sm);color:var(--text-secondary);text-align:center;line-height:1.6;margin-bottom:8px;}
|
||||
.confirm-detail{background:#FAFAFA;border-radius:var(--radius-sm);padding:12px 16px;margin:12px 0;font-size:var(--font-sm);}
|
||||
.confirm-detail__row{display:flex;justify-content:space-between;padding:4px 0;}
|
||||
.confirm-detail__row span{color:var(--text-secondary);}
|
||||
.confirm-detail__row b{color:var(--text-primary);font-weight:500;}
|
||||
|
||||
/* 退款方式标签 */
|
||||
.refund-method-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);margin-left:8px;}
|
||||
.refund-method-tag.full{background:#FFF1F0;color:var(--danger);}
|
||||
.refund-method-tag.partial{background:#FFF7E6;color:var(--warning);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -317,7 +373,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tbody>
|
||||
<tr><td>TX-20260421-001</td><td>张伟(EMP001)</td><td>总部机关/研发部</td><td><span class="tag tag-blue">点餐</span></td><td>午餐</td><td><span class="tag tag-green">消费</span></td><td><a class="link">¥22.00</a></td><td>¥22.00</td><td>余额支付</td><td><span class="tag tag-green">成功</span></td><td><span class="tag tag-green">88分</span></td><td><span class="tag tag-green">已对账</span></td><td>—</td><td>¥234.50</td><td>2026-04-21 11:02</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-002</td><td>李娜(EMP002)</td><td>总部机关/市场部</td><td><span class="tag tag-blue">点餐</span></td><td>午餐</td><td><span class="tag tag-green">消费</span></td><td><a class="link">¥18.00</a></td><td>¥18.00</td><td>余额支付</td><td><span class="tag tag-green">成功</span></td><td><span class="tag tag-green">92分</span></td><td><span class="tag tag-green">已对账</span></td><td>—</td><td>¥86.20</td><td>2026-04-21 11:08</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-003</td><td>王磊(EMP003)</td><td>总部机关/运营部</td><td><span class="tag tag-gray">餐线</span></td><td>午餐</td><td><span class="tag tag-green">消费</span></td><td><a class="link">¥28.00</a></td><td>¥25.20</td><td>微信支付</td><td><span class="tag tag-green">成功</span></td><td><span class="tag tag-orange">68分</span></td><td><span class="tag tag-orange">待对账</span></td><td><span class="tag tag-orange">短时多笔</span></td><td>—</td><td>2026-04-21 11:15</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-003</td><td>王磊(EMP003)</td><td>总部机关/运营部</td><td><span class="tag tag-gray">餐线</span></td><td>午餐</td><td><span class="tag tag-green">消费</span></td><td><a class="link">¥28.00</a></td><td>¥25.20</td><td>微信支付</td><td><span class="tag tag-orange">部分退款</span></td><td><span class="tag tag-orange">68分</span></td><td><span class="tag tag-orange">待对账</span></td><td><span class="tag tag-orange">短时多笔</span></td><td>—</td><td>2026-04-21 11:15</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-004</td><td>赵敏(EMP004)</td><td>总部机关/财务部</td><td><span class="tag tag-blue">账户</span></td><td>—</td><td><span class="tag tag-blue">充值</span></td><td>+¥100.00</td><td>¥100.00</td><td>微信支付</td><td><span class="tag tag-green">成功</span></td><td>—</td><td><span class="tag tag-green">已对账</span></td><td>—</td><td>¥356.80</td><td>2026-04-21 09:30</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-005</td><td>陈刚(EMP005)</td><td>总部机关/人事部</td><td><span class="tag tag-gray">餐线</span></td><td>早餐</td><td><span class="tag tag-green">消费</span></td><td><a class="link">¥12.00</a></td><td>¥12.00</td><td>余额支付</td><td><span class="tag tag-green">成功</span></td><td><span class="tag tag-green">85分</span></td><td><span class="tag tag-green">已对账</span></td><td>—</td><td>¥142.60</td><td>2026-04-21 07:45</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
<tr><td>TX-20260421-006</td><td>刘芳(EMP006)</td><td>总部机关/市场部</td><td><span class="tag tag-blue">点餐</span></td><td>午餐</td><td><span class="tag tag-red">退款</span></td><td>+¥18.00</td><td>¥18.00</td><td>原路退回</td><td><span class="tag tag-gray">已退款</span></td><td><span class="tag tag-orange">76分</span></td><td><span class="tag tag-red">差异</span></td><td><span class="tag tag-orange">退款频繁</span></td><td>¥68.40</td><td>2026-04-21 10:20</td><td class="td-actions"><a class="link">交易明细</a> <a class="link">餐品明细</a></td></tr>
|
||||
@@ -344,56 +400,99 @@ function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
|
||||
/* 日期默认今天 */
|
||||
(function(){var d=document.getElementById('filterDate');if(d){var t=new Date();d.value=t.getFullYear()+'-'+String(t.getMonth()+1).padStart(2,'0')+'-'+String(t.getDate()).padStart(2,'0');}})();
|
||||
|
||||
/* ========== 弹窗基础 ========== */
|
||||
/* ========== 弹窗基础 ==========
|
||||
* open(title, bodyHtml, footerHtml?, options?)
|
||||
* options.keepExisting 是否保留已有弹窗(叠加模式,用于二次确认)
|
||||
* options.zIndex 自定义 z-index(叠加层)
|
||||
* options.modalClass 自定义 modal 类名(如 confirm-modal)
|
||||
* options.hideHeader 是否隐藏 header(二次确认弹窗自带图标标题)
|
||||
* close() 关闭最上层弹窗
|
||||
* closeAll() 关闭所有弹窗
|
||||
*/
|
||||
const Modal = {
|
||||
open(title, bodyHtml) {
|
||||
this.close();
|
||||
open(title, bodyHtml, footerHtml, options = {}) {
|
||||
if (!options.keepExisting) this.closeAll();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = `<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title">${title}</div><button class="modal-close" onclick="Modal.close()">×</button></div>
|
||||
if (options.zIndex) overlay.style.zIndex = options.zIndex;
|
||||
const modalClass = options.modalClass || 'modal';
|
||||
const headerHtml = options.hideHeader ? '' : `<div class="modal-header"><div class="modal-title">${title}</div><button class="modal-close" onclick="Modal.close()">×</button></div>`;
|
||||
const footerSection = footerHtml ? `<div class="modal-footer">${footerHtml}</div>` : '';
|
||||
overlay.innerHTML = `<div class="${modalClass}">
|
||||
${headerHtml}
|
||||
<div class="modal-body">${bodyHtml}</div>
|
||||
${footerSection}
|
||||
</div>`;
|
||||
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 === '成功' ? '<span class="tag tag-green">成功</span>' : '<span class="tag tag-gray">已退款</span>';
|
||||
|
||||
// 计算可退款状态
|
||||
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 = '<span class="tag tag-gray">已退款</span>';
|
||||
else if (isPartiallyRefunded) statusTag = '<span class="tag tag-orange">部分退款</span>';
|
||||
else statusTag = '<span class="tag tag-green">成功</span>';
|
||||
|
||||
const typeMap = {'消费':'tag-green','充值':'tag-blue','退款':'tag-red'};
|
||||
const typeTag = `<span class="tag ${typeMap[d.type]||'tag-gray'}">${d.type}</span>`;
|
||||
const html = `<div class="detail-grid">
|
||||
|
||||
// 退款信息区(可退款或有退款记录时显示)
|
||||
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 = '<span class="refund-method-tag full">全额退款</span>';
|
||||
else if (d.refundMethod === 'partial') methodTag = '<span class="refund-method-tag partial">部分退款</span>';
|
||||
refundInfoHtml = `<div class="refund-info">
|
||||
<div class="refund-info__item">
|
||||
<span class="refund-info__label">原始消费金额</span>
|
||||
<span class="refund-info__value">${formatMoney(d.amountNum)}</span>
|
||||
</div>
|
||||
<div class="refund-info__divider"></div>
|
||||
<div class="refund-info__item">
|
||||
<span class="refund-info__label">已退金额</span>
|
||||
<span class="refund-info__value danger">${formatMoney(d.refundedAmount)}</span>
|
||||
</div>
|
||||
<div class="refund-info__divider"></div>
|
||||
<div class="refund-info__item">
|
||||
<span class="refund-info__label">可退金额${methodTag}</span>
|
||||
<span class="refund-info__value ${isRefundable ? 'success' : 'danger'}">${formatMoney(refundableAmount)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 完全退款提示
|
||||
if (isFullyRefunded) {
|
||||
refundInfoHtml = `<div class="refund-info" style="background:#F6FFED;border-color:#B7EB8F;">
|
||||
<div class="refund-info__item">
|
||||
<span class="refund-info__label">退款状态</span>
|
||||
<span class="refund-info__value success">已全额退款</span>
|
||||
</div>
|
||||
<div class="refund-info__divider" style="background:#B7EB8F;"></div>
|
||||
<div class="refund-info__item">
|
||||
<span class="refund-info__label">累计退款金额</span>
|
||||
<span class="refund-info__value">${formatMoney(d.refundedAmount)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 明细视图 HTML(始终构建)
|
||||
const detailViewHtml = `${refundInfoHtml}<div class="detail-grid">
|
||||
<div class="detail-grid__item"><span class="detail-grid__label">流水号</span><span class="detail-grid__value">${d.txId}</span></div>
|
||||
<div class="detail-grid__item"><span class="detail-grid__label">姓名/编号</span><span class="detail-grid__value">${d.user}</span></div>
|
||||
<div class="detail-grid__item"><span class="detail-grid__label">单位/部门</span><span class="detail-grid__value">${d.dept}</span></div>
|
||||
@@ -448,7 +609,295 @@ function showTransDetail(txId) {
|
||||
<div class="detail-grid__item"><span class="detail-grid__label">操作员</span><span class="detail-grid__value">${d.operator}</span></div>
|
||||
<div class="detail-grid__item"><span class="detail-grid__label">备注</span><span class="detail-grid__value">${d.remark}</span></div>
|
||||
</div>`;
|
||||
Modal.open('交易明细 — ' + txId, html);
|
||||
|
||||
// 退款视图 HTML(仅可退款时构建,初始隐藏)
|
||||
const refundViewHtml = isRefundable ? buildRefundView(txId) : '';
|
||||
const refundViewWrapper = isRefundable ? `<div id="refundView" style="display:none;">${refundViewHtml}</div>` : '';
|
||||
|
||||
// 整体 body:明细视图 + 退款视图(切换显示)
|
||||
const bodyHtml = `<div id="detailView">${detailViewHtml}</div>${refundViewWrapper}`;
|
||||
|
||||
// 底部按钮区:可退款时显示退款按钮
|
||||
let footerHtml;
|
||||
if (isRefundable) {
|
||||
footerHtml = `<div class="modal-footer__left">该交易可发起退款</div>
|
||||
<div class="modal-footer__right">
|
||||
<button class="btn btn-default" onclick="Modal.close()">关闭</button>
|
||||
<button class="btn btn-primary" style="background:var(--danger);" onclick="switchToRefundView('${txId}')">↩️ 退款</button>
|
||||
</div>`;
|
||||
} else {
|
||||
footerHtml = `<div class="modal-footer__left">${isFullyRefunded ? '该交易已全额退款,不可再退款' : (isConsumption ? '' : '充值/退款记录不可发起退款')}</div>
|
||||
<div class="modal-footer__right">
|
||||
<button class="btn btn-default" onclick="Modal.close()">关闭</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 => `<option value="${r}">${r}</option>`).join('');
|
||||
|
||||
return `<div class="refund-form">
|
||||
<div class="detail-info" style="margin-bottom:16px;">
|
||||
<span>流水号:<b>${d.txId}</b></span>
|
||||
<span>用户:<b>${d.user}</b></span>
|
||||
<span>订单:<b>${d.orderId}</b></span>
|
||||
<span>可退金额:<b style="color:var(--danger);">${formatMoney(refundableAmount)}</b></span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-row__label">退款方式</div>
|
||||
<div class="form-row__content">
|
||||
<div class="refund-tabs" id="refundTabs">
|
||||
<div class="refund-tab active" data-method="full" onclick="switchRefundMethod('full')">全额退款</div>
|
||||
<div class="refund-tab" data-method="partial" onclick="switchRefundMethod('partial')">部分退款</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-row__label required">退款金额</div>
|
||||
<div class="form-row__content">
|
||||
<div class="amount-input-group">
|
||||
<input type="number" class="form-input" id="refundAmount" value="${refundableAmount}" step="0.01" min="0.01" max="${refundableAmount}" disabled>
|
||||
<span class="quick-fill" id="quickFillBtn" onclick="fillMaxAmount()" style="display:none;">填入最大可退</span>
|
||||
</div>
|
||||
<div class="tip" id="amountTip">全额退款模式:自动填入剩余可退金额 ${formatMoney(refundableAmount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-row__label">退款原因</div>
|
||||
<div class="form-row__content">
|
||||
<select class="form-select" id="refundReason">
|
||||
<option value="">请选择退款原因(选填)</option>
|
||||
${reasonOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-row__label">备注</div>
|
||||
<div class="form-row__content">
|
||||
<textarea class="form-textarea" id="refundRemark" placeholder="请输入备注信息(选填),最多 200 字" maxlength="200"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="refund-summary">
|
||||
<span class="refund-summary__text">本次退款金额</span>
|
||||
<span class="refund-summary__amount" id="refundSummary">${formatMoney(refundableAmount)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* 视图切换:明细视图 → 退款视图 */
|
||||
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 = `<div class="modal-footer__left">退款后金额将原路返回至用户账户</div>
|
||||
<div class="modal-footer__right">
|
||||
<button class="btn btn-default" onclick="switchToDetailView('${txId}')">返回</button>
|
||||
<button class="btn btn-primary" style="background:var(--danger);" onclick="confirmRefund('${txId}')">确认退款</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 绑定金额输入实时更新汇总
|
||||
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 = `<div class="modal-footer__left">该交易可发起退款</div>
|
||||
<div class="modal-footer__right">
|
||||
<button class="btn btn-default" onclick="Modal.close()">关闭</button>
|
||||
<button class="btn btn-primary" style="background:var(--danger);" onclick="switchToRefundView('${txId}')">↩️ 退款</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* 切换退款方式(全额/部分) */
|
||||
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 = `<div style="text-align:center;">
|
||||
<div class="confirm-icon" style="background:#FFF1F0;">⚠️</div>
|
||||
<div class="confirm-title">确认发起${methodText}?</div>
|
||||
<div class="confirm-text">退款后金额将原路返回至用户账户,该操作不可撤销</div>
|
||||
<div class="confirm-detail">
|
||||
<div class="confirm-detail__row"><span>流水号</span><b>${d.txId}</b></div>
|
||||
<div class="confirm-detail__row"><span>用户</span><b>${d.user}</b></div>
|
||||
<div class="confirm-detail__row"><span>退款方式</span><b>${methodText}</b></div>
|
||||
<div class="confirm-detail__row"><span>本次退款金额</span><b style="color:var(--danger);font-weight:700;">${formatMoney(amount)}</b></div>
|
||||
<div class="confirm-detail__row"><span>累计已退金额</span><b>${formatMoney(newRefundedTotal)}</b></div>
|
||||
<div class="confirm-detail__row"><span>剩余可退金额</span><b>${formatMoney(remaining)}</b></div>
|
||||
${reason ? `<div class="confirm-detail__row"><span>退款原因</span><b>${reason}</b></div>` : ''}
|
||||
${remark ? `<div class="confirm-detail__row"><span>备注</span><b>${remark}</b></div>` : ''}
|
||||
</div>
|
||||
${(!isThisFull && refundableAmount - amount > 0.001) ? `<div style="font-size:12px;color:var(--warning);margin-top:8px;">本次退款后,该订单仍可继续退款 ${formatMoney(remaining)}</div>` : ''}
|
||||
${isThisFull ? `<div style="font-size:12px;color:var(--text-placeholder);margin-top:8px;">本次退款后,该订单将完成全部退款,不可再发起退款</div>` : ''}
|
||||
</div>`;
|
||||
|
||||
const footerHtml = `<div class="modal-footer__left"></div>
|
||||
<div class="modal-footer__right">
|
||||
<button class="btn btn-default" onclick="Modal.close()">取消</button>
|
||||
<button class="btn btn-primary" style="background:var(--danger);" onclick="processRefund('${txId}', '${method}', ${amount}, '${reason.replace(/'/g, "\\'")}', '${remark.replace(/'/g, "\\'")}')">确认退款</button>
|
||||
</div>`;
|
||||
|
||||
// 叠加模式:保留交易明细弹窗,二次确认弹窗在其之上
|
||||
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 = '<span class="tag tag-gray">已退款</span>';
|
||||
} else if (d.refundedAmount > 0) {
|
||||
statusCell.innerHTML = '<span class="tag tag-orange">部分退款</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ========== 级联选择器 ========== */
|
||||
|
||||
@@ -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: 由食谱快速创建订单支持餐次级人数 + 新增「生成中」状态
|
||||
|
||||
### 变更背景
|
||||
|
||||
@@ -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;}
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__icon blue">📋</div>
|
||||
<div class="stat-card__info"><div class="stat-card__value">68</div><div class="stat-card__label">采购单总数</div></div>
|
||||
<div class="stat-card__info"><div class="stat-card__value">69</div><div class="stat-card__label">采购单总数</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__icon orange">⏳</div>
|
||||
<div class="stat-card__info"><div class="stat-card__value">5</div><div class="stat-card__label">进行中</div></div>
|
||||
<div class="stat-card__info"><div class="stat-card__value">6</div><div class="stat-card__label">进行中</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__icon green">✅</div>
|
||||
@@ -247,7 +248,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="filter-item">
|
||||
<label>订单状态</label>
|
||||
<select class="filter-select">
|
||||
<option>全部</option><option>待接单</option><option>待发货</option><option>待收货</option><option>已完成</option><option>已取消</option>
|
||||
<option>全部</option><option>待确认</option><option>待接单</option><option>待发货</option><option>待收货</option><option>已完成</option><option>已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
@@ -423,11 +424,37 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="sub-table__footer">合计:2 种食材,1168 kg,¥6,188</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单6:待确认(系统自动生成,缺供应商与单价) -->
|
||||
<tr>
|
||||
<td><span class="expand-toggle" onclick="toggleRow(this,'detail-6')">▶</span></td>
|
||||
<td>PO-20260416-006</td>
|
||||
<td>一号场所</td>
|
||||
<td><span style="color:var(--danger);">⚠️ 待补充</span></td>
|
||||
<td>2 种</td>
|
||||
<td style="font-weight:600;color:var(--warning);">待核算</td>
|
||||
<td>2026-04-16</td>
|
||||
<td>2026-04-17</td>
|
||||
<td><span class="tag tag-purple">系统生成</span></td>
|
||||
<td><span class="tag tag-purple">待确认</span></td>
|
||||
<td class="td-actions"><a class="link" onclick="showDetail(5)">详情</a> <a class="link" style="color:#722ED1;" onclick="confirmOrder(5)">立即确认</a></td>
|
||||
</tr>
|
||||
<tr class="detail-row" id="detail-6">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-XLHU-080K-20260416-007</td><td>西兰花</td><td>蔬菜类</td><td>80</td><td>散装(kg)</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-QCAI-120K-20260416-008</td><td>芹菜</td><td>蔬菜类</td><td>120</td><td>散装(kg)</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer" style="color:var(--warning);">合计:2 种食材,200 kg,单价待补充</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<span>显示 5 条,共 68 条</span>
|
||||
<span>显示 6 条,共 69 条</span>
|
||||
<button class="page-btn">‹</button>
|
||||
<button class="page-btn active">1</button>
|
||||
<button class="page-btn">2</button>
|
||||
@@ -447,6 +474,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<span class="modal-close" onclick="closePurchaseModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 系统生成订单的补全提示条(仅"立即确认"入口显示) -->
|
||||
<div id="confirmHintBar" style="display:none;margin-bottom:16px;padding:12px 16px;background:#FFF7E6;border:1px solid #FFE58F;border-left:4px solid var(--warning);border-radius:var(--radius-sm);align-items:center;gap:8px;font-size:var(--font-sm);color:var(--text-primary);">
|
||||
<span style="font-size:16px;">⚠️</span>
|
||||
<span>该采购单由系统自动生成,请补全食材单价和供应商信息后提交</span>
|
||||
</div>
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-grid">
|
||||
<div class="form-item"><label>采购场所<span class="required">*</span></label><select id="fm-canteen" onchange="showCanteenContact()"><option value="">请选择场所</option><option>一号场所</option><option>二号场所</option><option>三号场所</option></select></div>
|
||||
@@ -704,6 +736,14 @@ var orders = [
|
||||
items:[
|
||||
{trace:'ST-EX-LY02-DG-DAMI-1000-20260413-001',name:'大米',cat:'粮油类',qty:1000,spec:'20kg/箱',price:3.5,subtotal:'¥3,500',acceptedQty:'—',remark:'东北五常大米'},
|
||||
{trace:'ST-EX-LY02-DG-HSYO-168K-20260413-002',name:'花生油',cat:'粮油类',qty:168,spec:'5kg/桶',price:16,subtotal:'¥2,688',acceptedQty:'—',remark:'—'}
|
||||
]},
|
||||
/* 待确认订单:系统自动生成,缺供应商与单价,需用户通过"立即确认"补全后流转为"待接单" */
|
||||
{ no:'PO-20260416-006', canteen:'一号场所', canteenContact:'王建国', canteenPhone:'029-8812-3001',
|
||||
supplier:'', supplierContact:'', supplierPhone:'',
|
||||
date:'2026-04-16', arrival:'2026-04-17', source:'系统生成', status:'待确认', total:'待核算',
|
||||
items:[
|
||||
{trace:'ST-EX-LY01-VG-XLHU-080K-20260416-007',name:'西兰花',cat:'蔬菜类',qty:80,spec:'散装(kg)',price:null,subtotal:'—',acceptedQty:'—',remark:'—'},
|
||||
{trace:'ST-EX-LY01-VG-QCAI-120K-20260416-008',name:'芹菜',cat:'蔬菜类',qty:120,spec:'散装(kg)',price:null,subtotal:'—',acceptedQty:'—',remark:'—'}
|
||||
]}
|
||||
];
|
||||
|
||||
@@ -752,6 +792,8 @@ function openCreateModal() {
|
||||
document.getElementById('ciCanteen').style.display = 'none';
|
||||
document.getElementById('ciSupplier').style.display = 'none';
|
||||
document.getElementById('contactInfoBar').style.display = 'none';
|
||||
document.getElementById('confirmHintBar').style.display = 'none';
|
||||
setSubmitBtnText('提交采购单');
|
||||
document.getElementById('itemsBody').innerHTML = '';
|
||||
addItemRow();
|
||||
recalcTotal();
|
||||
@@ -766,18 +808,23 @@ function openEditModal(idx) {
|
||||
document.getElementById('fm-source').value = o.source;
|
||||
showCanteenContact();
|
||||
showSupplierContact();
|
||||
document.getElementById('confirmHintBar').style.display = 'none';
|
||||
setSubmitBtnText('提交采购单');
|
||||
var tbody = document.getElementById('itemsBody');
|
||||
tbody.innerHTML = '';
|
||||
o.items.forEach(function(it) {
|
||||
var specParts = parseSpecText(it.spec);
|
||||
/* 待确认订单:单价为 null 时空值回填 + 橙色边框提醒 */
|
||||
var priceVal = (it.price === null || it.price === undefined) ? '' : it.price;
|
||||
var subtotalText = (it.subtotal === '—' || !it.subtotal) ? '¥0' : it.subtotal;
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td><input type="text" value="'+it.trace+'" placeholder="请输入溯源码(选填)"></td>'
|
||||
+'<td><select><option'+(it.name==='菠菜'?' selected':'')+'>菠菜</option><option'+(it.name==='白菜'?' selected':'')+'>白菜</option><option'+(it.name==='胡萝卜'?' selected':'')+'>胡萝卜</option><option'+(it.name==='西兰花'?' selected':'')+'>西兰花</option><option'+(it.name==='芹菜'?' selected':'')+'>芹菜</option><option'+(it.name==='猪里脊'?' selected':'')+'>猪里脊</option><option'+(it.name==='猪五花'?' selected':'')+'>猪五花</option><option'+(it.name==='草鱼'?' selected':'')+'>草鱼</option><option'+(it.name==='鲫鱼'?' selected':'')+'>鲫鱼</option><option'+(it.name==='虾'?' selected':'')+'>虾</option><option'+(it.name==='大米'?' selected':'')+'>大米</option><option'+(it.name==='花生油'?' selected':'')+'>花生油</option></select></td>'
|
||||
+'<td><select><option'+(it.cat==='蔬菜类'?' selected':'')+'>蔬菜类</option><option'+(it.cat==='肉禽类'?' selected':'')+'>肉禽类</option><option'+(it.cat==='水产类'?' selected':'')+'>水产类</option><option'+(it.cat==='粮油类'?' selected':'')+'>粮油类</option><option>调味品</option><option>豆制品</option></select></td>'
|
||||
+'<td><input type="number" class="item-qty" value="'+it.qty+'" onchange="calcRow(this)"></td>'
|
||||
+'<td><div class="spec-input-group"><input type="number" class="item-gram" value="'+specParts.gram+'" placeholder="克重" onchange="calcRow(this)"><select class="item-unit" onchange="calcRow(this)">'+SPEC_UNIT_OPTIONS.map(function(u){return '<option'+(u===specParts.unit?' selected':'')+'>'+u+'</option>';}).join('')+'</select></div></td>'
|
||||
+'<td><input type="number" class="item-price" value="'+it.price+'" onchange="calcRow(this)"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">'+it.subtotal+'</td>'
|
||||
+'<td><input type="number" class="item-price" value="'+priceVal+'" placeholder="0" onchange="calcRow(this)" style="border-color:var(--warning);"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">'+subtotalText+'</td>'
|
||||
+'<td><input type="text" value="'+(it.remark==='—'?'':it.remark)+'" placeholder="选填"></td>'
|
||||
+'<td><span class="btn-remove" onclick="removeItemRow(this)">✕</span></td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -839,28 +886,42 @@ function buildDetailRow(label, value) {
|
||||
}
|
||||
function showDetail(idx) {
|
||||
var o = orders[idx];
|
||||
var statusMap = {'待接单':'tag-orange','待发货':'tag-blue','待收货':'tag-orange','已完成':'tag-green','已取消':'tag-red'};
|
||||
var statusMap = {'待确认':'tag-purple','待接单':'tag-orange','待发货':'tag-blue','待收货':'tag-orange','已完成':'tag-green','已取消':'tag-red'};
|
||||
var statusHtml = '<span class="tag '+(statusMap[o.status]||'tag-gray')+'">'+o.status+'</span>';
|
||||
/* 待确认订单:总额显示橙色"待核算",并追加补全提示项 */
|
||||
var totalHtml = o.status === '待确认'
|
||||
? '<span style="font-weight:600;color:var(--warning);">'+o.total+'</span>'
|
||||
: '<span style="font-weight:600;color:var(--primary);">'+o.total+'</span>';
|
||||
var hintRow = o.status === '待确认'
|
||||
? buildDetailRow('提示', '<span style="color:var(--warning);">⚠️ 该订单由系统自动生成,请补全食材单价与供应商信息</span>')
|
||||
: '';
|
||||
document.getElementById('drawerOrderInfo').innerHTML =
|
||||
buildDetailRow('采购单号', o.no)
|
||||
+ buildDetailRow('订单状态', statusHtml)
|
||||
+ buildDetailRow('采购单来源', o.source)
|
||||
+ buildDetailRow('下单日期', o.date)
|
||||
+ buildDetailRow('预计到货', o.arrival)
|
||||
+ buildDetailRow('采购总额', '<span style="font-weight:600;color:var(--primary);">'+o.total+'</span>');
|
||||
+ buildDetailRow('采购总额', totalHtml)
|
||||
+ hintRow;
|
||||
document.getElementById('drawerCanteenInfo').innerHTML =
|
||||
buildDetailRow('采购场所', o.canteen)
|
||||
+ buildDetailRow('场所联系人', o.canteenContact)
|
||||
+ buildDetailRow('联系电话', o.canteenPhone);
|
||||
/* 待确认订单:供应商为空时显示"⚠️ 待补充" */
|
||||
var supplierDisplay = o.supplier ? o.supplier : '<span style="color:var(--danger);">⚠️ 待补充</span>';
|
||||
document.getElementById('drawerSupplierInfo').innerHTML =
|
||||
buildDetailRow('供应商', o.supplier)
|
||||
+ buildDetailRow('供应商联系人', o.supplierContact)
|
||||
+ buildDetailRow('联系电话', o.supplierPhone);
|
||||
buildDetailRow('供应商', supplierDisplay)
|
||||
+ buildDetailRow('供应商联系人', o.supplierContact || '—')
|
||||
+ buildDetailRow('联系电话', o.supplierPhone || '—');
|
||||
var tbody = document.getElementById('drawerItemsBody');
|
||||
tbody.innerHTML = '';
|
||||
o.items.forEach(function(it, i) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td>'+(i+1)+'</td><td style="font-size:10px;color:#999;">'+it.trace+'</td><td>'+it.name+'</td><td>'+it.cat+'</td><td>'+it.qty+'</td><td>'+it.spec+'</td><td>¥'+it.price+'</td><td>'+it.subtotal+'</td><td>'+(it.acceptedQty||'—')+'</td><td>'+it.remark+'</td>';
|
||||
/* 待确认订单:单价为 null 时显示"待补充" */
|
||||
var priceHtml = (it.price === null || it.price === '' || it.price === undefined)
|
||||
? '<span style="color:var(--danger);">待补充</span>'
|
||||
: '¥'+it.price;
|
||||
tr.innerHTML = '<td>'+(i+1)+'</td><td style="font-size:10px;color:#999;">'+it.trace+'</td><td>'+it.name+'</td><td>'+it.cat+'</td><td>'+it.qty+'</td><td>'+it.spec+'</td><td>'+priceHtml+'</td><td>'+(it.subtotal||'—')+'</td><td>'+(it.acceptedQty||'—')+'</td><td>'+it.remark+'</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
/* 配送信息(待收货/已完成显示) */
|
||||
@@ -908,6 +969,22 @@ function acceptOrder(idx) {
|
||||
showToast('采购单 ' + o.no + ' 已成功接单!', 'success');
|
||||
}
|
||||
|
||||
/* ===== 修改编辑弹窗提交按钮文案 ===== */
|
||||
function setSubmitBtnText(text) {
|
||||
var btn = document.querySelector('#purchaseModal .modal-footer .btn-primary');
|
||||
if (btn) btn.textContent = text;
|
||||
}
|
||||
|
||||
/* ===== 立即确认(待确认订单专用入口) =====
|
||||
业务路径:系统自动生成的采购单缺少供应商与食材单价
|
||||
点击"立即确认" → 复用编辑弹窗 → 顶部显示橙色补全提示条
|
||||
→ 用户补全供应商选择 + 各食材单价 → 提交后流转为"待接单" */
|
||||
function confirmOrder(idx) {
|
||||
openEditModal(idx);
|
||||
document.getElementById('confirmHintBar').style.display = 'flex';
|
||||
setSubmitBtnText('确认并提交采购单');
|
||||
}
|
||||
|
||||
/* ===== 取消订单弹窗 ===== */
|
||||
var cancelIdx = -1;
|
||||
function openCancelModal(idx) {
|
||||
|
||||
@@ -180,6 +180,10 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
|
||||
.spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
.spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
/* 内配调料单选组:重置 radio 宽高,避免被表格 input 规则拉伸 */
|
||||
.seasoning-radio-group{display:inline-flex;align-items:center;gap:12px;white-space:nowrap;}
|
||||
.seasoning-radio-group label{display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:var(--font-xs);color:var(--text-primary);}
|
||||
.seasoning-radio-group input[type="radio"]{width:auto;height:auto;margin:0;cursor:pointer;accent-color:var(--primary);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -332,6 +336,15 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
|
||||
<tbody id="dailyMenuBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="dishSeasoningSection" style="margin-top:16px;display:none;">
|
||||
<div class="drawer-section__title">餐品明细(重复餐品已自动合并,请逐道确认是否需要内配调料)</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="inline-builder-table" id="dishSeasoningTable" style="margin-bottom:0;">
|
||||
<thead><tr><th style="width:50%;">菜品名称</th><th>内配调料</th></tr></thead>
|
||||
<tbody id="dishSeasoningBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recipe-tip">
|
||||
<span class="recipe-tip__icon">⏳</span>
|
||||
<span class="recipe-tip__text"><b>系统智能核算提示:</b>订单提交后,系统将依据您填写的各餐次就餐人数自动核算采购量并生成净菜明细,处理过程约需 1~2 分钟。生成结果与最终订单金额将在 <b>订单列表</b> 中实时更新,请耐心等待并关注订单状态变化。</span>
|
||||
@@ -424,13 +437,13 @@ var orders=[
|
||||
orderNo:'CO-002',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
|
||||
lines:[
|
||||
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:8,subtotal:320,status:'配送中'},
|
||||
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:30,weight:15,price:12,subtotal:180,status:'已完成'}
|
||||
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:30,weight:15,price:12,subtotal:180,status:'已完成',needSeasoning:true}
|
||||
]
|
||||
},
|
||||
{
|
||||
orderNo:'CO-003',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
|
||||
lines:[
|
||||
{id:'L5',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(500g)',qty:15,weight:7.5,price:15,subtotal:112.5,status:'配送中'}
|
||||
{id:'L5',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(500g)',qty:15,weight:7.5,price:15,subtotal:112.5,status:'配送中',needSeasoning:false}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -443,7 +456,7 @@ var orders=[
|
||||
{
|
||||
orderNo:'CO-005',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:00',creator:'张厨师',
|
||||
lines:[
|
||||
{id:'L8',ncType:'cp',foodName:'红烧排骨',spec:'标准份(800g)',qty:10,weight:8,price:18,subtotal:144,status:'备货中'},
|
||||
{id:'L8',ncType:'cp',foodName:'红烧排骨',spec:'标准份(800g)',qty:10,weight:8,price:18,subtotal:144,status:'备货中',needSeasoning:true},
|
||||
{id:'L9',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丁',spec:'500g/袋',qty:15,weight:7.5,price:10,subtotal:75,status:'备货中'},
|
||||
{id:'L10',ncType:'nc',ingredient:'草鱼',ncName:'鱼片',spec:'1kg/盘',qty:8,weight:8,price:20,subtotal:160,status:'备货中'}
|
||||
]
|
||||
@@ -451,8 +464,8 @@ var orders=[
|
||||
{
|
||||
orderNo:'CO-006',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 10:00',deliverTime:'2026-04-22 11:30',creator:'王厨师',
|
||||
lines:[
|
||||
{id:'L11',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:25,weight:12.5,price:12,subtotal:150,status:'已完成'},
|
||||
{id:'L12',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g)',qty:20,weight:12,price:15,subtotal:180,status:'已完成'}
|
||||
{id:'L11',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:25,weight:12.5,price:12,subtotal:150,status:'已完成',needSeasoning:true},
|
||||
{id:'L12',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g)',qty:20,weight:12,price:15,subtotal:180,status:'已完成',needSeasoning:false}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -466,8 +479,8 @@ var orders=[
|
||||
orderNo:'CO-008',orderType:'内供',ncType:'cp',claimTime:'2026-04-22 11:00',deliverTime:'2026-04-22 13:00',creator:'张厨师',
|
||||
lines:[
|
||||
{id:'L15',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:5,subtotal:75,status:'已完成'},
|
||||
{id:'L16',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g)',qty:15,weight:6,price:10,subtotal:60,status:'已完成'},
|
||||
{id:'L17',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g)',qty:20,weight:6,price:8,subtotal:48,status:'已完成'}
|
||||
{id:'L16',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g)',qty:15,weight:6,price:10,subtotal:60,status:'已完成',needSeasoning:false},
|
||||
{id:'L17',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g)',qty:20,weight:6,price:8,subtotal:48,status:'已完成',needSeasoning:true}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -502,6 +515,14 @@ function ncTypeTag(type){
|
||||
return '<span class="tag tag-orange">🍱 餐品净菜包</span>';
|
||||
}
|
||||
|
||||
/* 内配调料徽标:cp 行显示「是/否」,nc 行显示 — */
|
||||
function seasoningTag(line){
|
||||
if(line.ncType!=='cp')return '<span style="color:var(--text-placeholder);">—</span>';
|
||||
return line.needSeasoning
|
||||
?'<span class="tag tag-blue">✓ 是</span>'
|
||||
:'<span class="tag tag-gray">✗ 否</span>';
|
||||
}
|
||||
|
||||
function orderTypeTag(type){
|
||||
if(type==='内供')return '<span class="tag tag-blue">🏢 内供</span>';
|
||||
return '<span class="tag tag-orange">🛒 外售</span>';
|
||||
@@ -545,9 +566,9 @@ function renderOrderTable(){
|
||||
/* 子表头 */
|
||||
if(order.lines.length>0){
|
||||
if(order.ncType==='nc'){
|
||||
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
}else{
|
||||
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
}
|
||||
}
|
||||
/* 明细行 */
|
||||
@@ -563,7 +584,9 @@ function renderOrderTable(){
|
||||
html+='<td>'+line.qty+'</td>';
|
||||
html+='<td>'+line.weight+'</td>';
|
||||
html+='<td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
|
||||
html+='<td></td><td></td><td></td><td></td>';
|
||||
/* 内配调料列:仅 cp 类型显示是否需要,nc 类型显示 — */
|
||||
html+='<td>'+seasoningTag(line)+'</td>';
|
||||
html+='<td></td><td></td><td></td>';
|
||||
html+='<td class="td-actions"></td></tr>';
|
||||
});
|
||||
});
|
||||
@@ -632,7 +655,8 @@ function resetOrderForm(){
|
||||
function addOrderLine(){
|
||||
lineIdCounter++;
|
||||
var lid='newLine'+lineIdCounter;
|
||||
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',portion:'标准份',baseGram:0,unit:newOrderNcType==='nc'?'袋':'份',qty:0,weight:0,price:0,subtotal:0});
|
||||
/* needSeasoning 默认 false(不需要内配调料),仅在 cp 模式下生效 */
|
||||
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',portion:'标准份',baseGram:0,unit:newOrderNcType==='nc'?'袋':'份',qty:0,weight:0,price:0,subtotal:0,needSeasoning:false});
|
||||
renderLineBuilders();
|
||||
}
|
||||
|
||||
@@ -726,6 +750,12 @@ function updateNewLinePrice(lid,val){
|
||||
calcLineWeight(line);
|
||||
refreshSummary();
|
||||
}
|
||||
/* 切换"是否需要内配调料"(仅 cp 模式生效) */
|
||||
function updateNewLineSeasoning(lid,val){
|
||||
var line=newOrderLines.find(function(l){return l.id===lid;});
|
||||
if(!line)return;
|
||||
line.needSeasoning=!!val;
|
||||
}
|
||||
|
||||
function calcLineWeight(line){
|
||||
if(line.baseGram>0&&line.qty>0){
|
||||
@@ -761,7 +791,7 @@ function renderLineBuilders(){
|
||||
if(isNc){
|
||||
html+='<th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
}else{
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>操作</th>';
|
||||
}
|
||||
html+='</tr></thead><tbody>';
|
||||
/* 缓存当前餐品名列表,避免 forEach 内重复读取 localStorage */
|
||||
@@ -807,6 +837,14 @@ function renderLineBuilders(){
|
||||
/* 单价与小计金额 */
|
||||
html+='<td><input inputmode="decimal" placeholder="0" value="'+(line.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updateNewLinePrice(\''+line.id+'\',this.value)" style="width:70px;"></td>';
|
||||
html+='<td><input value="'+(line.subtotal?formatMoney(line.subtotal):'')+'" readonly style="width:80px;"></td>';
|
||||
/* cp 模式:内配调料单选(默认否);nc 模式:无此列 */
|
||||
if(!isNc){
|
||||
var ns=line.needSeasoning===true;
|
||||
html+='<td><div class="seasoning-radio-group">';
|
||||
html+='<label><input type="radio" name="ns-'+line.id+'" '+(ns?'checked':'')+' onchange="updateNewLineSeasoning(\''+line.id+'\',true)"> 是</label>';
|
||||
html+='<label><input type="radio" name="ns-'+line.id+'" '+(!ns?'checked':'')+' onchange="updateNewLineSeasoning(\''+line.id+'\',false)"> 否</label>';
|
||||
html+='</div></td>';
|
||||
}
|
||||
/* 操作 */
|
||||
html+='<td><button class="btn-danger-text" onclick="removeOrderLine(\''+line.id+'\')" '+(newOrderLines.length<=1?'disabled':'')+'>移除</button></td>';
|
||||
html+='</tr>';
|
||||
@@ -851,7 +889,9 @@ function submitCreateOrder(){
|
||||
weight:l.weight,
|
||||
price:l.price,
|
||||
subtotal:l.subtotal,
|
||||
status:'备货中'
|
||||
status:'备货中',
|
||||
/* 内配调料:cp 模式默认 false(不需要),nc 模式无此属性(回显显示 —) */
|
||||
needSeasoning:newOrderNcType==='cp'?!!l.needSeasoning:false
|
||||
};
|
||||
if(newOrderNcType==='nc'){
|
||||
base.ingredient=l.ingredient;
|
||||
@@ -937,7 +977,7 @@ function showDetail(orderNo){
|
||||
}else{
|
||||
html+='<th>菜品/食材</th>';
|
||||
}
|
||||
html+='<th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>行状态</th></tr></thead><tbody>';
|
||||
html+='<th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>行状态</th></tr></thead><tbody>';
|
||||
order.lines.forEach(function(line){
|
||||
var tagCls={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
|
||||
html+='<tr><td>'+ncTypeTag(line.ncType)+'</td>';
|
||||
@@ -946,12 +986,12 @@ function showDetail(orderNo){
|
||||
}else{
|
||||
html+='<td'+(isNcOrder?' colspan="2"':'')+'><b>'+line.foodName+'</b></td>';
|
||||
}
|
||||
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td><td><span class="tag '+tagCls[line.status]+'">'+line.status+'</span></td></tr>';
|
||||
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td><td>'+seasoningTag(line)+'</td><td><span class="tag '+tagCls[line.status]+'">'+line.status+'</span></td></tr>';
|
||||
/* 餐品净菜包显示食材明细 */
|
||||
if(line.ncType==='cp'){
|
||||
var ings=dishIngredients[line.foodName]||[];
|
||||
if(ings.length>0){
|
||||
var colSpan=isNcOrder?9:8;
|
||||
var colSpan=isNcOrder?10:9;
|
||||
html+='<tr><td colspan="'+colSpan+'" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';
|
||||
ings.forEach(function(ing,i){
|
||||
if(i>0)html+=', ';
|
||||
@@ -1082,12 +1122,57 @@ function onRecipeDateChange(){
|
||||
rows+='<tr>'
|
||||
+'<td style="font-weight:600;">'+MEAL_NAMES[mi]+'</td>'
|
||||
+'<td>'+dishHtml+'</td>'
|
||||
+'<td><input type="text" inputmode="numeric" class="meal-diner-input" data-meal-idx="'+mi+'" placeholder="请输入人数" oninput="sanitizeIntegerInput(this)" style="width:110px;height:30px;border:1px solid var(--border);border-radius:4px;padding:0 8px;font-size:var(--font-sm);outline:none;"></td>'
|
||||
+'<td style="text-align:center;"><input type="checkbox" class="meal-cb" value="'+mi+'"></td>'
|
||||
+'<td><input type="text" inputmode="numeric" class="meal-diner-input" data-meal-idx="'+mi+'" placeholder="请输入人数" oninput="sanitizeIntegerInput(this);renderDishSeasoningTable()" style="width:110px;height:30px;border:1px solid var(--border);border-radius:4px;padding:0 8px;font-size:var(--font-sm);outline:none;"></td>'
|
||||
+'<td style="text-align:center;"><input type="checkbox" class="meal-cb" value="'+mi+'" onchange="renderDishSeasoningTable()"></td>'
|
||||
+'</tr>';
|
||||
}
|
||||
tbody.innerHTML=rows;
|
||||
dailySection.style.display='block';
|
||||
renderDishSeasoningTable();
|
||||
}
|
||||
|
||||
/* ========== 餐品调料配置表(食谱快速创建专用) ========== */
|
||||
/* recipeSeasoningMap: { 菜名: true|false },跨餐次重复菜品自动合并,状态在重渲时保留 */
|
||||
var recipeSeasoningMap={};
|
||||
function renderDishSeasoningTable(){
|
||||
var section=document.getElementById('dishSeasoningSection');
|
||||
var tbody=document.getElementById('dishSeasoningBody');
|
||||
if(!section||!tbody)return;
|
||||
var dateVal=document.getElementById('recipeDate').value;
|
||||
var daily=dateVal?getDailyMenu(dateVal):null;
|
||||
if(!daily){section.style.display='none';tbody.innerHTML='';return;}
|
||||
/* 收集所有已勾选餐次的菜品,按菜名去重 */
|
||||
var checkedMealIndices=[];
|
||||
document.querySelectorAll('#dailyMenuBody .meal-cb:checked').forEach(function(cb){checkedMealIndices.push(parseInt(cb.value));});
|
||||
var dishSet={};
|
||||
var orderedNames=[];
|
||||
checkedMealIndices.forEach(function(mi){
|
||||
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
|
||||
dayDishes.forEach(function(name){
|
||||
if(!dishSet[name]){dishSet[name]=true;orderedNames.push(name);}
|
||||
});
|
||||
});
|
||||
if(orderedNames.length===0){
|
||||
section.style.display='none';
|
||||
tbody.innerHTML='';
|
||||
return;
|
||||
}
|
||||
section.style.display='block';
|
||||
var html='';
|
||||
orderedNames.forEach(function(name){
|
||||
/* 未配置过的菜品默认 false(不需要);已配置的保留 */
|
||||
if(typeof recipeSeasoningMap[name]!=='boolean')recipeSeasoningMap[name]=false;
|
||||
var ns=recipeSeasoningMap[name]===true;
|
||||
html+='<tr><td style="font-weight:500;">'+name+'</td>';
|
||||
html+='<td><div class="seasoning-radio-group">';
|
||||
html+='<label><input type="radio" name="rs-'+encodeURIComponent(name)+'" '+(ns?'checked':'')+' onchange="updateRecipeSeasoning(\''+name.replace(/'/g,'\\\'')+'\',true)"> 是</label>';
|
||||
html+='<label><input type="radio" name="rs-'+encodeURIComponent(name)+'" '+(!ns?'checked':'')+' onchange="updateRecipeSeasoning(\''+name.replace(/'/g,'\\\'')+'\',false)"> 否</label>';
|
||||
html+='</div></td></tr>';
|
||||
});
|
||||
tbody.innerHTML=html;
|
||||
}
|
||||
function updateRecipeSeasoning(name,val){
|
||||
recipeSeasoningMap[name]=!!val;
|
||||
}
|
||||
|
||||
/* 校验所选餐次是否都已填写就餐人数,并返回缺失餐次名 */
|
||||
@@ -1141,7 +1226,8 @@ function submitRecipeOrder(){
|
||||
portion:sp.portion,baseGram:baseGram,unit:sp.unit,
|
||||
qty:servings,
|
||||
weight:parseFloat((baseGram*servings/1000).toFixed(2)),
|
||||
status:'生成中'
|
||||
status:'生成中',
|
||||
needSeasoning:recipeSeasoningMap[name]===true
|
||||
});
|
||||
di++;
|
||||
}
|
||||
@@ -1167,6 +1253,7 @@ function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
|
||||
document.getElementById('recipeDate').value=getDefaultDate();
|
||||
document.getElementById('recipeDeliveryTime').value=getDefaultDeliveryTime();
|
||||
document.getElementById('dailyMenuSection').style.display='none';
|
||||
recipeSeasoningMap={};
|
||||
onRecipeDateChange();
|
||||
}
|
||||
document.getElementById(id).classList.add('show');
|
||||
|
||||
@@ -153,6 +153,10 @@ textarea.form-control{height:auto;padding:8px 12px;resize:vertical;min-height:60
|
||||
/* 规格连体组件:左 input 克重 + 右 select 单位/份型 */
|
||||
.line-builder-table .spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.line-builder-table .spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
/* 内配调料单选组:重置 radio 宽高,避免被表格 input 规则拉伸 */
|
||||
.seasoning-radio-group{display:inline-flex;align-items:center;gap:12px;white-space:nowrap;}
|
||||
.seasoning-radio-group label{display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);}
|
||||
.seasoning-radio-group input[type="radio"]{width:auto;height:auto;margin:0;cursor:pointer;accent-color:var(--primary);}
|
||||
.line-builder-table .spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
@@ -377,35 +381,35 @@ var dishIngsExt={
|
||||
var extOrders=[
|
||||
{orderNo:'EXT-001',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 08:00',deliverTime:'2026-04-22 16:00',logistics:'SF1234567890',
|
||||
lines:[
|
||||
{id:'L1',ingredient:'土豆',ncName:'土豆丝',spec:'5kg/箱',qty:10,weight:50,price:3.5,subtotal:175,status:'已完成'},
|
||||
{id:'L2',ingredient:'土豆',ncName:'土豆块',spec:'5kg/箱',qty:6,weight:30,price:3.5,subtotal:105,status:'已完成'}
|
||||
{id:'L1',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'5kg/箱',qty:10,weight:50,price:3.5,subtotal:175,status:'已完成'},
|
||||
{id:'L2',ncType:'nc',ingredient:'土豆',ncName:'土豆块',spec:'5kg/箱',qty:6,weight:30,price:3.5,subtotal:105,status:'已完成'}
|
||||
]},
|
||||
{orderNo:'EXT-002',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 09:30',deliverTime:'2026-04-22 14:00',logistics:'SF9876543210',
|
||||
lines:[
|
||||
{id:'L3',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:10,weight:10,price:28,subtotal:280,status:'配送中'},
|
||||
{id:'L4',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g)',qty:40,weight:20,price:12,subtotal:240,status:'配送中'}
|
||||
{id:'L3',ncType:'nc',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:10,weight:10,price:28,subtotal:280,status:'配送中'},
|
||||
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:40,weight:20,price:12,subtotal:240,status:'配送中',needSeasoning:true}
|
||||
]},
|
||||
{orderNo:'EXT-003',ncType:'nc',customer:'学校食堂',orderTime:'2026-04-22 10:00',deliverTime:'2026-04-23 09:00',logistics:'—',
|
||||
lines:[
|
||||
{id:'L5',ingredient:'鸡肉',ncName:'鸡肉块',spec:'1kg/箱',qty:60,weight:60,price:20,subtotal:1200,status:'待配送'},
|
||||
{id:'L6',ingredient:'西兰花',ncName:'西兰花朵',spec:'5kg/箱',qty:8,weight:40,price:4.5,subtotal:180,status:'待配送'}
|
||||
{id:'L5',ncType:'nc',ingredient:'鸡肉',ncName:'鸡肉块',spec:'1kg/箱',qty:60,weight:60,price:20,subtotal:1200,status:'待配送'},
|
||||
{id:'L6',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'5kg/箱',qty:8,weight:40,price:4.5,subtotal:180,status:'待配送'}
|
||||
]},
|
||||
{orderNo:'EXT-004',ncType:'nc',customer:'职工家属区团购',orderTime:'2026-04-22 10:30',deliverTime:'2026-04-23 16:00',logistics:'—',
|
||||
lines:[
|
||||
{id:'L7',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g)',qty:20,weight:12,price:15,subtotal:180,status:'备货中'},
|
||||
{id:'L8',ingredient:'蒜蓉菠菜',ncName:'',spec:'标准份(400g)',qty:30,weight:12,price:8,subtotal:96,status:'备货中'},
|
||||
{id:'L9',ingredient:'胡萝卜',ncName:'胡萝卜条',spec:'2kg/箱',qty:15,weight:30,price:4,subtotal:120,status:'备货中'}
|
||||
{id:'L7',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g)',qty:20,weight:12,price:15,subtotal:180,status:'备货中',needSeasoning:false},
|
||||
{id:'L8',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g)',qty:30,weight:12,price:8,subtotal:96,status:'备货中',needSeasoning:true},
|
||||
{id:'L9',ncType:'nc',ingredient:'胡萝卜',ncName:'胡萝卜条',spec:'2kg/箱',qty:15,weight:30,price:4,subtotal:120,status:'备货中'}
|
||||
]},
|
||||
{orderNo:'EXT-005',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 11:00',deliverTime:'2026-04-23 10:00',logistics:'—',
|
||||
lines:[
|
||||
{id:'L10',ingredient:'菠菜',ncName:'菠菜段',spec:'3kg/箱',qty:12,weight:36,price:5,subtotal:180,status:'已完成'},
|
||||
{id:'L11',ingredient:'炒胡萝卜',ncName:'',spec:'标准份(300g)',qty:25,weight:7.5,price:18,subtotal:135,status:'配送中'}
|
||||
{id:'L10',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'3kg/箱',qty:12,weight:36,price:5,subtotal:180,status:'已完成'},
|
||||
{id:'L11',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g)',qty:25,weight:7.5,price:18,subtotal:135,status:'配送中',needSeasoning:true}
|
||||
]},
|
||||
{orderNo:'EXT-006',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 12:00',deliverTime:'2026-04-23 12:00',logistics:'SF5566778899',
|
||||
lines:[
|
||||
{id:'L12',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:15,weight:15,price:28,subtotal:420,status:'已完成'},
|
||||
{id:'L13',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g)',qty:50,weight:25,price:12,subtotal:300,status:'已完成'},
|
||||
{id:'L14',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g)',qty:30,weight:18,price:15,subtotal:270,status:'已完成'}
|
||||
{id:'L12',ncType:'nc',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:15,weight:15,price:28,subtotal:420,status:'已完成'},
|
||||
{id:'L13',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:50,weight:25,price:12,subtotal:300,status:'已完成',needSeasoning:true},
|
||||
{id:'L14',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g)',qty:30,weight:18,price:15,subtotal:270,status:'已完成',needSeasoning:false}
|
||||
]}
|
||||
];
|
||||
/* 计算订单总金额:Σ(各明细小计) */
|
||||
@@ -421,6 +425,13 @@ function aggregateStatus(lines){var ss=lines.map(function(l){return l.status;});
|
||||
return '部分完成';}
|
||||
function statusTag(s){var m={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray','部分完成':'tag-purple'};return '<span class="tag '+m[s]+'">'+s+'</span>';}
|
||||
function ncTypeTag(t){return t==='nc'?'<span class="tag tag-green">🥬 净菜包</span>':'<span class="tag tag-orange">🍱 餐品净菜包</span>';}
|
||||
/* 内配调料徽标:cp 行显示「是/否」,nc 行显示 — */
|
||||
function seasoningTagExt(l){
|
||||
if(l.ncType!=='cp')return '<span style="color:var(--text-placeholder);">—</span>';
|
||||
return l.needSeasoning
|
||||
?'<span class="tag tag-blue">✓ 是</span>'
|
||||
:'<span class="tag tag-gray">✗ 否</span>';
|
||||
}
|
||||
|
||||
function renderExtTable(){
|
||||
var tb=document.getElementById('extOrderTableBody');var h='';
|
||||
@@ -438,28 +449,22 @@ function renderExtTable(){
|
||||
else if(agg==='配送中')abtns='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+o.orderNo+'\',\'receive\')">确认收货</span> ';
|
||||
h+='<td class="td-actions">'+abtns+'<span class="link" onclick="event.stopPropagation();showDetailExt(\''+o.orderNo+'\')">详情</span> <span class="link" style="color:var(--danger)" onclick="event.stopPropagation();deleteExtOrder(\''+o.orderNo+'\')">删除</span></td>';
|
||||
h+='</tr>';
|
||||
/* 子表头 — 根据 ncType 动态显示列 */
|
||||
if(o.ncType==='nc'){
|
||||
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td></td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td></tr>';
|
||||
o.lines.forEach(function(l){
|
||||
h+='<tr class="detail-row" id="edetail-'+oi+'" style="display:none;">';
|
||||
h+='<td></td><td style="font-weight:500;">'+l.ingredient+'</td><td style="font-weight:500;">'+(l.ncName||'—')+'</td><td></td>';
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td>';
|
||||
h+='<td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td>';
|
||||
h+='<td></td><td></td><td></td>';
|
||||
h+='<td class="td-actions"></td></tr>';
|
||||
});
|
||||
}else{
|
||||
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td colspan="3">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td></tr>';
|
||||
o.lines.forEach(function(l){
|
||||
h+='<tr class="detail-row" id="edetail-'+oi+'" style="display:none;">';
|
||||
h+='<td></td><td colspan="3" style="font-weight:500;">'+l.foodName+'</td>';
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td>';
|
||||
h+='<td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td>';
|
||||
h+='<td></td><td></td><td></td>';
|
||||
h+='<td class="td-actions"></td></tr>';
|
||||
});
|
||||
}
|
||||
/* 子表头 — 按 nc/cp 显示列结构(含内配调料列,统一样式) */
|
||||
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td colspan="3">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td></tr>';
|
||||
o.lines.forEach(function(l){
|
||||
h+='<tr class="detail-row" id="edetail-'+oi+'" style="display:none;">';
|
||||
h+='<td></td>';
|
||||
if(l.ncType==='nc'){
|
||||
h+='<td style="font-weight:500;">'+(l.ingredient||'—')+'</td><td style="font-weight:500;">'+(l.ncName||'—')+'</td><td></td>';
|
||||
}else{
|
||||
h+='<td colspan="3" style="font-weight:500;">'+(l.foodName||'—')+'</td>';
|
||||
}
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td>';
|
||||
h+='<td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td>';
|
||||
h+='<td>'+seasoningTagExt(l)+'</td>';
|
||||
h+='<td></td><td></td><td></td>';
|
||||
h+='<td class="td-actions"></td></tr>';
|
||||
});
|
||||
});
|
||||
tb.innerHTML=h;
|
||||
}
|
||||
@@ -498,7 +503,8 @@ var newExtOrderType='nc';
|
||||
function updExtOrderType(v){newExtOrderType=v;renderExtLineBuilders();}
|
||||
var newExtLines=[];var extLineCnt=0;
|
||||
function resetExtForm(){newExtLines=[];extLineCnt=0;newExtOrderType=document.getElementById('extOrderNcType').value;document.getElementById('extCustomer').value='';document.getElementById('extContact').value='';document.getElementById('extPhone').value='';document.getElementById('extDeliverTime').value='';document.getElementById('extAddress').value='';document.getElementById('extRemark').value='';document.getElementById('extLinesContainer').innerHTML='';var taEl=document.getElementById('extTotalAmount');if(taEl)taEl.textContent=formatMoney(0);addExtLine();}
|
||||
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:newExtOrderType==='nc'?'箱':'份',qty:0,weight:0,price:0,subtotal:0});renderExtLineBuilders();}
|
||||
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:newExtOrderType==='nc'?'箱':'份',qty:0,weight:0,price:0,subtotal:0,needSeasoning:false});renderExtLineBuilders();}
|
||||
function updELSeasoning(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.needSeasoning=!!v;}
|
||||
function removeExtLine(lid){if(newExtLines.length<=1){alert('至少保留一条');return;}newExtLines=newExtLines.filter(function(l){return l.id!==lid;});renderExtLineBuilders();}
|
||||
|
||||
function updELIngredient(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.ingredient=v;l.ncType='';renderExtLineBuilders();}
|
||||
@@ -544,7 +550,7 @@ function renderExtLineBuilders(){
|
||||
if(isNc){
|
||||
h+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
}else{
|
||||
h+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
h+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>操作</th>';
|
||||
}
|
||||
h+='</tr></thead><tbody>';
|
||||
var dishNames=getDishNamesWithFallback();
|
||||
@@ -581,6 +587,14 @@ function renderExtLineBuilders(){
|
||||
h+='<td><input value="'+(l.weight||'')+'" readonly></td>';
|
||||
h+='<td><input inputmode="decimal" placeholder="0" value="'+(l.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updELPrice(\''+l.id+'\',this.value)"></td>';
|
||||
h+='<td><input value="'+(l.subtotal?formatMoney(l.subtotal):'')+'" readonly></td>';
|
||||
/* cp 模式:内配调料单选(默认否);nc 模式无此列 */
|
||||
if(!isNc){
|
||||
var ns=l.needSeasoning===true;
|
||||
h+='<td><div class="seasoning-radio-group">';
|
||||
h+='<label><input type="radio" name="ens-'+l.id+'" '+(ns?'checked':'')+' onchange="updELSeasoning(\''+l.id+'\',true)"> 是</label>';
|
||||
h+='<label><input type="radio" name="ens-'+l.id+'" '+(!ns?'checked':'')+' onchange="updELSeasoning(\''+l.id+'\',false)"> 否</label>';
|
||||
h+='</div></td>';
|
||||
}
|
||||
h+='<td><button class="btn-remove" onclick="removeExtLine(\''+l.id+'\')">移除</button></td>';
|
||||
h+='</tr>';
|
||||
});
|
||||
@@ -605,9 +619,9 @@ function submitExtOrder(){
|
||||
var newLines=vl.map(function(l,i){
|
||||
var specLbl=formatSpecLabelExt(l);
|
||||
if(newExtOrderType==='nc'){
|
||||
return {id:'L'+(Date.now()+i),ingredient:l.ingredient,ncName:l.ncType,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i),ncType:'nc',ingredient:l.ingredient,ncName:l.ncType,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中'};
|
||||
}
|
||||
return {id:'L'+(Date.now()+i),foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i),ncType:'cp',foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中',needSeasoning:!!l.needSeasoning};
|
||||
});
|
||||
extOrders.unshift({orderNo:no,customer:cust,orderTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:dt,logistics:'—',
|
||||
ncType:newExtOrderType,lines:newLines});
|
||||
@@ -634,20 +648,17 @@ function showDetailExt(no){
|
||||
h+='<div class="drawer-field"><div class="drawer-field__label">物流单号</div><div class="drawer-field__value">'+o.logistics+'</div></div>';
|
||||
h+='</div></div>';
|
||||
h+='<div class="drawer-section"><div class="drawer-section__title">订单明细('+o.lines.length+' 道)</div>';
|
||||
if(o.ncType==='nc'){
|
||||
h+='<table class="ing-table"><thead><tr><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
|
||||
o.lines.forEach(function(l){
|
||||
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
|
||||
h+='<tr><td style="font-weight:600;">'+l.ingredient+'</td><td style="font-weight:600;">'+(l.ncName||'—')+'</td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
|
||||
});
|
||||
}else{
|
||||
h+='<table class="ing-table"><thead><tr><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
|
||||
o.lines.forEach(function(l){
|
||||
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
|
||||
h+='<tr><td style="font-weight:600;"><b>'+l.foodName+'</b></td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
|
||||
var ings=dishIngsExt[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="7" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}
|
||||
});
|
||||
}
|
||||
h+='<table class="ing-table"><thead><tr><th>净菜类型</th><th>菜品/食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>状态</th></tr></thead><tbody>';
|
||||
o.lines.forEach(function(l){
|
||||
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
|
||||
if(l.ncType==='nc'){
|
||||
h+='<tr><td>'+ncTypeTag('nc')+'</td><td style="font-weight:600;">'+(l.ingredient||'—')+'</td><td style="font-weight:600;">'+(l.ncName||'—')+'</td>';
|
||||
}else{
|
||||
h+='<tr><td>'+ncTypeTag('cp')+'</td><td style="font-weight:600;"><b>'+(l.foodName||'—')+'</b></td><td>—</td>';
|
||||
}
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td>'+seasoningTagExt(l)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
|
||||
if(l.ncType==='cp'){var ings=dishIngsExt[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="10" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}}
|
||||
});
|
||||
h+='</tbody></table></div>';
|
||||
var dh='<div class="drawer-overlay" onclick="if(event.target===this)closeDrawer()"><div class="drawer"><div class="drawer__header"><div class="drawer__title">外售详情 — '+no+'</div><button class="drawer__close" onclick="closeDrawer()">✕</button></div><div class="drawer__body">'+h+'</div></div></div>';
|
||||
document.body.insertAdjacentHTML('beforeend',dh);
|
||||
|
||||
@@ -154,6 +154,10 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
|
||||
.inline-line-table .spec-input-group{display:flex;align-items:center;gap:0;}
|
||||
.inline-line-table .spec-input-group input{flex:1;min-width:50px;border-radius:4px 0 0 4px;}
|
||||
.inline-line-table .spec-input-group select{width:90px;min-width:60px;max-width:110px;border-radius:0 4px 4px 0;margin-left:-1px;}
|
||||
/* 内配调料单选组:重置 radio 宽高,避免被表格 input 规则拉伸 */
|
||||
.seasoning-radio-group{display:inline-flex;align-items:center;gap:12px;white-space:nowrap;}
|
||||
.seasoning-radio-group label{display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);}
|
||||
.seasoning-radio-group input[type="radio"]{width:auto;height:auto;margin:0;cursor:pointer;accent-color:var(--primary);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -401,29 +405,29 @@ var dishIngredients={
|
||||
var claims=[
|
||||
{claimNo:'INT-001',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 09:00',deliverPerson:'李配送',
|
||||
lines:[
|
||||
{id:'L1',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
|
||||
{id:'L2',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
|
||||
{id:'L1',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
|
||||
{id:'L2',ncType:'nc',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
|
||||
]},
|
||||
{claimNo:'INT-002',canteen:'二号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:00',deliverPerson:'王配送',
|
||||
lines:[
|
||||
{id:'L3',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:25,subtotal:1000,status:'配送中'},
|
||||
{id:'L4',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:30,weight:15,price:3.5,subtotal:52.5,status:'配送中'}
|
||||
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:25,subtotal:1000,status:'配送中'},
|
||||
{id:'L4',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:30,weight:15,price:3.5,subtotal:52.5,status:'配送中'}
|
||||
]},
|
||||
{claimNo:'INT-003',canteen:'三号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:30',deliverTime:'2026-04-22 10:30',deliverPerson:'—',
|
||||
lines:[
|
||||
{id:'L5',ingredient:'草鱼',ncName:'鱼片',spec:'2kg/盘',qty:20,weight:40,price:18,subtotal:720,status:'待配送'},
|
||||
{id:'L6',ingredient:'鸡胸肉',ncName:'鸡胸肉丝',spec:'500g/袋',qty:15,weight:7.5,price:20,subtotal:150,status:'待配送'}
|
||||
{id:'L5',ncType:'nc',ingredient:'草鱼',ncName:'鱼片',spec:'2kg/盘',qty:20,weight:40,price:18,subtotal:720,status:'待配送'},
|
||||
{id:'L6',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丝',spec:'500g/袋',qty:15,weight:7.5,price:20,subtotal:150,status:'待配送'}
|
||||
]},
|
||||
{claimNo:'INT-004',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:30',deliverPerson:'—',
|
||||
lines:[
|
||||
{id:'L7',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:3.5,subtotal:70,status:'备货中'},
|
||||
{id:'L8',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:25,weight:12.5,price:5,subtotal:62.5,status:'备货中'},
|
||||
{id:'L9',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:6,subtotal:90,status:'备货中'}
|
||||
{id:'L7',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:3.5,subtotal:70,status:'备货中'},
|
||||
{id:'L8',ncType:'nc',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:25,weight:12.5,price:5,subtotal:62.5,status:'备货中'},
|
||||
{id:'L9',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:6,subtotal:90,status:'备货中'}
|
||||
]},
|
||||
{claimNo:'INT-005',canteen:'二号食堂厨房',ncType:'cp',claimTime:'2026-04-22 09:30',deliverTime:'2026-04-22 12:00',deliverPerson:'—',
|
||||
lines:[
|
||||
{id:'L10',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:20,weight:10,price:12,subtotal:120,status:'已完成'},
|
||||
{id:'L11',foodName:'炒胡萝卜',spec:'标准份(300g)',qty:15,weight:4.5,price:18,subtotal:81,status:'配送中'}
|
||||
{id:'L10',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g)',qty:20,weight:10,price:12,subtotal:120,status:'已完成',needSeasoning:true},
|
||||
{id:'L11',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g)',qty:15,weight:4.5,price:18,subtotal:81,status:'配送中',needSeasoning:false}
|
||||
]}
|
||||
];
|
||||
/* 计算申领单总金额:Σ(各明细小计) */
|
||||
@@ -441,6 +445,13 @@ function aggregateStatus(lines){
|
||||
}
|
||||
function statusTag(s){var m={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray','部分完成':'tag-purple'};return '<span class="tag '+m[s]+'">'+s+'</span>';}
|
||||
function ncTypeTag(t){return t==='nc'?'<span class="tag tag-green">🥬 净菜包</span>':'<span class="tag tag-orange">🍱 餐品净菜包</span>';}
|
||||
/* 内配调料徽标:cp 行显示「是/否」,nc 行显示 — */
|
||||
function seasoningTagInt(l){
|
||||
if(l.ncType!=='cp')return '<span style="color:var(--text-placeholder);">—</span>';
|
||||
return l.needSeasoning
|
||||
?'<span class="tag tag-blue">✓ 是</span>'
|
||||
:'<span class="tag tag-gray">✗ 否</span>';
|
||||
}
|
||||
|
||||
function renderClaimTable(){
|
||||
var tbody=document.getElementById('claimTableBody');
|
||||
@@ -459,7 +470,7 @@ function renderClaimTable(){
|
||||
if(agg==='配送中')html+='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+c.claimNo+'\',\'receive\')">确认收货</span> ';
|
||||
html+='<span class="link" onclick="event.stopPropagation();showDetail(\''+c.claimNo+'\')">详情</span> <span class="link" style="color:var(--danger)" onclick="event.stopPropagation();deleteClaim(\''+c.claimNo+'\')">删除</span>';
|
||||
html+='</td></tr>';
|
||||
/* 明细子表头 — 根据 ncType 动态调整列 */
|
||||
/* 明细子表头 — 统一表头(含内配调料列) */
|
||||
var isNc=c.ncType==='nc';
|
||||
html+='<tr class="detail-header" id="chead-'+ci+'" style="display:none;"><td></td>';
|
||||
if(isNc){
|
||||
@@ -467,16 +478,17 @@ function renderClaimTable(){
|
||||
}else{
|
||||
html+='<td colspan="2">菜品/食材</td>';
|
||||
}
|
||||
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||||
c.lines.forEach(function(line){
|
||||
html+='<tr class="detail-row" id="cdetail-'+ci+'" style="display:none;">';
|
||||
html+='<td></td>';
|
||||
if(isNc){
|
||||
html+='<td style="font-weight:500;">'+line.ingredient+'</td><td>'+line.ncName+'</td>';
|
||||
if(line.ncType==='nc'){
|
||||
html+='<td style="font-weight:500;">'+(line.ingredient||'—')+'</td><td>'+(line.ncName||'—')+'</td>';
|
||||
}else{
|
||||
html+='<td colspan="2" style="font-weight:500;">'+line.foodName+'</td>';
|
||||
html+='<td colspan="2" style="font-weight:500;">'+(line.foodName||'—')+'</td>';
|
||||
}
|
||||
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
|
||||
html+='<td>'+seasoningTagInt(line)+'</td>';
|
||||
html+='<td></td><td></td><td></td><td></td><td class="td-actions"></td></tr>';
|
||||
});
|
||||
});
|
||||
@@ -566,9 +578,11 @@ function resetClaimForm(){
|
||||
function addClaimLine(){
|
||||
claimLineIdCnt++;
|
||||
var orderNcType=document.getElementById('claimNcType').value||'nc';
|
||||
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:orderNcType==='nc'?'盘':'份',qty:0,weight:0,price:0,subtotal:0});
|
||||
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:orderNcType==='nc'?'盘':'份',qty:0,weight:0,price:0,subtotal:0,needSeasoning:false});
|
||||
renderClaimLineBuilders();
|
||||
}
|
||||
/* 切换"是否需要内配调料"(仅 cp 模式生效) */
|
||||
function updateCLSeasoning(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.needSeasoning=!!val;}
|
||||
|
||||
function removeClaimLine(lid){
|
||||
if(newClaimLines.length<=1){alert('至少保留一条菜品明细');return;}
|
||||
@@ -615,7 +629,7 @@ function renderClaimLineBuilders(){
|
||||
if(isNc){
|
||||
html+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
}else{
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
|
||||
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>操作</th>';
|
||||
}
|
||||
html+='</tr></thead><tbody>';
|
||||
var dishNames=getDishNamesWithFallback();
|
||||
@@ -657,6 +671,14 @@ function renderClaimLineBuilders(){
|
||||
/* 单价与小计金额(nc/cp 共用) */
|
||||
html+='<td><input inputmode="decimal" placeholder="0" value="'+(line.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updateCLPrice(\''+line.id+'\',this.value)"></td>';
|
||||
html+='<td><input value="'+(line.subtotal?formatMoney(line.subtotal):'')+'" readonly></td>';
|
||||
/* cp 模式:内配调料单选(默认否);nc 模式无此列 */
|
||||
if(!isNc){
|
||||
var ns=line.needSeasoning===true;
|
||||
html+='<td><div class="seasoning-radio-group">';
|
||||
html+='<label><input type="radio" name="ins-'+line.id+'" '+(ns?'checked':'')+' onchange="updateCLSeasoning(\''+line.id+'\',true)"> 是</label>';
|
||||
html+='<label><input type="radio" name="ins-'+line.id+'" '+(!ns?'checked':'')+' onchange="updateCLSeasoning(\''+line.id+'\',false)"> 否</label>';
|
||||
html+='</div></td>';
|
||||
}
|
||||
html+='<td class="td-actions"><button class="btn-danger-text" onclick="removeClaimLine(\''+line.id+'\')">移除</button></td>';
|
||||
html+='</tr>';
|
||||
});
|
||||
@@ -687,9 +709,9 @@ function submitClaim(){
|
||||
lines:vl.map(function(l,i){
|
||||
var specLbl=formatSpecLabelInt(l);
|
||||
if(orderNcType==='nc'){
|
||||
return {id:'L'+(Date.now()+i), ingredient:l.ingredient, ncName:l.ncType, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i), ncType:'nc', ingredient:l.ingredient, ncName:l.ncType, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中'};
|
||||
}else{
|
||||
return {id:'L'+(Date.now()+i), foodName:l.dish, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中'};
|
||||
return {id:'L'+(Date.now()+i), ncType:'cp', foodName:l.dish, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中', needSeasoning:!!l.needSeasoning};
|
||||
}
|
||||
})});
|
||||
closeModal('modal-claim');renderClaimTable();
|
||||
@@ -720,7 +742,7 @@ function showDetail(claimNo){
|
||||
}else{
|
||||
h+='<th colspan="2">菜品/食材</th>';
|
||||
}
|
||||
h+='<th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
|
||||
h+='<th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>状态</th></tr></thead><tbody>';
|
||||
c.lines.forEach(function(l){
|
||||
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
|
||||
if(c.ncType==='nc'){
|
||||
@@ -728,8 +750,8 @@ function showDetail(claimNo){
|
||||
}else{
|
||||
h+='<tr><td colspan="2"><b>'+l.foodName+'</b></td>';
|
||||
}
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
|
||||
if(c.ncType==='cp'){var ings=dishIngredients[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="8" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}}
|
||||
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td>'+seasoningTagInt(l)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
|
||||
if(c.ncType==='cp'){var ings=dishIngredients[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="9" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}}
|
||||
});
|
||||
h+='</tbody></table></div>';
|
||||
var dh='<div class="drawer-overlay" onclick="if(event.target===this)closeDrawer()"><div class="drawer"><div class="drawer__header"><div class="drawer__title">净菜内供详情 — '+claimNo+'</div><button class="drawer__close" onclick="closeDrawer()">✕</button></div><div class="drawer__body">'+h+'</div></div></div>';
|
||||
|
||||
Reference in New Issue
Block a user