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

244 lines
9.5 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 { 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}
</>
);
}