Files
lisglosips/apps/web/src/pages/VendorsPage.jsx
T

195 lines
9.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Button, Field, Input, Textarea } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable, KeyValue } from '../components/layout.jsx';
import { explainApiError } from '../api.js';
export function VendorsPage({ vendorRows, setVendorRows, addRechargeRecord, apiLoading, apiError, refreshApi, can = () => true, onCreateVendor, onUpdateVendor, onDeleteVendor, onRechargeVendor }) {
const [showCreateVendor, setShowCreateVendor] = useState(false);
const [editingVendor, setEditingVendor] = useState(null);
const [rechargeVendor, setRechargeVendor] = useState(null);
const [deleteVendorTarget, setDeleteVendorTarget] = useState(null);
const [newVendor, setNewVendor] = useState({ name: '' });
const [editVendorForm, setEditVendorForm] = useState({ name: '' });
const [rechargeForm, setRechargeForm] = useState({ amount: '', remark: '' });
const [actionError, setActionError] = useState('');
const canManageVendors = can('vendors.manage');
const canManageRecharges = can('recharges.manage');
const parseMoney = (value) => Number(String(value).replace(/[^\d.-]/g, '')) || 0;
const formatMoney = (value) => ${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const openEditVendor = (vendor) => {
setEditingVendor(vendor);
setEditVendorForm({ name: vendor.name || '' });
};
const closeEditVendor = () => {
setEditingVendor(null);
setEditVendorForm({ name: '' });
};
const openRechargeVendor = (vendor) => {
setRechargeVendor(vendor);
setRechargeForm({ amount: '', remark: '' });
};
const closeRechargeVendor = () => {
setRechargeVendor(null);
setRechargeForm({ amount: '', remark: '' });
};
const submitVendor = async (event) => {
event.preventDefault();
if (!newVendor.name.trim()) return;
setActionError('');
try {
if (onCreateVendor) {
await onCreateVendor({ name: newVendor.name.trim(), creditLimit: '0.000000' });
} else {
const nextIndex = vendorRows.length + 1;
setVendorRows((rows) => [...rows, { id: `V${String(2000 + nextIndex)}`, name: newVendor.name.trim(), balance: '¥0.00', credit: '¥0', gateways: 0, cycle: '待配置', ratePlan: '待配置', contact: '-', createdAt: '2026-06-19' }]);
}
setNewVendor({ name: '' });
setShowCreateVendor(false);
} catch (error) {
setActionError(explainApiError(error));
}
};
const submitEditVendor = async (event) => {
event.preventDefault();
if (!editVendorForm.name.trim() || !editingVendor) return;
setActionError('');
try {
if (onUpdateVendor) {
await onUpdateVendor(editingVendor.id, { name: editVendorForm.name.trim() });
} else {
setVendorRows((rows) => rows.map((vendor) => (
vendor.id === editingVendor.id ? { ...vendor, name: editVendorForm.name.trim() } : vendor
)));
}
closeEditVendor();
} catch (error) {
setActionError(explainApiError(error));
}
};
const submitRecharge = async (event) => {
event.preventDefault();
const amount = Number(rechargeForm.amount);
if (!rechargeVendor || !Number.isFinite(amount) || amount <= 0) return;
setActionError('');
try {
if (onRechargeVendor) {
await onRechargeVendor(rechargeVendor.id, { amount: amount.toFixed(2), remark: rechargeForm.remark.trim() || undefined });
} else {
const beforeBalance = parseMoney(rechargeVendor.balance);
const afterBalance = beforeBalance + amount;
setVendorRows((rows) => rows.map((vendor) => (
vendor.id === rechargeVendor.id ? { ...vendor, balance: formatMoney(afterBalance) } : vendor
)));
addRechargeRecord({ id: `RCG-V-${Date.now()}`, type: 'vendor', owner: rechargeVendor.name, amount: formatMoney(amount), beforeBalance: formatMoney(beforeBalance), afterBalance: formatMoney(afterBalance), remark: rechargeForm.remark.trim() || '-', operator: '运营管理员', time: new Date().toLocaleString('zh-CN', { hour12: false }), status: '成功' });
}
closeRechargeVendor();
} catch (error) {
setActionError(explainApiError(error));
}
};
const deleteVendor = async (vendor) => {
setActionError('');
try {
if (onDeleteVendor) {
await onDeleteVendor(vendor.id);
} else {
if ((vendor.gateways ?? 0) > 0) {
throw new Error('该供应商仍有关联落地网关,不能删除。');
}
setVendorRows((rows) => rows.filter((item) => item.id !== vendor.id));
}
setDeleteVendorTarget(null);
} catch (error) {
setActionError(explainApiError(error));
}
};
return (
<>
<PageTitle title="供应商管理" desc="管理供应商账户、余额、授信和充值记录。" actions={canManageVendors ? <Button icon={<Icon type="plus" />} onClick={() => setShowCreateVendor(true)}>新增供应商</Button> : null} />
<ApiNotice loading={apiLoading} error={apiError || actionError} onRetry={refreshApi} />
<Toolbar>
<Field label="供应商名称"><Input placeholder="搜索供应商名称" /></Field>
<Button icon={<Icon type="search" />}>查询</Button>
</Toolbar>
<section className="master-detail">
<Panel title="供应商列表" className="wide-panel">
<SimpleTable loading={apiLoading} rows={vendorRows} columns={[
{ key: 'id', label: '供应商 ID', width: '112px', className: 'table-cell-compact' },
{ key: 'name', label: '名称', width: '168px', className: 'table-cell-compact' },
{ key: 'balance', label: '余额' },
{ key: 'credit', label: '授信额度' },
{ key: 'gateways', label: '落地网关数' },
{
key: 'actions',
label: '操作',
render: (row) => (
<div className="table-actions">
{canManageVendors ? <Button size="sm" variant="outline" onClick={() => openEditVendor(row)}>编辑</Button> : null}
{canManageRecharges ? <Button size="sm" variant="secondary" onClick={() => openRechargeVendor(row)}>充值</Button> : null}
{canManageVendors ? <Button size="sm" variant="danger" onClick={() => setDeleteVendorTarget(row)}>删除</Button> : null}
{!canManageVendors && !canManageRecharges ? '-' : null}
</div>
),
},
]} />
</Panel>
</section>
{showCreateVendor ? (
<Modal title="新增供应商" onClose={() => setShowCreateVendor(false)} size="sm">
<form className="modal-form" onSubmit={submitVendor}>
<Field label={<span>供应商名称 <span className="required-star">*</span></span>}>
<Input value={newVendor.name} onChange={(event) => setNewVendor({ ...newVendor, name: event.target.value })} placeholder="请输入供应商名称" required />
</Field>
<div className="modal-actions">
<Button type="button" variant="outline" onClick={() => setShowCreateVendor(false)}>取消</Button>
<Button type="submit">保存供应商</Button>
</div>
</form>
</Modal>
) : null}
{editingVendor ? (
<Modal title="编辑供应商" onClose={closeEditVendor} size="sm">
<form className="modal-form" onSubmit={submitEditVendor}>
<Field label={<span>供应商名称 <span className="required-star">*</span></span>}>
<Input value={editVendorForm.name} onChange={(event) => setEditVendorForm({ ...editVendorForm, name: event.target.value })} placeholder="请输入供应商名称" required />
</Field>
<div className="modal-actions">
<Button type="button" variant="outline" onClick={closeEditVendor}>取消</Button>
<Button type="submit">保存修改</Button>
</div>
</form>
</Modal>
) : null}
{rechargeVendor ? (
<Modal title={`${rechargeVendor.name} 充值`} onClose={closeRechargeVendor} size="sm">
<form className="modal-form" onSubmit={submitRecharge}>
<KeyValue label="当前余额" value={rechargeVendor.balance} />
<Field label={<span>充值金额 <span className="required-star">*</span></span>}>
<Input type="number" min="0.01" step="0.01" value={rechargeForm.amount} onChange={(event) => setRechargeForm({ ...rechargeForm, amount: event.target.value })} placeholder="请输入充值金额" required />
</Field>
<Field label="备注">
<Textarea rows="4" value={rechargeForm.remark} onChange={(event) => setRechargeForm({ ...rechargeForm, remark: event.target.value })} placeholder="请输入备注" />
</Field>
<div className="modal-actions">
<Button type="button" variant="outline" onClick={closeRechargeVendor}>取消</Button>
<Button type="submit">确认充值</Button>
</div>
</form>
</Modal>
) : null}
{deleteVendorTarget ? (
<ConfirmDialog
title="删除供应商确认"
confirmLabel="确认删除"
confirmVariant="danger"
onCancel={() => setDeleteVendorTarget(null)}
onConfirm={() => void deleteVendor(deleteVendorTarget)}
>
<p>确认删除供应商{deleteVendorTarget.name}删除后该供应商将不再出现在供应商列表中</p>
</ConfirmDialog>
) : null}
</>
);
}