/** * config-ui.ts 集成补丁 * * 此文件包含需要添加到 config-ui.ts 的代码片段 * 由于 config-ui.ts 文件太大(3800+ 行),这里提供独立的补丁代码 */ /* ===================================================== 1. 文件顶部导入 ===================================================== */ import { generateDashboardHtml, getDashboardCss, getDashboardJs, type DashboardData } from './dashboard-ui.js'; import { registerDashboardApiRoutes } from '../api/dashboard.js'; /* ===================================================== 2. PaymentRow 接口定义(添加到其他接口旁边) ===================================================== */ interface PaymentRow { id: string; company_id: string; direction: string; counterparty: string; amount: number; paid_amount: number; status: string; due_date: string; paid_date: string; invoice_id: string; contract_id: string; category: string; payment_method: string; notes: string; created_at: string; updated_at: string; } /* ===================================================== 3. handleCompanyDetail 函数中添加 payments 查询 ===================================================== */ // 在 handleCompanyDetail 函数的查询部分添加: const payments = db.query( `SELECT * FROM opc_payments WHERE company_id = ? ORDER BY CASE status WHEN 'overdue' THEN 0 WHEN 'pending' THEN 1 WHEN 'partial' THEN 2 ELSE 3 END, due_date ASC`, companyId, ) as PaymentRow[]; // 在返回对象中添加: return { // ... 其他字段 payments, }; /* ===================================================== 4. registerConfigUiRoutes 函数中注册 Dashboard API ===================================================== */ export function registerConfigUiRoutes(api: OpenClawPluginApi, db: OpcDatabase, gatewayToken?: string): void { // 注册 Dashboard API 路由 registerDashboardApiRoutes(api, db); // 添加收付款相关 API 端点 // GET /opc/admin/api/payments/:id api.registerHttpRoute({ path: '/opc/admin/api/payments', handler: async (req, res) => { const match = req.url?.match(/^\/opc\/admin\/api\/payments\/([^/]+)$/); if (!match) { sendJson(res, { error: 'Invalid payment ID' }, 400); return true; } const paymentId = match[1]; const payment = db.queryOne('SELECT * FROM opc_payments WHERE id = ?', paymentId); if (!payment) { sendJson(res, { error: 'Payment not found' }, 404); return true; } sendJson(res, payment); return true; }, }); // POST /opc/admin/api/payments - 创建收付款 api.registerHttpRoute({ path: '/opc/admin/api/payments', handler: async (req, res) => { if (req.method !== 'POST') return false; try { const body = await readBody(req); const data = JSON.parse(body); const id = 'PAY-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9).toUpperCase(); const now = new Date().toISOString(); db.execute( `INSERT INTO opc_payments ( id, company_id, direction, counterparty, amount, due_date, category, payment_method, notes, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, data.company_id, data.direction, data.counterparty, parseFloat(data.amount) || 0, data.due_date, data.category || '', data.payment_method || '', data.notes || '', now, now, ); sendJson(res, { ok: true, id }); return true; } catch (err) { sendJson(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500); return true; } }, }); // PUT /opc/admin/api/payments/:id - 更新收付款 api.registerHttpRoute({ path: '/opc/admin/api/payments', handler: async (req, res) => { if (req.method !== 'PUT') return false; const match = req.url?.match(/^\/opc\/admin\/api\/payments\/([^/]+)$/); if (!match) { sendJson(res, { error: 'Invalid payment ID' }, 400); return true; } const paymentId = match[1]; try { const body = await readBody(req); const data = JSON.parse(body); const now = new Date().toISOString(); db.execute( `UPDATE opc_payments SET direction = ?, counterparty = ?, amount = ?, due_date = ?, category = ?, payment_method = ?, notes = ?, updated_at = ? WHERE id = ?`, data.direction, data.counterparty, parseFloat(data.amount) || 0, data.due_date, data.category || '', data.payment_method || '', data.notes || '', now, paymentId, ); sendJson(res, { ok: true }); return true; } catch (err) { sendJson(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500); return true; } }, }); // POST /opc/admin/api/payments/:id/record - 记账 api.registerHttpRoute({ path: '/opc/admin/api/payments', handler: async (req, res) => { if (req.method !== 'POST') return false; const match = req.url?.match(/^\/opc\/admin\/api\/payments\/([^/]+)\/record$/); if (!match) { sendJson(res, { error: 'Invalid payment ID' }, 400); return true; } const paymentId = match[1]; try { const body = await readBody(req); const data = JSON.parse(body); const paidAmount = parseFloat(data.paid_amount) || 0; // 获取当前记录 const payment = db.queryOne('SELECT * FROM opc_payments WHERE id = ?', paymentId) as PaymentRow | null; if (!payment) { sendJson(res, { error: 'Payment not found' }, 404); return true; } const newPaidAmount = payment.paid_amount + paidAmount; const now = new Date().toISOString(); // 更新状态 let newStatus = payment.status; if (newPaidAmount >= payment.amount) { newStatus = 'paid'; } else if (newPaidAmount > 0) { newStatus = 'partial'; } db.execute( `UPDATE opc_payments SET paid_amount = ?, status = ?, paid_date = ?, updated_at = ? WHERE id = ?`, newPaidAmount, newStatus, now.slice(0, 10), now, paymentId, ); sendJson(res, { ok: true }); return true; } catch (err) { sendJson(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500); return true; } }, }); // ... 原有路由注册代码继续 } /* ===================================================== 5. getCss() 函数中添加 Dashboard 和 Modal 样式 ===================================================== */ // 在 getCss() 返回值末尾添加: + getDashboardCss() + "\n.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}" + "\n.modal-content{background:var(--card);border-radius:var(--r);max-width:800px;width:100%;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04)}" + "\n.modal-header{padding:20px 24px;border-bottom:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center}" + "\n.modal-header h3{font-size:18px;font-weight:600;margin:0}" + "\n.modal-close{font-size:28px;font-weight:300;line-height:1;cursor:pointer;color:var(--tx3);transition:color .15s}" + "\n.modal-close:hover{color:var(--tx)}" + "\n.modal-body{padding:24px;overflow-y:auto}" + "\n.modal-actions{display:flex;gap:12px;justify-content:flex-end;padding-top:16px;border-top:1px solid var(--bd);margin-top:16px}" + "\n.form-group{margin-bottom:16px}" + "\n.form-group label{display:block;font-size:13px;font-weight:500;margin-bottom:6px;color:var(--tx)}" + "\n.form-group input,.form-group select,.form-group textarea{width:100%;padding:9px 14px;border:1px solid var(--bd);border-radius:var(--r);font-size:13px;font-family:var(--font);background:var(--card);color:var(--tx)}" + "\n.form-group input:focus,.form-group select:focus,.form-group textarea:focus{outline:none;border-color:var(--tx3)}" + "\n.btn-link{background:none;border:none;color:var(--tx);cursor:pointer;font-size:13px;text-decoration:underline;padding:0}" + "\n.btn-link:hover{color:var(--tx2)}" + "\n.btn-primary{padding:9px 18px;background:var(--tx);color:white;border:none;border-radius:var(--r);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--font);transition:all .15s}" + "\n.btn-primary:hover{background:var(--pri-l)}" + "\n.btn-secondary{padding:9px 18px;background:var(--bg);color:var(--tx);border:1px solid var(--bd);border-radius:var(--r);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--font);transition:all .15s}" + "\n.btn-secondary:hover{background:#f3f4f6}" /* ===================================================== 6. getBodyHtml() 函数修改 ===================================================== */ // 在侧边栏部分,将原来的 monitoring 改为 dashboard-monitor: + ' 监控中心' // 在 main 区域添加新的 dashboard-monitor 视图: + '
' // 在 body 末尾添加收付款弹窗: + '' /* ===================================================== 7. getJs() 函数修改 ===================================================== */ // 修改 showView 函数,添加 dashboard-monitor 支持: + "\nfunction showView(name){currentView=name;document.querySelectorAll('.view').forEach(function(v){v.classList.remove('active')});document.querySelectorAll('.sidebar-nav a').forEach(function(a){a.classList.remove('active')});var el=document.getElementById('view-'+name);if(el)el.classList.add('active');var nav=document.querySelector('.sidebar-nav a[data-view=\"'+name+'\"]');if(nav)nav.classList.add('active');if(name==='dashboard')loadDashboard();if(name==='companies')loadCompanies();if(name==='finance')loadFinance();if(name==='monitoring')loadMonitoring();if(name==='dashboard-monitor')loadDashboardMonitor();if(name==='tools')loadConfig();if(name==='guide')loadGuide();if(name==='canvas')initCanvasView();if(name==='feishu')loadFeishu();}" // 添加 Dashboard 加载函数: + "\nfunction loadDashboardMonitor(){" + "var el=document.getElementById('dashboard-monitor-content');" + "el.innerHTML='
';" + "fetch('/opc/admin/api/dashboard').then(function(r){return r.json()}).then(function(data){" + "el.innerHTML=data.html||'

暂无数据

';" + "}).catch(function(e){el.innerHTML='

加载失败: '+e.message+'

';});}" // 添加 Dashboard 专用 JS: + getDashboardJs() // 添加收付款相关函数: + "\nfunction openPaymentModal(companyId,paymentId){" + "var modal=document.getElementById('payment-modal');" + "var form=document.getElementById('payment-form');" + "var title=document.getElementById('payment-modal-title');" + "document.getElementById('payment-company-id').value=companyId;" + "if(paymentId){" + "title.textContent='编辑收付款';" + "document.getElementById('payment-id').value=paymentId;" + "fetch('/opc/admin/api/payments/'+paymentId).then(function(r){return r.json()}).then(function(p){" + "form.direction.value=p.direction;form.counterparty.value=p.counterparty;form.amount.value=p.amount;" + "form.due_date.value=p.due_date;form.category.value=p.category||'';form.payment_method.value=p.payment_method||'';" + "form.notes.value=p.notes||'';" + "}).catch(function(e){showToast('加载失败','err');});" + "}else{" + "title.textContent='新增收付款';document.getElementById('payment-id').value='';form.reset();" + "}" + "modal.style.display='flex';}" + "\nfunction closePaymentModal(){document.getElementById('payment-modal').style.display='none';}" + "\nfunction savePayment(event){" + "event.preventDefault();var form=event.target;var data=new FormData(form);var json={};" + "data.forEach(function(value,key){json[key]=value;});" + "var url=json.payment_id?'/opc/admin/api/payments/'+json.payment_id:'/opc/admin/api/payments';" + "var method=json.payment_id?'PUT':'POST';" + "fetch(url,{method:method,headers:{'Content-Type':'application/json'},body:JSON.stringify(json)})" + ".then(function(r){return r.json()}).then(function(d){" + "if(d.ok||d.id){showToast('保存成功','ok');closePaymentModal();loadCompanyDetail(json.company_id);}" + "else{showToast('保存失败','err');}" + "}).catch(function(e){showToast('网络错误','err');});}" + "\nfunction recordPayment(paymentId){" + "var amount=prompt('请输入本次收/付款金额(元):');" + "if(!amount||isNaN(parseFloat(amount)))return;" + "fetch('/opc/admin/api/payments/'+paymentId+'/record',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paid_amount:parseFloat(amount)})})" + ".then(function(r){return r.json()}).then(function(d){" + "if(d.ok){showToast('记账成功','ok');location.reload();}" + "else{showToast('记账失败','err');}" + "}).catch(function(e){showToast('网络错误','err');});}" /* ===================================================== 8. 公司详情 Tab 渲染部分 ===================================================== */ // 在 renderCompanyDetail 的 tabNames 数组中添加: + "var tabNames=[{k:'overview',l:'概览'},{k:'finance',l:'财务'},{k:'payments',l:'收付款'},{k:'team',l:'团队'},{k:'projects',l:'项目'},{k:'contracts',l:'合同'},{k:'investment',l:'投融资'},{k:'timeline',l:'时间线'},{k:'staff',l:'AI员工'},{k:'media',l:'新媒体'},{k:'procurement',l:'采购'}];" // 在 finance tab 后添加 payments tab 渲染逻辑: + "if(activeTab==='payments'){" + "h+='
';" + "h+='
';" + "var receivableTotal=0,payableTotal=0,overdueCount=0;" + "if(d.payments){d.payments.forEach(function(p){" + "var unpaid=p.amount-p.paid_amount;" + "if(p.direction==='receivable'&&p.status!=='paid'&&p.status!=='cancelled')receivableTotal+=unpaid;" + "if(p.direction==='payable'&&p.status!=='paid'&&p.status!=='cancelled')payableTotal+=unpaid;" + "if(p.status==='overdue')overdueCount++;});}" + "h+='
待收总额
'+fmt(receivableTotal)+'
';" + "h+='
待付总额
'+fmt(payableTotal)+'
';" + "h+='
逾期笔数
'+overdueCount+'
';" + "h+='
';" + "h+='
';" + "h+='

收付款列表

';" + "h+='';" + "h+='
';" + "if(d.payments&&d.payments.length){" + "h+='';" + "d.payments.forEach(function(p){" + "var directionBadge=p.direction==='receivable'?'应收':'应付';" + "var statusBadge=p.status==='paid'?'已付':p.status==='partial'?'部分':p.status==='overdue'?'逾期':'待付';" + "h+='';" + "h+='';});" + "h+='
方向对方金额已付状态到期日类别操作
'+directionBadge+''+esc(p.counterparty)+''+fmt(p.amount)+' 元'+fmt(p.paid_amount)+' 元'+statusBadge+''+fmtDate(p.due_date)+''+esc(p.category||'--')+'
';" + "}else{h+='

暂无收付款记录

';}" + "h+='
';}" // 完成!