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

276 lines
14 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, 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}
</>
);
}