244 lines
9.5 KiB
React
244 lines
9.5 KiB
React
import { useEffect, useState } from 'react';
|
||
import { Alert, Button, Field, Input, Select, Textarea } from '../components/ui.jsx';
|
||
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, PageLoadingDialog, Modal, ConfirmDialog, SimpleTable } from '../components/layout.jsx';
|
||
import { enStatus, formatDate, zhStatus } from '../utils/formatters.js';
|
||
import { api, explainApiError } from '../api.js';
|
||
|
||
const emptyBusinessPrefixForm = { prefix: '', name: '', description: '', priority: 100, status: 'ENABLED' };
|
||
|
||
function normalizeBusinessPrefix(item) {
|
||
return {
|
||
id: item.id,
|
||
prefix: item.prefix,
|
||
name: item.name,
|
||
description: item.description || '-',
|
||
priority: item.priority ?? 100,
|
||
status: zhStatus(item.status),
|
||
gatewayCount: item.gatewayCount ?? 0,
|
||
createdAt: formatDate(item.createdAt),
|
||
};
|
||
}
|
||
|
||
export function BusinessPrefixesPage({ can = () => true }) {
|
||
const [rows, setRows] = useState([]);
|
||
const [filters, setFilters] = useState({ keyword: '', status: 'all' });
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [message, setMessage] = useState('');
|
||
const [editingPrefix, setEditingPrefix] = useState(undefined);
|
||
const [prefixForm, setPrefixForm] = useState(emptyBusinessPrefixForm);
|
||
const [statusTarget, setStatusTarget] = useState(null);
|
||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const canManage = can('customer_gateways.manage');
|
||
|
||
const loadPrefixes = async () => {
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
const payload = await api.businessPrefixes(filters);
|
||
setRows((Array.isArray(payload) ? payload : []).map(normalizeBusinessPrefix));
|
||
} catch (loadError) {
|
||
setError(explainApiError(loadError));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
void loadPrefixes();
|
||
}, []);
|
||
|
||
const openCreate = () => {
|
||
setEditingPrefix(null);
|
||
setPrefixForm(emptyBusinessPrefixForm);
|
||
setError('');
|
||
setMessage('');
|
||
};
|
||
|
||
const openEdit = (row) => {
|
||
setEditingPrefix(row);
|
||
setPrefixForm({
|
||
prefix: row.prefix,
|
||
name: row.name,
|
||
description: row.description === '-' ? '' : row.description,
|
||
priority: row.priority,
|
||
status: enStatus(row.status),
|
||
});
|
||
setError('');
|
||
setMessage('');
|
||
};
|
||
|
||
const closeForm = () => {
|
||
setEditingPrefix(undefined);
|
||
setPrefixForm(emptyBusinessPrefixForm);
|
||
setSubmitting(false);
|
||
};
|
||
|
||
const submitPrefix = async (event) => {
|
||
event.preventDefault();
|
||
setSubmitting(true);
|
||
setError('');
|
||
try {
|
||
const body = {
|
||
prefix: prefixForm.prefix.trim(),
|
||
name: prefixForm.name.trim(),
|
||
description: prefixForm.description.trim() || null,
|
||
priority: Number(prefixForm.priority),
|
||
status: prefixForm.status,
|
||
};
|
||
if (editingPrefix) {
|
||
await api.updateBusinessPrefix(editingPrefix.id, body);
|
||
setMessage(`业务前缀「${body.prefix}」已更新。`);
|
||
} else {
|
||
await api.createBusinessPrefix(body);
|
||
setMessage(`业务前缀「${body.prefix}」已创建。`);
|
||
}
|
||
closeForm();
|
||
await loadPrefixes();
|
||
} catch (submitError) {
|
||
setError(explainApiError(submitError));
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const toggleStatus = async (row) => {
|
||
setSubmitting(true);
|
||
setError('');
|
||
try {
|
||
if (row.status === '启用') {
|
||
await api.disableBusinessPrefix(row.id);
|
||
} else {
|
||
await api.enableBusinessPrefix(row.id);
|
||
}
|
||
setStatusTarget(null);
|
||
await loadPrefixes();
|
||
} catch (toggleError) {
|
||
setError(explainApiError(toggleError));
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const deletePrefix = async (row) => {
|
||
setSubmitting(true);
|
||
setError('');
|
||
try {
|
||
await api.deleteBusinessPrefix(row.id);
|
||
setDeleteTarget(null);
|
||
await loadPrefixes();
|
||
} catch (deleteError) {
|
||
setError(explainApiError(deleteError));
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const formOpen = editingPrefix !== undefined;
|
||
|
||
return (
|
||
<>
|
||
<PageTitle
|
||
title="业务前缀管理"
|
||
desc="维护客户呼入被叫号码前置的业务标识,后续客户网关按 IP、主叫规则和业务前缀识别归属。"
|
||
actions={canManage ? <Button icon={<Icon type="plus" />} onClick={openCreate}>新增业务前缀</Button> : null}
|
||
/>
|
||
<ApiNotice loading={loading} error={error} onRetry={loadPrefixes} />
|
||
<PageLoadingDialog loading={loading} />
|
||
{message ? <Alert title="操作完成" tone="success">{message}</Alert> : null}
|
||
<Panel
|
||
title="业务前缀"
|
||
className="wide-panel"
|
||
aside={<Button size="sm" variant="outline" icon={<Icon type="reload" />} onClick={loadPrefixes}>刷新</Button>}
|
||
>
|
||
<Toolbar>
|
||
<Field label="关键字">
|
||
<Input value={filters.keyword} onChange={(event) => setFilters({ ...filters, keyword: event.target.value })} placeholder="前缀或名称" />
|
||
</Field>
|
||
<Field label="状态">
|
||
<Select value={filters.status} onChange={(event) => setFilters({ ...filters, status: event.target.value })}>
|
||
<option value="all">全部</option>
|
||
<option value="ENABLED">启用</option>
|
||
<option value="DISABLED">停用</option>
|
||
</Select>
|
||
</Field>
|
||
<Button icon={<Icon type="search" />} onClick={loadPrefixes}>查询</Button>
|
||
</Toolbar>
|
||
<SimpleTable loading={loading} rows={rows} columns={[
|
||
{ key: 'prefix', label: '业务前缀', width: '120px' },
|
||
{ key: 'name', label: '名称', width: '160px' },
|
||
{ key: 'priority', label: '优先级', width: '90px' },
|
||
{ key: 'gatewayCount', label: '使用客户网关数', width: '140px' },
|
||
{ key: 'status', label: '状态', width: '90px', status: true },
|
||
{ key: 'description', label: '备注' },
|
||
{ key: 'createdAt', label: '创建日期', width: '120px' },
|
||
{
|
||
key: 'actions',
|
||
label: '操作',
|
||
width: '220px',
|
||
render: (row) => (
|
||
<div className="table-actions">
|
||
{canManage ? <Button size="sm" variant="outline" onClick={() => openEdit(row)}>编辑</Button> : null}
|
||
{canManage ? <Button size="sm" variant={row.status === '启用' ? 'ghost' : 'secondary'} onClick={() => setStatusTarget(row)}>{row.status === '启用' ? '禁用' : '启用'}</Button> : null}
|
||
{canManage ? <Button size="sm" variant="danger" onClick={() => setDeleteTarget(row)}>删除</Button> : '-'}
|
||
</div>
|
||
),
|
||
},
|
||
]} />
|
||
</Panel>
|
||
{formOpen ? (
|
||
<Modal title={editingPrefix ? '编辑业务前缀' : '新增业务前缀'} onClose={submitting ? () => {} : closeForm} size="sm">
|
||
<form className="modal-form" onSubmit={submitPrefix}>
|
||
<Field label={<span>业务前缀 <span className="required-star">*</span></span>}>
|
||
<Input value={prefixForm.prefix} onChange={(event) => setPrefixForm({ ...prefixForm, prefix: event.target.value })} placeholder="如 671" required />
|
||
</Field>
|
||
<Field label={<span>名称 <span className="required-star">*</span></span>}>
|
||
<Input value={prefixForm.name} onChange={(event) => setPrefixForm({ ...prefixForm, name: event.target.value })} placeholder="如 国内移动业务" required />
|
||
</Field>
|
||
<Field label="优先级">
|
||
<Input type="number" min="1" max="9999" value={prefixForm.priority} onChange={(event) => setPrefixForm({ ...prefixForm, priority: event.target.value })} />
|
||
</Field>
|
||
<Field label="状态">
|
||
<Select value={prefixForm.status} onChange={(event) => setPrefixForm({ ...prefixForm, status: event.target.value })}>
|
||
<option value="ENABLED">启用</option>
|
||
<option value="DISABLED">停用</option>
|
||
</Select>
|
||
</Field>
|
||
<Field label="备注">
|
||
<Textarea rows={3} value={prefixForm.description} onChange={(event) => setPrefixForm({ ...prefixForm, description: event.target.value })} />
|
||
</Field>
|
||
<div className="modal-actions">
|
||
<Button type="button" variant="outline" disabled={submitting} onClick={closeForm}>取消</Button>
|
||
<Button type="submit" disabled={submitting}>{submitting ? '保存中' : '保存'}</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
) : null}
|
||
{statusTarget ? (
|
||
<ConfirmDialog
|
||
title={`${statusTarget.status === '启用' ? '禁用' : '启用'}业务前缀确认`}
|
||
confirmLabel={`确认${statusTarget.status === '启用' ? '禁用' : '启用'}`}
|
||
busy={submitting}
|
||
onCancel={() => setStatusTarget(null)}
|
||
onConfirm={() => void toggleStatus(statusTarget)}
|
||
>
|
||
<p>确认{statusTarget.status === '启用' ? '禁用' : '启用'}业务前缀「{statusTarget.prefix}」吗?</p>
|
||
</ConfirmDialog>
|
||
) : null}
|
||
{deleteTarget ? (
|
||
<ConfirmDialog
|
||
title="删除业务前缀确认"
|
||
confirmLabel="确认删除"
|
||
confirmVariant="danger"
|
||
busy={submitting}
|
||
onCancel={() => setDeleteTarget(null)}
|
||
onConfirm={() => void deletePrefix(deleteTarget)}
|
||
>
|
||
<p>确认删除业务前缀「{deleteTarget.prefix}」吗?删除后不能再被客户网关选择。</p>
|
||
</ConfirmDialog>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|