276 lines
14 KiB
React
276 lines
14 KiB
React
import { useState } from 'react';
|
||
import { Button, Field, Input, Select, 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 CustomersPage({ customerRows, setCustomerRows, addRechargeRecord, apiLoading, apiError, refreshApi, can = () => true, onCreateCustomer, onUpdateCustomer, onToggleCustomerStatus, onDeleteCustomer, onRechargeCustomer }) {
|
||
const [showCreateCustomer, setShowCreateCustomer] = useState(false);
|
||
const [editingCustomer, setEditingCustomer] = useState(null);
|
||
const [rechargeCustomer, setRechargeCustomer] = useState(null);
|
||
const [deleteCustomerTarget, setDeleteCustomerTarget] = useState(null);
|
||
const [statusCustomerTarget, setStatusCustomerTarget] = useState(null);
|
||
const [newCustomer, setNewCustomer] = useState({ name: '', contact: '', phone: '', email: '' });
|
||
const [editCustomerForm, setEditCustomerForm] = useState({ name: '', contact: '', phone: '', email: '' });
|
||
const [rechargeForm, setRechargeForm] = useState({ amount: '', remark: '' });
|
||
const [actionError, setActionError] = useState('');
|
||
const canManageCustomers = can('customers.manage');
|
||
const canManageRecharges = can('recharges.manage');
|
||
const openEditCustomer = (customer) => {
|
||
setEditingCustomer(customer);
|
||
setEditCustomerForm({
|
||
name: customer.name || '',
|
||
contact: customer.contact === '-' ? '' : customer.contact || '',
|
||
phone: customer.phone === '-' ? '' : customer.phone || '',
|
||
email: customer.email === '-' ? '' : customer.email || '',
|
||
});
|
||
};
|
||
const closeEditCustomer = () => {
|
||
setEditingCustomer(null);
|
||
setEditCustomerForm({ name: '', contact: '', phone: '', email: '' });
|
||
};
|
||
const openRechargeCustomer = (customer) => {
|
||
setRechargeCustomer(customer);
|
||
setRechargeForm({ amount: '', remark: '' });
|
||
};
|
||
const closeRechargeCustomer = () => {
|
||
setRechargeCustomer(null);
|
||
setRechargeForm({ amount: '', remark: '' });
|
||
};
|
||
const parseMoney = (value) => Number(String(value).replace(/[^\d.-]/g, '')) || 0;
|
||
const formatMoney = (value) => `¥${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||
const submitCustomer = async (event) => {
|
||
event.preventDefault();
|
||
if (!newCustomer.name.trim()) return;
|
||
setActionError('');
|
||
try {
|
||
if (onCreateCustomer) {
|
||
await onCreateCustomer({
|
||
name: newCustomer.name.trim(),
|
||
contactName: newCustomer.contact.trim() || undefined,
|
||
phone: newCustomer.phone.trim() || undefined,
|
||
email: newCustomer.email.trim() || undefined,
|
||
billingMode: 'PREPAID',
|
||
creditLimit: '0.000000',
|
||
minBalance: '0.000000',
|
||
});
|
||
} else {
|
||
const nextIndex = customerRows.length + 1;
|
||
setCustomerRows([...customerRows, { id: `C${String(1000 + nextIndex)}`, name: newCustomer.name.trim(), contact: newCustomer.contact.trim() || '-', phone: newCustomer.phone.trim() || '-', email: newCustomer.email.trim() || '-', domain: '-', auth: '待配置', status: '启用', balance: '¥0.00', credit: '¥0', billing: '待配置', routeGroup: '待配置', gateways: 0, createdAt: '2026-06-18' }]);
|
||
}
|
||
setNewCustomer({ name: '', contact: '', phone: '', email: '' });
|
||
setShowCreateCustomer(false);
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
const submitEditCustomer = async (event) => {
|
||
event.preventDefault();
|
||
if (!editCustomerForm.name.trim() || !editingCustomer) return;
|
||
setActionError('');
|
||
try {
|
||
if (onUpdateCustomer) {
|
||
await onUpdateCustomer(editingCustomer.id, {
|
||
name: editCustomerForm.name.trim(),
|
||
contactName: editCustomerForm.contact.trim() || null,
|
||
phone: editCustomerForm.phone.trim() || null,
|
||
email: editCustomerForm.email.trim() || null,
|
||
});
|
||
} else {
|
||
setCustomerRows(customerRows.map((customer) => (
|
||
customer.id === editingCustomer.id ? { ...customer, name: editCustomerForm.name.trim(), contact: editCustomerForm.contact.trim() || '-', phone: editCustomerForm.phone.trim() || '-', email: editCustomerForm.email.trim() || '-' } : customer
|
||
)));
|
||
}
|
||
closeEditCustomer();
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
const submitRecharge = async (event) => {
|
||
event.preventDefault();
|
||
const amount = Number(rechargeForm.amount);
|
||
if (!rechargeCustomer || !Number.isFinite(amount) || amount <= 0) return;
|
||
setActionError('');
|
||
try {
|
||
if (onRechargeCustomer) {
|
||
await onRechargeCustomer(rechargeCustomer.id, {
|
||
amount: amount.toFixed(2),
|
||
remark: rechargeForm.remark.trim() || undefined,
|
||
});
|
||
} else {
|
||
const beforeBalance = parseMoney(rechargeCustomer.balance);
|
||
const afterBalance = beforeBalance + amount;
|
||
setCustomerRows(customerRows.map((customer) => (
|
||
customer.id === rechargeCustomer.id ? { ...customer, balance: formatMoney(afterBalance) } : customer
|
||
)));
|
||
addRechargeRecord({ id: `RCG-${Date.now()}`, type: 'customer', owner: rechargeCustomer.name, amount: formatMoney(amount), beforeBalance: formatMoney(beforeBalance), afterBalance: formatMoney(afterBalance), remark: rechargeForm.remark.trim() || '-', operator: '运营管理员', time: new Date().toLocaleString('zh-CN', { hour12: false }), status: '成功' });
|
||
}
|
||
closeRechargeCustomer();
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
const deleteCustomer = async (customer) => {
|
||
setActionError('');
|
||
try {
|
||
if (onDeleteCustomer) {
|
||
await onDeleteCustomer(customer.id);
|
||
} else {
|
||
if ((customer.gateways ?? 0) > 0) {
|
||
throw new Error('该客户仍有关联客户网关,不能删除。');
|
||
}
|
||
setCustomerRows((rows) => rows.filter((item) => item.id !== customer.id));
|
||
}
|
||
setDeleteCustomerTarget(null);
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
const toggleCustomerStatus = async (customer) => {
|
||
setActionError('');
|
||
try {
|
||
if (onToggleCustomerStatus) {
|
||
await onToggleCustomerStatus(customer);
|
||
} else {
|
||
setCustomerRows((rows) => rows.map((item) => (
|
||
item.id === customer.id ? { ...item, status: item.status === '启用' ? '停用' : '启用' } : item
|
||
)));
|
||
}
|
||
setStatusCustomerTarget(null);
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<PageTitle
|
||
title="客户管理"
|
||
desc="客户开通、SIP 账号/IP 白名单、费率方案、线路组、余额授信和操作日志。"
|
||
actions={canManageCustomers ? <Button icon={<Icon type="plus" />} onClick={() => setShowCreateCustomer(true)}>新增客户</Button> : null}
|
||
/>
|
||
<ApiNotice loading={apiLoading} error={apiError || actionError} onRetry={refreshApi} />
|
||
<Toolbar>
|
||
<Field label="客户名称"><Input placeholder="搜索客户名称 / 域名" /></Field>
|
||
<Field label="状态"><Select defaultValue="all"><option value="all">全部状态</option><option>启用</option><option>停用</option></Select></Field>
|
||
<Button icon={<Icon type="search" />}>查询</Button>
|
||
</Toolbar>
|
||
<section className="master-detail">
|
||
<Panel title="客户列表" className="main-list wide-panel">
|
||
<SimpleTable
|
||
rows={customerRows}
|
||
columns={[
|
||
{ key: 'id', label: '客户 ID', width: '108px', className: 'table-cell-compact' },
|
||
{ key: 'name', label: '名称', width: '180px', className: 'table-cell-compact' },
|
||
{ key: 'balance', label: '余额' },
|
||
{ key: 'credit', label: '授信额度' },
|
||
{ key: 'status', label: '状态', status: true },
|
||
{ key: 'gateways', label: '客户网关数' },
|
||
{
|
||
key: 'actions',
|
||
label: '操作',
|
||
render: (row) => (
|
||
<div className="table-actions">
|
||
{canManageCustomers ? <Button size="sm" variant="outline" onClick={() => openEditCustomer(row)}>编辑</Button> : null}
|
||
{canManageRecharges ? <Button size="sm" variant="secondary" onClick={() => openRechargeCustomer(row)}>充值</Button> : null}
|
||
{canManageCustomers ? (
|
||
<Button size="sm" variant={row.status === '启用' ? 'ghost' : 'secondary'} onClick={() => setStatusCustomerTarget(row)}>
|
||
{row.status === '启用' ? '禁用' : '启用'}
|
||
</Button>
|
||
) : null}
|
||
{canManageCustomers ? <Button size="sm" variant="danger" onClick={() => setDeleteCustomerTarget(row)}>删除</Button> : null}
|
||
{!canManageCustomers && !canManageRecharges ? '-' : null}
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</Panel>
|
||
</section>
|
||
{showCreateCustomer ? (
|
||
<Modal title="新增客户" onClose={() => setShowCreateCustomer(false)} size="sm">
|
||
<form className="modal-form" onSubmit={submitCustomer}>
|
||
<Field label={<span>客户名称 <span className="required-star">*</span></span>}>
|
||
<Input value={newCustomer.name} onChange={(event) => setNewCustomer({ ...newCustomer, name: event.target.value })} placeholder="请输入企业名称" required />
|
||
</Field>
|
||
<Field label="联系人">
|
||
<Input value={newCustomer.contact} onChange={(event) => setNewCustomer({ ...newCustomer, contact: event.target.value })} placeholder="请输入联系人" />
|
||
</Field>
|
||
<Field label="联系电话">
|
||
<Input value={newCustomer.phone} onChange={(event) => setNewCustomer({ ...newCustomer, phone: event.target.value })} placeholder="请输入联系电话" />
|
||
</Field>
|
||
<Field label="邮箱">
|
||
<Input type="email" value={newCustomer.email} onChange={(event) => setNewCustomer({ ...newCustomer, email: event.target.value })} placeholder="请输入邮箱" />
|
||
</Field>
|
||
<div className="modal-actions">
|
||
<Button type="button" variant="outline" onClick={() => setShowCreateCustomer(false)}>取消</Button>
|
||
<Button type="submit">保存客户</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
) : null}
|
||
{rechargeCustomer ? (
|
||
<Modal title={`${rechargeCustomer.name} 充值`} onClose={closeRechargeCustomer} size="sm">
|
||
<form className="modal-form" onSubmit={submitRecharge}>
|
||
<KeyValue label="当前余额" value={rechargeCustomer.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={closeRechargeCustomer}>取消</Button>
|
||
<Button type="submit">确认充值</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
) : null}
|
||
{editingCustomer ? (
|
||
<Modal title="编辑客户" onClose={closeEditCustomer} size="sm">
|
||
<form className="modal-form" onSubmit={submitEditCustomer}>
|
||
<Field label={<span>客户名称 <span className="required-star">*</span></span>}>
|
||
<Input value={editCustomerForm.name} onChange={(event) => setEditCustomerForm({ ...editCustomerForm, name: event.target.value })} placeholder="请输入客户名称" required />
|
||
</Field>
|
||
<Field label="联系人">
|
||
<Input value={editCustomerForm.contact} onChange={(event) => setEditCustomerForm({ ...editCustomerForm, contact: event.target.value })} placeholder="请输入联系人" />
|
||
</Field>
|
||
<Field label="联系电话">
|
||
<Input value={editCustomerForm.phone} onChange={(event) => setEditCustomerForm({ ...editCustomerForm, phone: event.target.value })} placeholder="请输入联系电话" />
|
||
</Field>
|
||
<Field label="邮箱">
|
||
<Input type="email" value={editCustomerForm.email} onChange={(event) => setEditCustomerForm({ ...editCustomerForm, email: event.target.value })} placeholder="请输入邮箱" />
|
||
</Field>
|
||
<div className="modal-actions">
|
||
<Button type="button" variant="outline" onClick={closeEditCustomer}>取消</Button>
|
||
<Button type="submit">保存修改</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
) : null}
|
||
{deleteCustomerTarget ? (
|
||
<ConfirmDialog
|
||
title="删除客户确认"
|
||
confirmLabel="确认删除"
|
||
confirmVariant="danger"
|
||
onCancel={() => setDeleteCustomerTarget(null)}
|
||
onConfirm={() => void deleteCustomer(deleteCustomerTarget)}
|
||
>
|
||
<p>确认删除客户「{deleteCustomerTarget.name}」吗?删除后该客户将不再出现在客户列表中。</p>
|
||
</ConfirmDialog>
|
||
) : null}
|
||
{statusCustomerTarget ? (
|
||
<ConfirmDialog
|
||
title={`${statusCustomerTarget.status === '启用' ? '禁用' : '启用'}客户确认`}
|
||
confirmLabel={`确认${statusCustomerTarget.status === '启用' ? '禁用' : '启用'}`}
|
||
confirmVariant="primary"
|
||
onCancel={() => setStatusCustomerTarget(null)}
|
||
onConfirm={() => void toggleCustomerStatus(statusCustomerTarget)}
|
||
>
|
||
<p>确认{statusCustomerTarget.status === '启用' ? '禁用' : '启用'}客户「{statusCustomerTarget.name}」吗?</p>
|
||
</ConfirmDialog>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|