fix web ui smoke and brand assets

This commit is contained in:
hectorzhao
2026-06-30 11:13:30 +08:00
parent 0dfb2988b2
commit 9920575bba
103 changed files with 6650 additions and 135 deletions
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>LisgloSIPS - 聆界SIP管理平台</title>
</head>
<body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+13 -1
View File
@@ -495,7 +495,19 @@ export default function App() {
<div className={`prototype-app ${collapsed ? 'sidebar-collapsed' : ''}`}>
<aside className="sidebar">
<div className="brand-block">
<div className="brand-mark"></div>
<div className="brand-expanded">
<img
className="brand-logo brand-logo-expanded"
src="/brand/logo1.png"
alt="聆界SIP管理平台"
/>
<span className="brand-sip-text">SIP</span>
</div>
<img
className="brand-logo brand-logo-collapsed"
src="/brand/logo2.png"
alt="聆界SIP管理平台"
/>
<div className="brand-copy">
<strong>聆界SIP管理平台</strong>
<span>LisgloSIPS</span>
+16 -12
View File
@@ -30,7 +30,7 @@ export function Field({ label, hint, error, children }) {
);
}
export function Input(props) {
export function Input({ children: _children, ...props }) {
return <input className="ui-input" {...props} />;
}
@@ -46,34 +46,37 @@ export function Select({ children, ...props }) {
);
}
export function Checkbox({ label, checked, ...props }) {
export function Checkbox({ label, checked, children, onChange, ...props }) {
const labelContent = label ?? children;
return (
<label className="ui-check">
<input type="checkbox" checked={checked} {...props} />
<input type="checkbox" checked={checked} onChange={(event) => onChange?.(event.target.checked, event)} {...props} />
<span className="ui-check-box" aria-hidden="true" />
<span>{label}</span>
<span>{labelContent}</span>
</label>
);
}
export function Radio({ label, checked, ...props }) {
export function Radio({ label, checked, children, onChange, ...props }) {
const labelContent = label ?? children;
return (
<label className="ui-check">
<input type="radio" checked={checked} {...props} />
<input type="radio" checked={checked} onChange={(event) => onChange?.(event.target.checked, event)} {...props} />
<span className="ui-radio-dot" aria-hidden="true" />
<span>{label}</span>
<span>{labelContent}</span>
</label>
);
}
export function Switch({ label, checked, ...props }) {
export function Switch({ label, checked, children, onChange, ...props }) {
const labelContent = label ?? children;
return (
<label className="ui-switch">
<input type="checkbox" checked={checked} {...props} />
<input type="checkbox" checked={checked} onChange={(event) => onChange?.(event.target.checked, event)} {...props} />
<span className="ui-switch-track" aria-hidden="true">
<span className="ui-switch-thumb" />
</span>
<span>{label}</span>
<span>{labelContent}</span>
</label>
);
}
@@ -138,10 +141,11 @@ export function Progress({ value }) {
);
}
export function Slider({ label, value, ...props }) {
export function Slider({ label, value, children, ...props }) {
const labelContent = label ?? children;
return (
<label className="ui-slider">
<span>{label}</span>
<span>{labelContent}</span>
<input type="range" value={value} {...props} />
<output>{value}</output>
</label>
-1
View File
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { Badge, Button } from '../components/ui.jsx';
import { Icon, PageTitle, Panel, ApiNotice, ConfirmDialog, SimpleTable } from '../components/layout.jsx';
import { formatDateTime, formatDurationText } from '../utils/formatters.js';
import { metrics } from '../fixtures/devFixtures.js';
export function ActiveCallsPage({ activeCalls, activeCallsLoading, activeCallsError, refreshActiveCalls, can = () => true, onHangupActiveCall }) {
const [busyId, setBusyId] = useState('');
+14 -45
View File
@@ -1,11 +1,24 @@
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 { 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' });
@@ -227,47 +240,3 @@ export function BusinessPrefixesPage({ can = () => true }) {
</>
);
}
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,
};
}
+42 -25
View File
@@ -31,6 +31,7 @@ export function CustomerGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRo
const strategyPolicies = strategyGateway
? policyRows.filter((item) => item.gateway === strategyGateway).sort((first, second) => first.priority - second.priority)
: [];
const selectedBusinessPrefixCount = gatewayForm.businessPrefixIds.length;
useEffect(() => {
api.businessPrefixes({ status: 'ENABLED' })
.then((items) => setBusinessPrefixOptions(Array.isArray(items) ? items : []))
@@ -170,12 +171,22 @@ export function CustomerGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRo
setPolicyRows((rows) => rows.filter((policy) => policy.gateway !== gateway.id));
setSubmitting(false);
};
const openPolicyDrawer = (gatewayId) => {
setStrategyGateway(gatewayId);
setPolicyModalOpen(false);
setEditingPolicy(null);
setDeletePolicyTarget(null);
setPolicyForm(emptyPolicyForm);
const toggleBusinessPrefix = (prefixId, checked) => {
setGatewayForm((current) => {
const nextIds = checked
? Array.from(new Set([...current.businessPrefixIds, prefixId]))
: current.businessPrefixIds.filter((id) => id !== prefixId);
return { ...current, businessPrefixIds: nextIds };
});
};
const selectAllBusinessPrefixes = () => {
setGatewayForm((current) => ({
...current,
businessPrefixIds: businessPrefixOptions.map((prefix) => prefix.id),
}));
};
const clearBusinessPrefixes = () => {
setGatewayForm((current) => ({ ...current, businessPrefixIds: [] }));
};
const closePolicyDrawer = () => {
setStrategyGateway(null);
@@ -287,10 +298,8 @@ export function CustomerGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRo
<Panel title="客户网关列表" className="wide-panel">
<SimpleTable rows={gatewayRows} columns={[
{ key: 'id', label: 'ID', width: '104px', className: 'table-cell-compact' },
{ key: 'name', label: '名称', width: '168px', className: 'table-cell-compact' },
{ key: 'customer', label: '客户', width: '148px', className: 'table-cell-compact' },
{ key: 'authMode', label: '认证方式' },
{ key: 'authTarget', label: 'IP地址 / 账号名称', render: (row) => (row.authMode === 'IP' || row.authMode === '混合认证' ? (row.sourceIps || []).join(', ') || row.ipAddress : row.sipAccount) },
{ key: 'name', label: '名称', width: '240px', className: 'table-cell-compact' },
{ key: 'customer', label: '客户', width: '220px', className: 'table-cell-compact' },
{ key: 'lineGroupName', label: '落地线路组' },
{ key: 'rate', label: '客户费率', render: (row) => `${row.billingCycleSec || 60}s / ¥${Number(row.cycleRate || 0).toFixed(6)}` },
{ key: 'callerRule', label: '主叫规则', render: (row) => row.callerMatchMode === 'PREFIXES' ? (row.callerPrefixes || []).join(', ') : '任意号码' },
@@ -384,21 +393,29 @@ export function CustomerGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRo
</Select>
</Field>
{gatewayForm.calleeMatchMode === 'BUSINESS_PREFIXES' ? (
<div className="checkbox-grid">
{businessPrefixOptions.map((prefix) => (
<Checkbox
key={prefix.id}
checked={gatewayForm.businessPrefixIds.includes(prefix.id)}
onChange={(checked) => setGatewayForm((current) => ({
...current,
businessPrefixIds: checked
? [...current.businessPrefixIds, prefix.id]
: current.businessPrefixIds.filter((id) => id !== prefix.id),
}))}
>
{prefix.prefix} / {prefix.name}
</Checkbox>
))}
<div className="prefix-picker">
<div className="prefix-picker-head">
<span>已选择 {selectedBusinessPrefixCount} / {businessPrefixOptions.length}</span>
<div className="prefix-picker-actions">
<Button type="button" size="sm" variant="outline" disabled={!businessPrefixOptions.length || selectedBusinessPrefixCount === businessPrefixOptions.length} onClick={selectAllBusinessPrefixes}>全选</Button>
<Button type="button" size="sm" variant="ghost" disabled={!selectedBusinessPrefixCount} onClick={clearBusinessPrefixes}>清空</Button>
</div>
</div>
{businessPrefixOptions.length ? (
<div className="checkbox-grid">
{businessPrefixOptions.map((prefix) => (
<Checkbox
key={prefix.id}
checked={gatewayForm.businessPrefixIds.includes(prefix.id)}
onChange={(checked) => toggleBusinessPrefix(prefix.id, checked)}
>
{prefix.prefix} / {prefix.name}
</Checkbox>
))}
</div>
) : (
<div className="empty-inline">暂无可用业务前缀</div>
)}
</div>
) : null}
<div className="modal-actions">
-1
View File
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { Button, Field, Input, Select, Textarea } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable, KeyValue } from '../components/layout.jsx';
import { gateways } from '../fixtures/devFixtures.js';
import { explainApiError } from '../api.js';
export function CustomersPage({ customerRows, setCustomerRows, addRechargeRecord, apiLoading, apiError, refreshApi, can = () => true, onCreateCustomer, onUpdateCustomer, onToggleCustomerStatus, onDeleteCustomer, onRechargeCustomer }) {
+44
View File
@@ -4,6 +4,50 @@ import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, SimpleTable } from '
import { formatDate, zhStatus, carrierLabel } from '../utils/formatters.js';
import { api, explainApiError } from '../api.js';
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,
};
}
export function NumberLibraryPage({ can = () => true }) {
const [activeTab, setActiveTab] = useState('cities');
const [rows, setRows] = useState(emptyNumberLibraryRows);
+4 -1
View File
@@ -3,6 +3,10 @@ import { Badge, Button, Field, Input, Select } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Drawer, SimpleTable, KeyValue } from '../components/layout.jsx';
import { operationLogRows } from '../fixtures/devFixtures.js';
function toneForStatus(status) {
return status === '成功' || status === 'SUCCESS' ? 'success' : status === '失败' || status === 'FAILURE' ? 'danger' : 'neutral';
}
export function OperationLogsPage({ logRows = operationLogRows, apiLoading, apiError, refreshApi }) {
const [keyword, setKeyword] = useState('');
const [moduleFilter, setModuleFilter] = useState('all');
@@ -64,4 +68,3 @@ export function OperationLogsPage({ logRows = operationLogRows, apiLoading, apiE
</>
);
}
+1 -2
View File
@@ -1,6 +1,6 @@
import { Badge, Button } from '../components/ui.jsx';
import { Icon, PageTitle, Panel, SimpleTable } from '../components/layout.jsx';
import { customers, gateways, customerGatewayPolicies, vendorGatewayPolicies, routeGroups, routeRules } from '../fixtures/devFixtures.js';
import { customerGatewayPolicies, vendorGatewayPolicies, routeGroups, routeRules } from '../fixtures/devFixtures.js';
export function RoutesPage() {
return (
@@ -57,4 +57,3 @@ export function RoutesPage() {
</>
);
}
-3
View File
@@ -34,6 +34,3 @@ export function SettingsPage() {
);
}
const emptyUserForm = { username: '', name: '', phone: '', email: '', roleId: 'R002', status: '启用' };
const emptyRoleForm = { name: '', description: '', status: '启用' };
+1 -2
View File
@@ -1,4 +1,4 @@
import { Badge, Button, Progress } from '../components/ui.jsx';
import { Badge, Button } from '../components/ui.jsx';
import { Icon, PageTitle, Panel, StatusBadge, SimpleTable } from '../components/layout.jsx';
import { sipAccounts, opsItems } from '../fixtures/devFixtures.js';
@@ -22,4 +22,3 @@ export function SipOpsPage() {
</>
);
}
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { Badge, Button, Field, Input, Select } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, Drawer, SimpleTable } from '../components/layout.jsx';
import { formatDate, zhStatus } from '../utils/formatters.js';
import { gateways, vendorLineGroups } from '../fixtures/devFixtures.js';
import { explainApiError } from '../api.js';
@@ -183,18 +182,3 @@ export function VendorLineGroupsPage({ lineGroupRows: apiLineGroupRows, setLineG
</>
);
}
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),
};
}
-1
View File
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { Button, Field, Input, Textarea } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable, KeyValue } from '../components/layout.jsx';
import { gateways } from '../fixtures/devFixtures.js';
import { explainApiError } from '../api.js';
export function VendorsPage({ vendorRows, setVendorRows, addRechargeRecord, apiLoading, apiError, refreshApi, can = () => true, onCreateVendor, onUpdateVendor, onDeleteVendor, onRechargeVendor }) {
+117 -5
View File
@@ -156,13 +156,48 @@ button:disabled {
.brand-block {
display: grid;
grid-template-columns: 38px minmax(0, 1fr) 34px;
grid-template-columns: minmax(0, 1fr) 34px;
align-items: center;
gap: 6px;
gap: 10px;
padding: 16px 12px;
border-bottom: 1px solid var(--line);
}
.brand-logo {
display: block;
object-fit: contain;
min-width: 0;
}
.brand-expanded {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 8px;
}
.brand-logo-expanded {
width: min(100%, 152px);
flex: 0 1 auto;
height: 38px;
object-position: left center;
}
.brand-sip-text {
flex: 0 0 auto;
color: var(--brand);
font-size: 19px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
}
.brand-logo-collapsed {
display: none;
width: 38px;
height: 38px;
}
.brand-mark {
display: grid;
place-items: center;
@@ -175,7 +210,7 @@ button:disabled {
}
.brand-copy {
display: grid;
display: none;
min-width: 0;
gap: 2px;
}
@@ -296,6 +331,18 @@ button:disabled {
padding: 14px 10px;
}
.sidebar-collapsed .brand-logo-expanded {
display: none;
}
.sidebar-collapsed .brand-expanded {
display: none;
}
.sidebar-collapsed .brand-logo-collapsed {
display: block;
}
.sidebar-collapsed .brand-copy,
.sidebar-collapsed .nav-group p,
.sidebar-collapsed .nav-label,
@@ -862,6 +909,60 @@ button:disabled {
gap: 8px;
}
.prefix-picker {
display: grid;
gap: 10px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-muted);
}
.prefix-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 13px;
font-weight: 750;
}
.prefix-picker-actions {
display: inline-flex;
gap: 8px;
}
.checkbox-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px 14px;
}
.checkbox-grid .ui-check {
width: 100%;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
}
.checkbox-grid .ui-check > span:last-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-inline {
padding: 12px;
color: var(--muted);
border: 1px dashed var(--line);
border-radius: 6px;
background: var(--surface);
}
.mini-chart {
display: grid;
grid-template-columns: repeat(12, 1fr);
@@ -1680,12 +1781,23 @@ button:disabled {
}
.sidebar-collapsed .brand-block {
grid-template-columns: 38px minmax(0, 1fr) 34px;
grid-template-columns: minmax(0, 1fr) 34px;
justify-items: stretch;
padding: 12px;
}
.sidebar-collapsed .brand-copy,
.sidebar-collapsed .brand-logo-expanded {
display: block;
}
.sidebar-collapsed .brand-expanded {
display: inline-flex;
}
.sidebar-collapsed .brand-logo-collapsed {
display: none;
}
.sidebar-collapsed .nav-label {
display: grid;
}
-2
View File
@@ -1,6 +1,4 @@
const selectedBlue = '#2563EB';
function formatCurrency(value, digits = 2) {
const numeric = Number(value || 0);
return `¥${numeric.toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;