feat: add number library routing and cdr location support
This commit is contained in:
+1257
-115
File diff suppressed because it is too large
Load Diff
+88
-1
@@ -1,6 +1,18 @@
|
||||
const API_BASE = (import.meta.env.VITE_LISGLOSIPS_API_BASE || '/api/v2').replace(/\/$/, '');
|
||||
const ACCESS_TOKEN_KEY = 'lisglosips.accessToken';
|
||||
|
||||
export function getAccessToken() {
|
||||
return window.localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setAccessToken(token) {
|
||||
if (token) {
|
||||
window.localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||
} else {
|
||||
window.localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, { status, code } = {}) {
|
||||
super(message);
|
||||
@@ -55,12 +67,39 @@ function idempotencyKey(scope) {
|
||||
return `${scope}:${random}`;
|
||||
}
|
||||
|
||||
function queryString(params = {}) {
|
||||
const entries = Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== '' && value !== 'all');
|
||||
return entries.length ? `?${new URLSearchParams(Object.fromEntries(entries))}` : '';
|
||||
}
|
||||
|
||||
export const api = {
|
||||
captcha: () => request('/auth/captcha'),
|
||||
login: async (body) => {
|
||||
const response = await request('/auth/login', { method: 'POST', body: jsonBody(body) });
|
||||
setAccessToken(response.accessToken);
|
||||
return response;
|
||||
},
|
||||
refresh: async () => {
|
||||
const response = await request('/auth/refresh', { method: 'POST' });
|
||||
setAccessToken(response.accessToken);
|
||||
return response;
|
||||
},
|
||||
logout: async () => {
|
||||
try {
|
||||
await request('/auth/logout', { method: 'POST' });
|
||||
} finally {
|
||||
setAccessToken('');
|
||||
}
|
||||
},
|
||||
dashboardSummary: () => request('/dashboard/summary'),
|
||||
dashboardTrends: (params = { hours: 24, bucketMinutes: 60 }) => request(`/dashboard/trends?${new URLSearchParams(params)}`),
|
||||
activeCalls: () => request('/active-calls'),
|
||||
hangupActiveCall: (id) => request(`/active-calls/${encodeURIComponent(id)}/hangup`, { method: 'POST' }),
|
||||
cdrs: (params = {}) => request(`/cdrs${queryString({ take: 100, ...params })}`),
|
||||
customers: () => request('/customers'),
|
||||
createCustomer: (body) => request('/customers', { method: 'POST', body: jsonBody(body) }),
|
||||
updateCustomer: (id, body) => request(`/customers/${encodeURIComponent(id)}`, { method: 'PATCH', body: jsonBody(body) }),
|
||||
deleteCustomer: (id) => request(`/customers/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
rechargeCustomer: (id, body) =>
|
||||
request(`/customers/${encodeURIComponent(id)}/recharges`, {
|
||||
method: 'POST',
|
||||
@@ -69,20 +108,68 @@ export const api = {
|
||||
vendors: () => request('/vendors'),
|
||||
createVendor: (body) => request('/vendors', { method: 'POST', body: jsonBody(body) }),
|
||||
updateVendor: (id, body) => request(`/vendors/${encodeURIComponent(id)}`, { method: 'PATCH', body: jsonBody(body) }),
|
||||
deleteVendor: (id) => request(`/vendors/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
rechargeVendor: (id, body) =>
|
||||
request(`/vendors/${encodeURIComponent(id)}/recharges`, {
|
||||
method: 'POST',
|
||||
body: jsonBody({ ...body, idempotencyKey: idempotencyKey('vendor-recharge') }),
|
||||
}),
|
||||
customerGateways: () => request('/customer-gateways'),
|
||||
enableCustomerGateway: (id) => request(`/customer-gateways/${encodeURIComponent(id)}/enable`, { method: 'POST' }),
|
||||
disableCustomerGateway: (id) => request(`/customer-gateways/${encodeURIComponent(id)}/disable`, { method: 'POST' }),
|
||||
deleteCustomerGateway: (id) => request(`/customer-gateways/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
vendorGateways: () => request('/vendor-gateways'),
|
||||
enableVendorGateway: (id) => request(`/vendor-gateways/${encodeURIComponent(id)}/enable`, { method: 'POST' }),
|
||||
disableVendorGateway: (id) => request(`/vendor-gateways/${encodeURIComponent(id)}/disable`, { method: 'POST' }),
|
||||
deleteVendorGateway: (id) => request(`/vendor-gateways/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
landingLineGroups: () => request('/landing-line-groups'),
|
||||
deleteLandingLineGroup: (id) => request(`/landing-line-groups/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
recharges: () => request('/recharges?take=100'),
|
||||
users: () => request('/users'),
|
||||
deleteUser: (id) => request(`/users/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
roles: () => request('/roles'),
|
||||
deleteRole: (id) => request(`/roles/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
auditLogs: () => request('/audit-logs?take=100'),
|
||||
numberLibraryCities: (params = {}) => request(`/number-library/cities${queryString({ take: 100, ...params })}`),
|
||||
importNumberLibraryCities: (items) => request('/number-library/cities/import', { method: 'POST', body: jsonBody({ items }) }),
|
||||
numberLibraryPhoneSegments: (params = {}) => request(`/number-library/phone-segments${queryString({ take: 100, ...params })}`),
|
||||
importNumberLibraryPhoneSegments: (items) => request('/number-library/phone-segments/import', { method: 'POST', body: jsonBody({ items }) }),
|
||||
numberLibraryAreaCodes: (params = {}) => request(`/number-library/area-codes${queryString({ take: 100, ...params })}`),
|
||||
importNumberLibraryAreaCodes: (items) => request('/number-library/area-codes/import', { method: 'POST', body: jsonBody({ items }) }),
|
||||
numberLibraryCarrierPrefixRules: (params = {}) => request(`/number-library/carrier-prefix-rules${queryString({ take: 100, ...params })}`),
|
||||
importNumberLibraryCarrierPrefixRules: (items) => request('/number-library/carrier-prefix-rules/import', { method: 'POST', body: jsonBody({ items }) }),
|
||||
};
|
||||
|
||||
export function explainApiError(error) {
|
||||
if (error instanceof ApiError && error.code === 'AUTH_CAPTCHA_INVALID') {
|
||||
return '验证码错误或已过期,请重新输入。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'AUTH_INVALID_CREDENTIALS') {
|
||||
return '用户名、密码或验证码不正确。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'CUSTOMER_HAS_GATEWAYS') {
|
||||
return '该客户仍有关联客户网关,不能删除。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'VENDOR_HAS_GATEWAYS') {
|
||||
return '该供应商仍有关联落地网关,不能删除。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'LINE_GROUP_IN_USE') {
|
||||
return '该落地线路组仍被客户网关使用,不能删除。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'VENDOR_GATEWAY_IN_LINE_GROUP') {
|
||||
return '该落地网关仍被落地线路组引用,不能删除。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'ROLE_HAS_USERS') {
|
||||
return '该角色仍有关联用户,不能删除。';
|
||||
}
|
||||
if (error instanceof ApiError && error.code === 'BUILT_IN_ROLE_PROTECTED') {
|
||||
return '系统内置角色受保护,不能删除或修改关键权限。';
|
||||
}
|
||||
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
||||
return 'API 已启用鉴权,请先通过登录接口获取会话或在同源环境使用有效 Cookie。';
|
||||
return '登录状态已失效,请重新登录。';
|
||||
}
|
||||
if (error instanceof ApiError && error.status === 429) {
|
||||
return '登录尝试过于频繁,请稍后再试。';
|
||||
}
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
|
||||
}
|
||||
|
||||
@@ -58,6 +58,80 @@ button:disabled {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(241, 242, 245, 0.96)),
|
||||
var(--page);
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
width: min(100%, 420px);
|
||||
padding: 28px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login-brand h1 {
|
||||
margin: 0 0 2px;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.login-brand span {
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 132px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 132px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.captcha-image:hover {
|
||||
border-color: var(--selected);
|
||||
}
|
||||
|
||||
.captcha-image img {
|
||||
display: block;
|
||||
width: 132px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.prototype-app {
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
@@ -668,6 +742,11 @@ button:disabled {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
color: var(--muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -759,6 +838,12 @@ button:disabled {
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.proto-table .table-cell-compact {
|
||||
max-width: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.proto-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
@@ -850,6 +935,80 @@ button:disabled {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cdr-detail {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cdr-detail-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
background: var(--surface-muted);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cdr-detail-hero div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cdr-detail-hero span,
|
||||
.cdr-detail-section h3 {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.cdr-detail-hero strong {
|
||||
overflow: hidden;
|
||||
color: var(--brand);
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cdr-detail-hero svg {
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 9px;
|
||||
color: var(--selected);
|
||||
background: var(--selected-soft);
|
||||
border: 1px solid #cfe0ff;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.cdr-detail-strip,
|
||||
.cdr-detail-grid,
|
||||
.cdr-timeline {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cdr-detail-strip {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cdr-detail-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cdr-timeline {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cdr-detail-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rank-list div,
|
||||
.ops-row {
|
||||
display: flex;
|
||||
@@ -1444,6 +1603,12 @@ button:disabled {
|
||||
border-color: #b9e8c7;
|
||||
}
|
||||
|
||||
.ui-alert-danger {
|
||||
color: #991b1b;
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -1594,10 +1759,19 @@ button:disabled {
|
||||
.match-grid,
|
||||
.option-grid,
|
||||
.rate-config-fields,
|
||||
.cdr-detail-hero,
|
||||
.cdr-detail-strip,
|
||||
.cdr-detail-grid,
|
||||
.cdr-timeline,
|
||||
.code-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.cdr-detail-hero svg {
|
||||
justify-self: center;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.inline-control,
|
||||
.time-range,
|
||||
.prefix-rule {
|
||||
@@ -1615,6 +1789,15 @@ button:disabled {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.captcha-image,
|
||||
.captcha-image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user