import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Alert, Badge, Button, Field, Input } from './components/ui.jsx';
import { Icon, PageLoadingDialog } from './components/layout.jsx';
import { api, explainApiError } from './api.js';
import { dateRangeParams, lastSevenDays } from './utils/dateRange.js';
import {
formatCurrency,
normalizeAuditLog,
normalizeCustomer,
normalizeCustomerGateway,
normalizeLandingLineGroup,
normalizeRecharge,
normalizeRole,
normalizeUser,
normalizeVendor,
normalizeVendorGateway,
} from './utils/formatters.js';
import { DashboardPage } from './pages/DashboardPage.jsx';
import { CallerAnalyticsPage } from './pages/CallerAnalyticsPage.jsx';
import { ActiveCallsPage } from './pages/ActiveCallsPage.jsx';
import { CustomersPage } from './pages/CustomersPage.jsx';
import { RechargeRecordsPage } from './pages/RechargeRecordsPage.jsx';
import { CustomerGatewaysPage } from './pages/CustomerGatewaysPage.jsx';
import { VendorsPage } from './pages/VendorsPage.jsx';
import { VendorGatewaysPage } from './pages/VendorGatewaysPage.jsx';
import { VendorLineGroupsPage } from './pages/VendorLineGroupsPage.jsx';
import { BusinessPrefixesPage } from './pages/BusinessPrefixesPage.jsx';
import { NumberLibraryPage } from './pages/NumberLibraryPage.jsx';
import { RoutesPage } from './pages/RoutesPage.jsx';
import { BillingPage } from './pages/BillingPage.jsx';
import { CdrPage } from './pages/CdrPage.jsx';
import { QualityPage } from './pages/QualityPage.jsx';
import { SipOpsPage } from './pages/SipOpsPage.jsx';
import { MonitoringPage } from './pages/MonitoringPage.jsx';
import { SettingsPage } from './pages/SettingsPage.jsx';
import { UsersPage } from './pages/UsersPage.jsx';
import { RolesPage } from './pages/RolesPage.jsx';
import { OperationLogsPage } from './pages/OperationLogsPage.jsx';
import { PAGE_PERMISSIONS, can, canAll, permissionSet } from './permissions.js';
const navGroups = [
{
title: '运营',
items: [
{ key: 'dashboard', label: '概览 Dashboard', permissions: PAGE_PERMISSIONS.dashboard },
{ key: 'customers', label: '客户管理', permissions: PAGE_PERMISSIONS.customers },
{ key: 'customerGateways', label: '客户网关管理', permissions: PAGE_PERMISSIONS.customerGateways },
{ key: 'businessPrefixes', label: '业务前缀管理', permissions: PAGE_PERMISSIONS.businessPrefixes },
{ key: 'rechargeRecords', label: '充值记录', permissions: PAGE_PERMISSIONS.rechargeRecords },
{ key: 'vendors', label: '供应商管理', permissions: PAGE_PERMISSIONS.vendors },
{ key: 'vendorGateways', label: '落地网关管理', permissions: PAGE_PERMISSIONS.vendorGateways },
{ key: 'vendorLineGroups', label: '落地线路组', permissions: PAGE_PERMISSIONS.vendorLineGroups },
{ key: 'numberLibrary', label: '号码库', permissions: PAGE_PERMISSIONS.numberLibrary },
{ key: 'billing', label: '费率与计费', pending: true },
],
},
{
title: '业务',
items: [
{ key: 'activeCalls', label: '当前通话', permissions: PAGE_PERMISSIONS.activeCalls },
{ key: 'cdr', label: '话单中心', permissions: PAGE_PERMISSIONS.cdr },
{ key: 'callerAnalytics', label: '主叫号码分析', permissions: PAGE_PERMISSIONS.callerAnalytics },
{ key: 'quality', label: '质检中心', permissions: PAGE_PERMISSIONS.quality },
{ key: 'sipops', label: 'SIP 运维', pending: true },
{ key: 'monitoring', label: '监控告警', pending: true },
],
},
{
title: '系统',
items: [
{ key: 'settings', label: '系统设置', pending: true },
{ key: 'users', label: '用户管理', permissions: PAGE_PERMISSIONS.users },
{ key: 'roles', label: '角色与权限', permissions: PAGE_PERMISSIONS.roles },
{ key: 'operationLogs', label: '操作日志', permissions: PAGE_PERMISSIONS.operationLogs },
],
},
];
const pages = {
callerAnalytics: CallerAnalyticsPage,
dashboard: DashboardPage,
activeCalls: ActiveCallsPage,
customers: CustomersPage,
customerGateways: CustomerGatewaysPage,
businessPrefixes: BusinessPrefixesPage,
rechargeRecords: RechargeRecordsPage,
vendors: VendorsPage,
vendorGateways: VendorGatewaysPage,
vendorLineGroups: VendorLineGroupsPage,
numberLibrary: NumberLibraryPage,
routes: RoutesPage,
billing: BillingPage,
cdr: CdrPage,
quality: QualityPage,
sipops: SipOpsPage,
monitoring: MonitoringPage,
settings: SettingsPage,
users: UsersPage,
roles: RolesPage,
operationLogs: OperationLogsPage,
};
function NoPermissionPage({ label }) {
return (
当前账号没有访问「{label || '该页面'}」所需的权限,请联系管理员调整角色权限。
);
}
function LoginPage({ checkingSession, onLogin }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [captchaCode, setCaptchaCode] = useState('');
const [captcha, setCaptcha] = useState(null);
const [captchaLoading, setCaptchaLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const refreshCaptcha = async () => {
setCaptchaLoading(true);
try {
const nextCaptcha = await api.captcha();
setCaptcha(nextCaptcha);
setCaptchaCode('');
} catch (captchaError) {
setError(explainApiError(captchaError));
} finally {
setCaptchaLoading(false);
}
};
useEffect(() => {
if (!checkingSession) {
void refreshCaptcha();
}
}, [checkingSession]);
const submitLogin = async (event) => {
event.preventDefault();
if (!username.trim() || !password || !captcha?.captchaId || !captchaCode.trim() || submitting) {
return;
}
setSubmitting(true);
setError('');
try {
await onLogin({ username, password, captchaId: captcha.captchaId, captchaCode });
} catch (loginError) {
setError(explainApiError(loginError));
setPassword('');
await refreshCaptcha();
} finally {
setSubmitting(false);
}
};
return (
聆
聆界SIP管理平台
LisgloSIPS V2
{checkingSession ? (
正在校验当前浏览器的安全会话。
) : (
)}
);
}
export default function App() {
const [active, setActive] = useState('dashboard');
const [collapsed, setCollapsed] = useState(false);
const [authUser, setAuthUser] = useState(null);
const [authChecking, setAuthChecking] = useState(true);
const [customerRows, setCustomerRows] = useState([]);
const [vendorRows, setVendorRows] = useState([]);
const [customerGatewayRows, setCustomerGatewayRows] = useState([]);
const [vendorGatewayRows, setVendorGatewayRows] = useState([]);
const [landingLineGroupRows, setLandingLineGroupRows] = useState([]);
const [rechargeRows, setRechargeRows] = useState([]);
const [userRows, setUserRows] = useState([]);
const [roleRows, setRoleRows] = useState([]);
const [logRows, setLogRows] = useState([]);
const [dashboardSummary, setDashboardSummary] = useState(null);
const [dashboardTrends, setDashboardTrends] = useState(null);
const [activeCalls, setActiveCalls] = useState([]);
const [activeCallsLoading, setActiveCallsLoading] = useState(false);
const [activeCallsBlockingLoading, setActiveCallsBlockingLoading] = useState(false);
const [activeCallsError, setActiveCallsError] = useState('');
const [activeCallsUpdatedAt, setActiveCallsUpdatedAt] = useState(null);
const [apiLoading, setApiLoading] = useState(true);
const [apiError, setApiError] = useState('');
const loadedPagesRef = useRef(new Set());
const pageRequestRef = useRef({ id: 0, controller: null });
const currentPermissions = useMemo(() => permissionSet(authUser), [authUser]);
const canPermission = (permission) => can(currentPermissions, permission);
const allowedNavGroups = useMemo(() => navGroups
.map((group) => ({ ...group, items: group.items.filter((item) => !item.permissions || canAll(currentPermissions, item.permissions)) }))
.filter((group) => group.items.some((item) => !item.pending)), [currentPermissions]);
const ActivePage = pages[active];
const activeLabel = useMemo(() => navGroups.flatMap((group) => group.items).find((item) => item.key === active)?.label, [active]);
const canAccessPage = (key, permissions = currentPermissions) => canAll(permissions, PAGE_PERMISSIONS[key] || []);
const loadPageData = async (page = active, user = authUser, { force = false } = {}) => {
const sourceUser = user && Array.isArray(user.permissions) ? user : authUser;
if (!sourceUser) return;
if (!force && loadedPagesRef.current.has(page)) {
setApiLoading(false);
setApiError('');
return;
}
const granted = permissionSet(sourceUser);
pageRequestRef.current.controller?.abort('PAGE_CHANGED');
const controller = new AbortController();
const requestId = pageRequestRef.current.id + 1;
pageRequestRef.current = { id: requestId, controller };
setApiLoading(true);
setApiError('');
const signalOptions = { signal: controller.signal };
const definitions = [];
const add = (permission, load, apply) => {
if (can(granted, permission)) definitions.push({ load, apply });
};
if (page === 'dashboard') {
add('dashboard.view', () => api.dashboardOverview(undefined, signalOptions), (value) => {
setDashboardSummary(value.summary);
setDashboardTrends(value.trends);
});
} else if (page === 'customers') {
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
} else if (page === 'vendors') {
add('vendors.view', () => api.vendors(signalOptions), (rows) => setVendorRows(rows.map(normalizeVendor)));
} else if (page === 'customerGateways') {
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
add('customer_gateways.view', () => api.customerGateways(signalOptions), (rows) => setCustomerGatewayRows(rows.map(normalizeCustomerGateway)));
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
} else if (page === 'vendorGateways') {
add('vendors.view', () => api.vendors(signalOptions), (rows) => setVendorRows(rows.map(normalizeVendor)));
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
} else if (page === 'vendorLineGroups') {
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
} else if (page === 'rechargeRecords') {
add('recharges.view', () => api.recharges(signalOptions), (value) => setRechargeRows(value.items.map(normalizeRecharge)));
} else if (page === 'users' || page === 'roles') {
add('users.view', () => api.users(signalOptions), (rows) => setUserRows(rows.map(normalizeUser)));
add('roles.view', () => api.roles(signalOptions), (rows) => setRoleRows(rows.map(normalizeRole)));
} else if (page === 'operationLogs') {
const range = lastSevenDays();
add('audit.view', () => api.auditLogs(dateRangeParams(range.startDate, range.endDate, 'createdFrom', 'createdTo'), signalOptions), (value) => setLogRows(value.items.map(normalizeAuditLog)));
} else if (page === 'cdr') {
add('customer_gateways.view', () => api.customerGateways(signalOptions), (rows) => setCustomerGatewayRows(rows.map(normalizeCustomerGateway)));
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
} else if (page === 'qualityRuleDependencies') {
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
}
const results = await Promise.allSettled(definitions.map((definition) => definition.load()));
if (pageRequestRef.current.id !== requestId) return;
const failures = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') definitions[index].apply(result.value);
else if (!controller.signal.aborted) failures.push(result.reason);
});
if (failures.length) setApiError(explainApiError(failures[0]));
else loadedPagesRef.current.add(page);
setApiLoading(false);
};
const refreshApi = () => loadPageData(active, authUser, { force: true });
const refreshAuditLogs = async (params = {}) => {
setApiLoading(true);
setApiError('');
try {
const value = await api.auditLogs(params);
setLogRows((value.items || []).map(normalizeAuditLog));
} catch (error) {
setApiError(explainApiError(error));
} finally {
setApiLoading(false);
}
};
const refreshActiveCalls = useCallback(async ({ silent = false } = {}) => {
setActiveCallsLoading(true);
if (!silent) setActiveCallsBlockingLoading(true);
setActiveCallsError('');
try {
const response = await api.activeCalls();
setActiveCalls(response.items || []);
setActiveCallsUpdatedAt(new Date());
} catch (error) {
setActiveCallsError(explainApiError(error));
} finally {
setActiveCallsLoading(false);
if (!silent) setActiveCallsBlockingLoading(false);
}
}, []);
useEffect(() => {
let mounted = true;
api.refresh()
.then((response) => {
if (!mounted) return;
setAuthUser(response.user);
void loadPageData('dashboard', response.user, { force: true });
})
.catch(() => {
if (!mounted) return;
setAuthUser(null);
setApiLoading(false);
})
.finally(() => {
if (mounted) {
setAuthChecking(false);
}
});
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (authUser && active === 'activeCalls' && can(authUser, 'active_calls.view')) {
void refreshActiveCalls();
}
}, [authUser, active]);
useEffect(() => {
if (!authUser || canAccessPage(active)) {
return;
}
const firstAllowed = allowedNavGroups.flatMap((group) => group.items).find((item) => !item.pending);
if (firstAllowed) {
setActive(firstAllowed.key);
}
}, [active, allowedNavGroups, authUser]);
const handleLogin = async (credentials) => {
const response = await api.login(credentials);
setAuthUser(response.user);
const firstAllowed = navGroups.flatMap((group) => group.items).find((item) => !item.pending && canAll(response.user, item.permissions || []));
if (firstAllowed) {
setActive(firstAllowed.key);
}
await loadPageData(firstAllowed?.key || 'dashboard', response.user, { force: true });
};
const handleLogout = async () => {
await api.logout();
setAuthUser(null);
setActive('dashboard');
setApiError('');
setCustomerRows([]);
setVendorRows([]);
setCustomerGatewayRows([]);
setVendorGatewayRows([]);
setLandingLineGroupRows([]);
setRechargeRows([]);
setUserRows([]);
setRoleRows([]);
setLogRows([]);
setDashboardSummary(null);
setDashboardTrends(null);
setActiveCalls([]);
setActiveCallsError('');
loadedPagesRef.current.clear();
pageRequestRef.current.controller?.abort('LOGOUT');
};
const reloadAfterMutation = async (operation) => {
await operation();
await loadPageData(active, authUser, { force: true });
};
const applyRechargeResult = (response) => {
const normalized = normalizeRecharge(response);
setRechargeRows((rows) => [normalized, ...rows.filter((row) => row.id !== normalized.id)]);
if (response.accountType === 'CUSTOMER') {
setCustomerRows((rows) =>
rows.map((row) => (row.id === response.accountId ? { ...row, balance: formatCurrency(response.afterBalance) } : row))
);
}
if (response.accountType === 'VENDOR') {
setVendorRows((rows) =>
rows.map((row) => (row.id === response.accountId ? { ...row, balance: formatCurrency(response.afterBalance) } : row))
);
}
return response;
};
const rechargeAndApply = async (operation) => {
const response = await operation();
return applyRechargeResult(response);
};
const activePageProps = active === 'customers'
? {
customerRows,
setCustomerRows,
addRechargeRecord: (record) => setRechargeRows((rows) => [record, ...rows]),
apiLoading,
apiError,
refreshApi,
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)),
}
: active === 'vendors'
? {
vendorRows,
setVendorRows,
addRechargeRecord: (record) => setRechargeRows((rows) => [record, ...rows]),
apiLoading,
apiError,
refreshApi,
can: canPermission,
onCreateVendor: (body) => reloadAfterMutation(() => api.createVendor(body)),
onUpdateVendor: (id, body) => reloadAfterMutation(() => api.updateVendor(id, body)),
onDeleteVendor: (id) => reloadAfterMutation(() => api.deleteVendor(id)),
onRechargeVendor: (id, body) => rechargeAndApply(() => api.rechargeVendor(id, body)),
}
: active === 'customerGateways'
? {
gatewayRows: customerGatewayRows,
setGatewayRows: setCustomerGatewayRows,
customerRows,
lineGroupRows: landingLineGroupRows,
apiLoading,
apiError,
refreshApi,
can: canPermission,
onCreateGateway: (body) => reloadAfterMutation(() => api.createCustomerGateway(body)),
onUpdateGateway: (id, body) => reloadAfterMutation(() => api.updateCustomerGateway(id, body)),
onToggleGatewayStatus: (row) => reloadAfterMutation(() => (row.status === '启用' ? api.disableCustomerGateway(row.id) : api.enableCustomerGateway(row.id))),
onDeleteGateway: (id) => reloadAfterMutation(() => api.deleteCustomerGateway(id)),
}
: active === 'vendorGateways'
? {
gatewayRows: vendorGatewayRows,
setGatewayRows: setVendorGatewayRows,
vendorRows,
apiLoading,
apiError,
refreshApi,
can: canPermission,
onUpdateGateway: (id, body) => reloadAfterMutation(() => api.updateVendorGateway(id, body)),
onToggleGatewayStatus: (row) => reloadAfterMutation(() => (row.status === '启用' ? api.disableVendorGateway(row.id) : api.enableVendorGateway(row.id))),
onDeleteGateway: (id) => reloadAfterMutation(() => api.deleteVendorGateway(id)),
}
: active === 'vendorLineGroups'
? {
lineGroupRows: landingLineGroupRows,
setLineGroupRows: setLandingLineGroupRows,
gatewayRows: vendorGatewayRows,
apiLoading,
apiError,
refreshApi,
can: canPermission,
onDeleteLineGroup: (id) => reloadAfterMutation(() => api.deleteLandingLineGroup(id)),
}
: active === 'rechargeRecords'
? { rechargeRows, apiLoading, apiError, refreshApi }
: active === 'dashboard'
? { dashboardSummary, dashboardTrends, apiLoading, apiError, refreshApi }
: active === 'businessPrefixes'
? { can: canPermission }
: active === 'numberLibrary'
? { can: canPermission }
: active === 'activeCalls'
? {
activeCalls,
activeCallsLoading,
activeCallsBlockingLoading,
activeCallsError,
activeCallsUpdatedAt,
refreshActiveCalls,
can: canPermission,
onHangupActiveCall: async (id) => {
await api.hangupActiveCall(id);
await refreshActiveCalls();
},
}
: active === 'cdr'
? { customerGatewayRows, vendorGatewayRows, can: canPermission }
: active === 'quality'
? {
customerRows,
lineGroupRows: landingLineGroupRows,
can: canPermission,
loadRuleDependencies: () => loadPageData('qualityRuleDependencies', authUser),
}
: active === 'users'
? { userRows, setUserRows, roleRows, apiLoading, apiError, refreshApi, can: canPermission, onDeleteUser: (id) => reloadAfterMutation(() => api.deleteUser(id)) }
: active === 'roles'
? { roleRows, setRoleRows, userRows, apiLoading, apiError, refreshApi, can: canPermission, onDeleteRole: (id) => reloadAfterMutation(() => api.deleteRole(id)) }
: active === 'operationLogs'
? { logRows, apiLoading, apiError, refreshApi: refreshAuditLogs }
: {};
if (!authUser) {
return ;
}
return (
{canAccessPage(active) ? : }
);
}