fix vendor status customer toggles and cps
This commit is contained in:
@@ -398,6 +398,7 @@ export default function App() {
|
||||
can: canPermission,
|
||||
onCreateCustomer: (body) => reloadAfterMutation(() => api.createCustomer(body)),
|
||||
onUpdateCustomer: (id, body) => reloadAfterMutation(() => api.updateCustomer(id, body)),
|
||||
onToggleCustomerStatus: (row) => reloadAfterMutation(() => (row.status === '启用' ? api.disableCustomer(row.id) : api.enableCustomer(row.id))),
|
||||
onDeleteCustomer: (id) => reloadAfterMutation(() => api.deleteCustomer(id)),
|
||||
onRechargeCustomer: (id, body) => rechargeAndApply(() => api.rechargeCustomer(id, body)),
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ export const api = {
|
||||
customers: () => request('/customers'),
|
||||
createCustomer: (body) => request('/customers', { method: 'POST', body: jsonBody(body) }),
|
||||
updateCustomer: (id, body) => request(`/customers/${encodeURIComponent(id)}`, { method: 'PATCH', body: jsonBody(body) }),
|
||||
enableCustomer: (id) => request(`/customers/${encodeURIComponent(id)}/enable`, { method: 'POST' }),
|
||||
disableCustomer: (id) => request(`/customers/${encodeURIComponent(id)}/disable`, { method: 'POST' }),
|
||||
deleteCustomer: (id) => request(`/customers/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
rechargeCustomer: (id, body) =>
|
||||
request(`/customers/${encodeURIComponent(id)}/recharges`, {
|
||||
|
||||
@@ -29,15 +29,15 @@ export const sipAccounts = [
|
||||
];
|
||||
|
||||
export const vendors = [
|
||||
{ id: 'V2001', name: '供应商 A', balance: '¥42,860.00', credit: '¥300,000', gateways: 8, status: '启用', cycle: '月结', ratePlan: 'CN-Mobile-2026', contact: '陈经理', createdAt: '2026-04-02' },
|
||||
{ id: 'V2002', name: '供应商 B', balance: '¥18,200.00', credit: '¥120,000', gateways: 5, status: '启用', cycle: '周结', ratePlan: 'CN-LowCost-2026', contact: '王经理', createdAt: '2026-04-16' },
|
||||
{ id: 'V2001', name: '供应商 A', balance: '¥42,860.00', credit: '¥300,000', gateways: 8, cycle: '月结', ratePlan: 'CN-Mobile-2026', contact: '陈经理', createdAt: '2026-04-02' },
|
||||
{ id: 'V2002', name: '供应商 B', balance: '¥18,200.00', credit: '¥120,000', gateways: 5, cycle: '周结', ratePlan: 'CN-LowCost-2026', contact: '王经理', createdAt: '2026-04-16' },
|
||||
{ id: 'V2003', name: '国际供应商 C', balance: '¥8,450.00', credit: '¥80,000', gateways: 3, status: '观察', cycle: '月结', ratePlan: 'Global-Std', contact: 'Lina', createdAt: '2026-05-03' },
|
||||
];
|
||||
|
||||
export const gateways = [
|
||||
{ id: 'GW-A-01', vendor: '供应商 A', name: '华东移动主用', authMode: 'IP', ipAddress: '203.0.113.18', sipAccount: '', sipPassword: '', concurrencyLimit: 800, billingCycle: 60, cycleRate: 0.031, requestRate: '120 CPS', blockedProvinces: '新疆、西藏', callTimeLimit: '08:00-22:00', codecs: 'PCMA, PCMU', calleePrefixTransform: '13/15/18 保持原样', callerPrefixTransform: '0216001* -> 0216001*', status: '启用' },
|
||||
{ id: 'GW-A-02', vendor: '供应商 A', name: '华东联通备用', authMode: 'IP', ipAddress: '203.0.113.19', sipAccount: '', sipPassword: '', concurrencyLimit: 500, billingCycle: 30, cycleRate: 0.017, requestRate: '80 CPS', blockedProvinces: '无', callTimeLimit: '00:00-23:59', codecs: 'PCMA', calleePrefixTransform: '021 保持原样', callerPrefixTransform: '0217002* -> 0217002*', status: '启用' },
|
||||
{ id: 'GW-B-01', vendor: '供应商 B', name: '成本最低路由', authMode: 'SIP注册', ipAddress: '', sipAccount: 'vendor-b-main', sipPassword: '******', concurrencyLimit: 360, billingCycle: 6, cycleRate: 0.0028, requestRate: '60 CPS', blockedProvinces: '北京', callTimeLimit: '09:00-21:00', codecs: 'PCMA, G729', calleePrefixTransform: '0571 -> 0571', callerPrefixTransform: '0571888* -> 0571888*', status: '禁用' },
|
||||
{ id: 'GW-A-01', vendor: '供应商 A', name: '华东移动主用', authMode: 'IP', ipAddress: '203.0.113.18', sipAccount: '', sipPassword: '', concurrencyLimit: 800, billingCycle: 60, cycleRate: 0.031, requestRate: 120, blockedProvinces: '新疆、西藏', callTimeLimit: '08:00-22:00', codecs: 'PCMA, PCMU', calleePrefixTransform: '13/15/18 保持原样', callerPrefixTransform: '0216001* -> 0216001*', status: '启用' },
|
||||
{ id: 'GW-A-02', vendor: '供应商 A', name: '华东联通备用', authMode: 'IP', ipAddress: '203.0.113.19', sipAccount: '', sipPassword: '', concurrencyLimit: 500, billingCycle: 30, cycleRate: 0.017, requestRate: 80, blockedProvinces: '无', callTimeLimit: '00:00-23:59', codecs: 'PCMA', calleePrefixTransform: '021 保持原样', callerPrefixTransform: '0217002* -> 0217002*', status: '启用' },
|
||||
{ id: 'GW-B-01', vendor: '供应商 B', name: '成本最低路由', authMode: 'SIP注册', ipAddress: '', sipAccount: 'vendor-b-main', sipPassword: '******', concurrencyLimit: 360, billingCycle: 6, cycleRate: 0.0028, requestRate: 60, blockedProvinces: '北京', callTimeLimit: '09:00-21:00', codecs: 'PCMA, G729', calleePrefixTransform: '0571 -> 0571', callerPrefixTransform: '0571888* -> 0571888*', status: '禁用' },
|
||||
];
|
||||
|
||||
export const vendorLineGroups = [
|
||||
|
||||
@@ -16,7 +16,7 @@ export function ActiveCallsPage({ activeCalls, activeCallsLoading, activeCallsEr
|
||||
calleeText: call.callee || '-',
|
||||
callerIpText: call.callerIp || '-',
|
||||
landingIpText: call.landingIp || '-',
|
||||
stateText: call.state || '未知',
|
||||
stateText: call.sipState || call.sipStatusText || call.state || '未知',
|
||||
startedText: formatDateTime(call.startedAt),
|
||||
durationText: call.durationSec === null || call.durationSec === undefined ? '-' : formatDurationText(call.durationSec),
|
||||
}));
|
||||
@@ -82,7 +82,7 @@ export function ActiveCallsPage({ activeCalls, activeCallsLoading, activeCallsEr
|
||||
{ key: 'calleeText', label: '被叫' },
|
||||
{ key: 'callerIpText', label: '呼叫方 IP' },
|
||||
{ key: 'landingIpText', label: '落地 IP' },
|
||||
{ key: 'stateText', label: '状态', status: true },
|
||||
{ key: 'stateText', label: 'SIP状态', status: true },
|
||||
{ key: 'durationText', label: '持续时长' },
|
||||
{ key: 'startedText', label: '开始时间' },
|
||||
{
|
||||
|
||||
@@ -4,11 +4,12 @@ import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, Simpl
|
||||
import { gateways } from '../fixtures/devFixtures.js';
|
||||
import { explainApiError } from '../api.js';
|
||||
|
||||
export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord, apiLoading, apiError, refreshApi, can = () => true, onCreateCustomer, onUpdateCustomer, onDeleteCustomer, onRechargeCustomer }) {
|
||||
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: '' });
|
||||
@@ -125,6 +126,21 @@ export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord
|
||||
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 (
|
||||
<>
|
||||
@@ -136,7 +152,7 @@ export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord
|
||||
<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><option>停用</option></Select></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">
|
||||
@@ -157,6 +173,11 @@ export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord
|
||||
<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>
|
||||
@@ -238,6 +259,17 @@ export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord
|
||||
<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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
.filter((item) => item.start || item.end);
|
||||
return ranges.length ? ranges : [{ start: '', end: '' }];
|
||||
};
|
||||
const splitRate = (value) => {
|
||||
return String(value || '').trim();
|
||||
};
|
||||
const splitRate = (value) => String(value || '').replace(/[^\d]/g, '');
|
||||
const formatMinuteRate = (cycle, rate) => {
|
||||
const billingCycle = Number(cycle);
|
||||
const cycleRate = Number(rate);
|
||||
@@ -84,10 +82,12 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
};
|
||||
const submitGateway = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!editingGateway || !gatewayForm.name.trim() || !gatewayForm.concurrencyLimit || !gatewayForm.billingCycle || !gatewayForm.cycleRate) return;
|
||||
if (!editingGateway || !gatewayForm.name.trim() || !gatewayForm.concurrencyLimit || !gatewayForm.billingCycle || !gatewayForm.cycleRate || !gatewayForm.requestRate) return;
|
||||
const billingCycle = Number(gatewayForm.billingCycle);
|
||||
const cycleRate = Number(gatewayForm.cycleRate);
|
||||
const cpsLimit = Number(gatewayForm.requestRate);
|
||||
if (!Number.isFinite(billingCycle) || billingCycle <= 0 || billingCycle > 60 || !Number.isFinite(cycleRate) || cycleRate < 0) return;
|
||||
if (!Number.isInteger(cpsLimit) || cpsLimit < 1 || cpsLimit > 10000) return;
|
||||
const ipAddress = gatewayForm.ipAddress.trim();
|
||||
const sipAccount = gatewayForm.sipAccount.trim();
|
||||
const sipPassword = gatewayForm.sipPassword.trim();
|
||||
@@ -105,7 +105,7 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
port: editingGateway.port || 5060,
|
||||
transport: editingGateway.transport || 'udp',
|
||||
sipUsername: sipAccount || undefined,
|
||||
cpsLimit: Number(String(gatewayForm.requestRate).match(/\d+/)?.[0] || 0),
|
||||
cpsLimit,
|
||||
concurrencyLimit: Number(gatewayForm.concurrencyLimit),
|
||||
billingCycleSec: billingCycle,
|
||||
cycleRate: String(cycleRate),
|
||||
@@ -139,7 +139,7 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
concurrencyLimit: body.concurrencyLimit,
|
||||
billingCycle,
|
||||
cycleRate,
|
||||
requestRate: gatewayForm.requestRate.trim() || '-',
|
||||
requestRate: cpsLimit,
|
||||
blockedProvinces: gatewayForm.blockedProvinces.length ? gatewayForm.blockedProvinces.join('、') : '无',
|
||||
callTimeLimit: gatewayForm.forbiddenPeriods
|
||||
.filter((period) => period.start || period.end)
|
||||
@@ -240,6 +240,7 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
{ key: 'name', label: '落地网关名称', width: '168px', className: 'table-cell-compact' },
|
||||
{ key: 'authMode', label: '认证方式' },
|
||||
{ key: 'authTarget', label: 'IP/账号', render: (row) => (row.authMode === 'IP' ? row.ipAddress : row.sipAccount) },
|
||||
{ key: 'requestRate', label: 'CPS' },
|
||||
{ key: 'concurrencyLimit', label: '并发上限' },
|
||||
{ key: 'minuteRate', label: '价格', render: (row) => formatMinuteRate(row.billingCycle, row.cycleRate) },
|
||||
{ key: 'landingCalleePrefix', label: '落地被叫前缀', render: (row) => row.landingCalleePrefix || '-' },
|
||||
@@ -311,10 +312,9 @@ export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="config-card">
|
||||
<strong>请求速率</strong>
|
||||
<Input value={gatewayForm.requestRate} onChange={(event) => setGatewayForm({ ...gatewayForm, requestRate: event.target.value })} placeholder="例如 120 CPS" />
|
||||
</div>
|
||||
<Field label={<span>CPS <span className="required-star">*</span></span>}>
|
||||
<Input type="number" min="1" max="10000" step="1" value={gatewayForm.requestRate} onChange={(event) => setGatewayForm({ ...gatewayForm, requestRate: event.target.value })} placeholder="1-10000" required />
|
||||
</Field>
|
||||
<div className="config-card">
|
||||
<strong>屏蔽省份</strong>
|
||||
<div className="option-grid">
|
||||
|
||||
@@ -42,7 +42,7 @@ export function VendorsPage({ vendorRows, setVendorRows, addRechargeRecord, apiL
|
||||
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' }]);
|
||||
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);
|
||||
@@ -121,7 +121,6 @@ export function VendorsPage({ vendorRows, setVendorRows, addRechargeRecord, apiL
|
||||
{ key: 'balance', label: '余额' },
|
||||
{ key: 'credit', label: '授信额度' },
|
||||
{ key: 'gateways', label: '落地网关数' },
|
||||
{ key: 'status', label: '状态', status: true },
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
|
||||
@@ -80,7 +80,6 @@ function normalizeVendor(item) {
|
||||
balance: formatCurrency(item.balance),
|
||||
credit: formatCurrency(item.creditLimit),
|
||||
gateways: item.gatewayCount ?? 0,
|
||||
status: zhStatus(item.status),
|
||||
cycle: item.settlement || '-',
|
||||
ratePlan: '-',
|
||||
contact: item.contactName || '-',
|
||||
@@ -133,7 +132,7 @@ function normalizeVendorGateway(item) {
|
||||
concurrencyLimit: item.concurrencyLimit ?? 0,
|
||||
billingCycle: item.billingCycleSec ?? 60,
|
||||
cycleRate: Number(item.cycleRate || 0),
|
||||
requestRate: `${item.cpsLimit ?? 0} CPS`,
|
||||
requestRate: item.cpsLimit ?? 1,
|
||||
blockedProvinces: '-',
|
||||
callTimeLimit: '-',
|
||||
codecs: (item.codecs || []).map((codec) => codec.codec).join(', ') || '-',
|
||||
|
||||
Reference in New Issue
Block a user