feat: improve application access and money precision

This commit is contained in:
hectorzhao
2026-07-16 17:54:05 +08:00
parent 9d5c507007
commit faa716b8d0
49 changed files with 1699 additions and 489 deletions
+40 -24
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { Database, ListFilter, Plus, RotateCcw, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
@@ -18,6 +18,32 @@ type CarrierRule = DictionaryItem & {
remark?: string | null;
};
const carrierTone: Record<string, 'success' | 'info' | 'warning' | 'neutral'> = {
: 'success',
: 'info',
: 'warning',
};
type PhoneSegmentSummaryProps = {
icon: ReactNode;
label: string;
description: string;
total: number;
};
function PhoneSegmentSummary({ icon, label, description, total }: PhoneSegmentSummaryProps) {
return (
<section className="phone-segment-summary" aria-label={label}>
<span className="phone-segment-summary__icon">{icon}</span>
<div className="phone-segment-summary__value">
<span>{label}</span>
<strong>{total.toLocaleString('zh-CN')}</strong>
</div>
<p>{description}</p>
</section>
);
}
export function AdminPhoneSegmentsPage() {
const pageSize = 25;
const [segments, setSegments] = useState<PhoneSegment[]>([]);
@@ -121,12 +147,12 @@ export function AdminPhoneSegmentsPage() {
}
const columns = useMemo<Array<TableColumn<PhoneSegment>>>(() => [
{ key: 'segment', title: '手机号段(手机号码前7位)', width: '230px', render: (record) => <strong>{record.prefix}</strong> },
{ key: 'carrier', title: '运营商', width: '150px', render: (record) => record.carrier ?? '-' },
{ key: 'province', title: '省份', width: '140px', render: (record) => record.province ?? '-' },
{ key: 'city', title: '城市', width: '140px', render: (record) => record.city ?? '-' },
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => formatDateTime(record.createdAt) },
{ key: 'actions', title: '操作', width: '120px', align: 'right', render: (record) => <Button icon={<Trash2 size={14} />} onClick={() => setDeleteTarget(record)} size="sm" variant="danger"></Button> },
{ key: 'segment', title: '手机号段(前7位)', width: '190px', render: (record) => <strong className="phone-segment-prefix">{record.prefix}</strong> },
{ key: 'carrier', title: '运营商', width: '130px', render: (record) => record.carrier ? <Tag tone={carrierTone[record.carrier] ?? 'neutral'}>{record.carrier}</Tag> : '-' },
{ key: 'province', title: '省份', width: '110px', render: (record) => record.province ?? '-' },
{ key: 'city', title: '城市', width: '110px', render: (record) => record.city ?? '-' },
{ key: 'createdAt', title: '创建时间', width: '170px', render: (record) => formatDateTime(record.createdAt) },
{ key: 'actions', title: '操作', width: '90px', align: 'right', render: (record) => <Button className="phone-segment-delete" icon={<Trash2 size={14} />} onClick={() => setDeleteTarget(record)} size="sm" variant="ghost"></Button> },
], []);
const ruleColumns = useMemo<Array<TableColumn<CarrierRule>>>(() => [
@@ -138,7 +164,7 @@ export function AdminPhoneSegmentsPage() {
const queryPanel = (
<div className="phone-segment-query">
<Input label="关键词" onChange={(event) => setKeyword(event.target.value)} placeholder={activeTab === 'segments' ? '手机号段、运营商、省份或城市' : '运营商、正则或备注'} prefix={<Search size={16} />} value={keyword} />
<Input aria-label="关键词" onChange={(event) => setKeyword(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') query(); }} placeholder={activeTab === 'segments' ? '搜索手机号段、运营商、省份或城市' : '搜索运营商、正则或备注'} prefix={<Search size={16} />} value={keyword} />
<div className="phone-segment-query__actions">
<Button icon={<Search size={16} />} onClick={query}></Button>
<Button icon={<RotateCcw size={16} />} onClick={reset} variant="ghost"></Button>
@@ -149,9 +175,9 @@ export function AdminPhoneSegmentsPage() {
return (
<section className="page-stack admin-system-page phone-segment-workbench">
<div className="page-heading">
<div>
<div className="phone-segment-heading">
<Breadcrumb items={['系统管理', '手机号段库']} />
<h1></h1>
<p></p>
</div>
<Button icon={<Plus size={16} />} onClick={() => activeTab === 'segments' ? setCreating(true) : setCreatingRule(true)}>
{activeTab === 'segments' ? '新增号段' : '新增规则'}
@@ -159,7 +185,7 @@ export function AdminPhoneSegmentsPage() {
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="surface admin-system-table-card">
<div className="surface admin-system-table-card phone-segment-panel">
<Tabs
className="phone-segment-workbench__tabs"
onChange={(value) => {
@@ -172,12 +198,7 @@ export function AdminPhoneSegmentsPage() {
value: 'segments',
content: (
<div className="phone-segment-tab-content">
<div className="phone-segment-overview phone-segment-overview--single" aria-label="手机号段统计">
<section>
<span><Database size={20} /></span>
<div><strong>{segmentTotal.toLocaleString('zh-CN')}</strong><p></p></div>
</section>
</div>
<PhoneSegmentSummary description="当前库中可查询的号码前七位记录" icon={<Database size={22} />} label="已收录手机号段" total={segmentTotal} />
{queryPanel}
<Table columns={columns} data={segments} emptyText={loading ? '加载中...' : '暂无手机号段'} pagination={false} rowKey="id" />
<Pagination
@@ -198,12 +219,7 @@ export function AdminPhoneSegmentsPage() {
value: 'rules',
content: (
<div className="phone-segment-tab-content">
<div className="phone-segment-overview phone-segment-overview--single" aria-label="运营商区分规则统计">
<section>
<span><ListFilter size={20} /></span>
<div><strong>{ruleTotal.toLocaleString('zh-CN')}</strong><p></p></div>
</section>
</div>
<PhoneSegmentSummary description="按优先级匹配号码前缀的识别规则" icon={<ListFilter size={22} />} label="运营商识别规则" total={ruleTotal} />
{queryPanel}
<Table columns={ruleColumns} data={rules} emptyText={loading ? '加载中...' : '暂无运营商区分规则'} pagination={false} rowKey="id" />
<Pagination