feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Field, Input, Select, Textarea } from '../components/ui.jsx';
|
||||
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable } from '../components/layout.jsx';
|
||||
import { enStatus } from '../utils/formatters.js';
|
||||
import { api, explainApiError } from '../api.js';
|
||||
|
||||
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} />
|
||||
{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 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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const numberLibraryTabs = [
|
||||
{ value: 'cities', label: '地级市字典' },
|
||||
{ value: 'phoneSegments', label: '手机号码库' },
|
||||
{ value: 'areaCodes', label: '城市区号' },
|
||||
{ value: 'carrierPrefixRules', label: '运营商号码段规则' },
|
||||
];
|
||||
|
||||
const numberLibraryImportExamples = {
|
||||
cities: [
|
||||
{ code: '340100', provinceCode: '340000', provinceName: '安徽省', cityName: '合肥市', cityLevel: 'PREFECTURE' },
|
||||
],
|
||||
phoneSegments: [
|
||||
{ segment7: '1380013', provinceName: '北京市', cityCode: '110100', cityName: '北京市', carrier: 'MOBILE' },
|
||||
],
|
||||
areaCodes: [
|
||||
{ areaCode: '0551', provinceName: '安徽省', cityCode: '340100', cityName: '合肥市' },
|
||||
],
|
||||
carrierPrefixRules: [
|
||||
{ prefix: '138', carrier: 'MOBILE', priority: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
const emptyNumberLibraryRows = {
|
||||
cities: [],
|
||||
phoneSegments: [],
|
||||
areaCodes: [],
|
||||
carrierPrefixRules: [],
|
||||
};
|
||||
|
||||
const emptyNumberLibraryTotals = {
|
||||
cities: 0,
|
||||
phoneSegments: 0,
|
||||
areaCodes: 0,
|
||||
carrierPrefixRules: 0,
|
||||
};
|
||||
|
||||
function normalizeNumberLibraryList(payload, mapItem) {
|
||||
const items = Array.isArray(payload?.items) ? payload.items : [];
|
||||
return {
|
||||
rows: items.map(mapItem),
|
||||
total: payload?.total ?? items.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user