feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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 { gateways } from '../fixtures/devFixtures.js';
|
||||
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, status: '启用', 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 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: 'status', label: '状态', status: true },
|
||||
{
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user