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 ? ( 正在校验当前浏览器的安全会话。 ) : (
{error ? {error} : null} setUsername(event.target.value)} placeholder="请输入用户名" required /> setPassword(event.target.value)} placeholder="请输入密码" required />
setCaptchaCode(event.target.value.toUpperCase())} placeholder="请输入验证码" maxLength="5" required />
)}
); } 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 (
当前页面 {activeLabel}
{apiError ? 'API 待登录' : '真实 API'} {new Date().toLocaleString('zh-CN', { hour12: false })} {authUser.displayName || authUser.username}
{canAccessPage(active) ? : }
); }