commit 2f3c274a301c8656953c34de8ada56018d687005 Author: hectorzhao Date: Tue Jun 30 16:09:46 2026 +0800 Initial CMPP frontend prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d9d421 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +.env +.env.* +*.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +.DS_Store +Thumbs.db diff --git a/docs/ui-design-guidelines.md b/docs/ui-design-guidelines.md new file mode 100644 index 0000000..8fc7c5a --- /dev/null +++ b/docs/ui-design-guidelines.md @@ -0,0 +1,138 @@ +# CMPP 平台 UI 设计规范 + +## 设计原则 + +- 后台页面优先服务扫描、比对和操作,避免营销式大留白。 +- 信息密度保持克制:列表、表格、卡片要紧凑,但标题、正文、状态之间必须保留可辨识层级。 +- 页面以白色和浅灰为主,品牌黑用于导航和关键结构,蓝色只用于选中、主操作和链接。 +- 所有业务页面优先复用 `src/components/ui` 内的自研组件。 + +## 颜色 + +- 品牌主色:`#12121A`,用于侧边栏、强品牌识别区域。 +- 辅助金色:`#D9C3A0`,用于品牌点缀,不作为大面积背景。 +- 选中蓝色:`#2563EB`,用于当前菜单、主按钮、链接、聚焦态。 +- 背景色:页面背景 `#F6F7F9`,卡片和表单背景 `#FFFFFF`。 +- 边框色:默认 `#E5E7EB`,较强分隔 `#D1D5DB`。 +- 文本色:主文本 `#1F2937`,标题 `#111827`,辅助文本 `#6B7280`。 +- 状态色:成功 `#16A34A`,警告 `#D97706`,失败 `#DC2626`,信息 `#2563EB`。 + +## 字号与层级 + +- 页面标题:24px,700。只用于页面主标题、重要弹窗标题。 +- 区块标题:16px,600。用于表单分组、卡片区块、小节标题。 +- 卡片标题:16px,600。短信模板名称、彩信模板名称、应用名称等都使用这一档,不要放大到 20px 以上。 +- 正文文本:14px,400-500,行高 1.5。用于表格正文、卡片正文、普通说明。 +- 次要文本:13px,400-500,使用辅助色。用于 ID、时间、应用名、补充说明。 +- 辅助标签:12px,500-600。用于 Tag、字段提示、状态旁注。 +- 表格正文:14px,关键字段可 600。 +- 状态 Tag:12px,600,保持短胶囊,不拉满列宽。 + +## 文字规范 + +- 同一业务类型的名称必须使用同一档字号:短信模板名称、彩信模板名称、签名名称、应用名称统一用卡片标题 `16px / 600`。 +- 页面标题和卡片标题要明显区分:页面标题可 20-24px,卡片内标题不超过 16px,避免列表卡片视觉过重。 +- ID、时间、哈希、编码等机器信息统一使用 13px、辅助色,可使用等宽字体。 +- 大段短信/彩信内容统一使用 14px、辅助色、行高 1.5-1.6;卡片内最多展示 3 行,详情弹窗再展示全文。 +- 字重优先使用 400 / 500 / 600 / 700,不使用 800 以上。 +- 不使用负字距;后台界面文字不随视口宽度缩放。 +- 中文按钮文字保持 2-6 个字为宜,动词优先,例如「预览」「编辑」「删除」「确认」。 + +## 间距 + +- 页面区块间距:20-24px。 +- 工具栏和列表间距:16-20px。 +- 卡片内边距:20px。 +- 卡片内部常规间距:10-12px。 +- 卡片顶部「名称 / ID / 标题」建议节奏: + - 名称到 ID:约 7-8px。 + - ID 到标题:约 7-8px。 + - 标题到媒体或正文:约 10-12px。 +- 正文到状态区域:约 8-12px;不要使用 `margin-top: auto` 把状态推到底部,除非卡片高度必须统一。 + +## 卡片 + +- 圆角统一不超过 8px。 +- 列表卡片使用 `1px` 边框,hover 可轻微阴影和边框变色。 +- 彩信/短信模板类卡片可用左侧 5px 状态色条,但不要再叠加过多装饰。 +- 业务标签放右上角时,要为标题保留右侧 padding,避免重叠。 +- 卡片底部操作条高度建议 48-52px,背景用浅灰 `#F4F6F8`。 + +## 表格 + +- 表头使用浅灰背景,字号 13-14px,字体 600。 +- 行高常规 64-86px,复杂两行信息可增加,但避免超过 110px。 +- 操作按钮采用文字或图标加文字,危险操作使用红色。 + +## 表单和弹窗 + +- 弹窗默认 `md`,复杂编辑器使用 `xl`。 +- 弹窗必须采用 Header / Body / Footer 三段布局,Footer 固定完整显示。 +- 表单字段在宽弹窗中可双列;编辑器内容、上传区、长文本保持单列。 +- Footer 按钮顺序:辅助操作、取消、确认。确认按钮在最右侧。 +- 弹窗内容高度超过视口时,只滚动 Body,不滚动 Header/Footer。 + +## 状态与反馈 + +- `已通过` 使用成功绿。 +- `审核中` 使用信息蓝。 +- `已驳回` 使用危险红。 +- `待报备 / 未开通 / 未知` 使用中性灰。 +- 同一行展示多个运营商状态时,使用「移动:Tag 联通:Tag 电信:Tag」的键值形式,便于横向扫描。 + +## 导航 + +### 左侧菜单 + +- 左侧菜单采用白底:背景使用 `#FFFFFF`,右侧使用 `1px #E5E7EB` 分隔线,不使用深色整栏背景。 +- 菜单宽度默认使用全局变量 `--sidebar-width: 248px`;折叠态宽度为 `84px`。 +- 侧边栏固定在视口内:高度 `100vh`,页面主内容滚动时菜单不跟随滚动。 +- 侧边栏内边距建议为上下 24px、左右 16px;菜单项之间保持紧凑,分组间距约 20px。 +- 品牌区左上固定展示,图标使用浅蓝底和蓝色图形,品牌文字使用强文本色,不再使用大面积品牌黑底。 + +### 菜单分组 + +- 分组标题使用 12px、600、辅助浅灰色,字距为 0,不使用全大写或过强颜色。 +- 分组标题左右内边距与菜单项保持对齐,建议 `0 12px`。 +- 分组标题只承担结构提示,不作为强视觉元素。 + +### 菜单项 + +- 菜单项高度由 `padding: 10px 12px` 控制,图标和文字间距约 12px。 +- 菜单项圆角使用 8px 或 `var(--radius-md)`,不要使用过大的胶囊形。 +- 默认文字使用主文本色,字重 500。 +- 菜单文字单行展示,超长时省略号,不换行,不撑开侧栏。 +- 图标使用线性图标,大小约 18-20px,颜色跟随文字状态。 + +### 选中与悬停 + +- 当前菜单和 hover 菜单统一使用浅蓝背景 `var(--color-selected-soft)`,文字和图标使用选中蓝 `#2563EB`。 +- 当前菜单左侧增加 `3px` 蓝色内嵌竖条:`box-shadow: inset 3px 0 0 var(--color-selected)`。 +- 选中态不使用投影,不使用大面积深蓝底,避免菜单视觉过重。 +- hover 只改变背景和文字颜色,不改变尺寸,避免菜单跳动。 + +### 菜单滚动条 + +- 分组较多时,只让菜单列表区域滚动,不让整个侧边栏或页面标题区域滚动。 +- 菜单滚动条默认隐藏:`scrollbar-color: transparent transparent`,WebKit thumb 透明。 +- 鼠标进入侧边栏时再显示滚动条,thumb 使用低透明蓝色:约 `rgba(37, 99, 235, 0.18)`。 +- 鼠标悬停在滚动区域时可略微加深到 `rgba(37, 99, 235, 0.22)`;拖拽/hover thumb 时可到 `0.34`。 +- 滚动条宽度建议 6px,靠近侧栏右边界显示;通过负 `margin-right` 和补偿 `padding-right` 让滚动条贴边但不遮挡菜单文字。 + +### 复用要求 + +- 客户端、运营端、系统管理类页面都应复用同一套左侧菜单规范。 +- 新增一级模块时,优先加入现有分组;只有业务域明显不同才新增分组。 +- 后续若抽离布局组件,应保留现有类名语义:`.sidebar`、`.side-nav`、`.side-nav-section`、`.side-nav-list`。 + +## 组件约定 + +- 按钮:使用 `Button`。 +- 输入框:使用 `Input` / `Textarea`。 +- 下拉:使用 `Select`。 +- 日期:使用 `DateRangeInput` / `DateTimeInput`。 +- 表格:使用 `Table`。 +- 状态:使用 `Tag`。 +- 弹窗:使用 `Modal`。 + +新增页面或改版时,先检查是否可以复用以上组件;只有现有组件无法表达业务交互时,才新增全局组件。 diff --git a/index.html b/index.html new file mode 100644 index 0000000..6c1ad74 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + CMPP 短信平台 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c4d977b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1126 @@ +{ + "name": "cmpp-platform-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cmpp-platform-frontend", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^6.0.2", + "dayjs": "^1.11.21", + "echarts": "^6.1.0", + "lucide-react": "^1.18.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.17.0", + "vite": "^8.0.16", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "typescript": "^6.0.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd96667 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "cmpp-platform-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-react": "^6.0.2", + "dayjs": "^1.11.21", + "echarts": "^6.1.0", + "lucide-react": "^1.18.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.17.0", + "vite": "^8.0.16", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "typescript": "^6.0.3" + } +} diff --git a/src/apps/admin/AdminAnalyticsPage.tsx b/src/apps/admin/AdminAnalyticsPage.tsx new file mode 100644 index 0000000..d64a21b --- /dev/null +++ b/src/apps/admin/AdminAnalyticsPage.tsx @@ -0,0 +1,123 @@ +import { useMemo } from 'react'; +import { BarChart3 } from 'lucide-react'; +import { Button, Chart, Tag } from '@/components/ui'; +import { auditTrend, channelShare, customerGrowth, hourlySendTrend } from '@/mock/chartData'; +import { adminService } from '@/mock'; +import { createBarOption, createLineOption, createPieOption } from '@/theme/chartOptions'; + +export function AdminAnalyticsPage() { + const overview = adminService.getOverview(); + const customers = adminService.getCustomers(); + + const sendTrendOption = useMemo( + () => createLineOption({ + labels: hourlySendTrend.map((item) => item.time), + series: [ + { name: '提交量', data: hourlySendTrend.map((item) => item.sent) }, + { name: '成功量', data: hourlySendTrend.map((item) => item.success) }, + ], + }), + [], + ); + + const auditTrendOption = useMemo( + () => createBarOption({ + labels: auditTrend.map((item) => item.day), + series: [ + { name: '通过', data: auditTrend.map((item) => item.approved) }, + { name: '驳回', data: auditTrend.map((item) => item.rejected) }, + { name: '待审', data: auditTrend.map((item) => item.pending) }, + ], + }), + [], + ); + + const customerGrowthOption = useMemo( + () => createLineOption({ + labels: customerGrowth.map((item) => item.month), + series: [ + { name: '活跃客户', data: customerGrowth.map((item) => item.active) }, + { name: '新增客户', data: customerGrowth.map((item) => item.new) }, + ], + }), + [], + ); + + const channelShareOption = useMemo(() => createPieOption({ data: channelShare }), []); + + return ( +
+
+
+

运营

+

数据统计

+
+ +
+ +
+
+ 提交量 + {overview.todaySubmissions} + 今日审核提交 +
+
+ 活跃客户 + {customers.filter((item) => item.status === 'active').length} + 当前平台客户 +
+
+ 通道健康度 + {overview.channelHealth}% + 近 24 小时 +
+
+ +
+
+
+
+

发送趋势

+

按 3 小时聚合短信提交与成功量。

+
+ 今日 +
+ +
+
+
+
+

通道占比

+

按提交量估算。

+
+ 通道 +
+ +
+
+ +
+
+
+
+

审核趋势

+

近 7 天审核处理情况。

+
+ 审核 +
+ +
+
+
+
+

客户增长

+

平台客户增长与活跃趋势。

+
+ 客户 +
+ +
+
+
+ ); +} diff --git a/src/apps/admin/AdminBillingPage.tsx b/src/apps/admin/AdminBillingPage.tsx new file mode 100644 index 0000000..a2023ec --- /dev/null +++ b/src/apps/admin/AdminBillingPage.tsx @@ -0,0 +1,38 @@ +import { Table, Tag, type TableColumn } from '@/components/ui'; +import { adminService, type Customer } from '@/mock'; + +const columns: Array> = [ + { key: 'id', title: '客户编号', render: (record) => record.id }, + { key: 'name', title: '客户名称', render: (record) => record.name }, + { key: 'balance', title: '短信余额', render: (record) => `${record.balance.toLocaleString('zh-CN')} 条` }, + { + key: 'amount', + title: '预估账户价值', + render: (record) => `¥${Math.round(record.balance * 0.06).toLocaleString('zh-CN')}`, + }, + { + key: 'status', + title: '账户状态', + render: (record) => ( + + {record.status === 'active' ? '正常' : '已停用'} + + ), + }, +]; + +export function AdminBillingPage() { + return ( +
+
+
+

配置

+

账单流水

+
+
+
+ + + + ); +} diff --git a/src/apps/admin/AdminChannelGroupFormPage.tsx b/src/apps/admin/AdminChannelGroupFormPage.tsx new file mode 100644 index 0000000..525a57e --- /dev/null +++ b/src/apps/admin/AdminChannelGroupFormPage.tsx @@ -0,0 +1,266 @@ +import { useMemo, useState } from 'react'; +import { Info, Plus, Trash2 } from 'lucide-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button, Input, Modal, Select, Table, Tag } from '@/components/ui'; +import type { TableColumn } from '@/components/ui'; + +type Carrier = 'mobile' | 'unicom' | 'telecom'; +type ChannelStatus = 'normal' | 'stopped'; +type ProvinceRoute = { + id: string; + province: string; + channel: string; + status: ChannelStatus; +}; +type NationalRoute = { + id: string; + priority: number; + channel: string; + status: ChannelStatus; +}; +type RouteModalState = { + type: 'province' | 'national'; + mode: 'create' | 'edit'; + route?: ProvinceRoute | NationalRoute; +}; + +const provinceOptions = [ + { label: '请选择', value: '' }, + { label: '山东', value: '山东' }, + { label: '河南', value: '河南' }, + { label: '北京', value: '北京' }, + { label: '上海', value: '上海' }, + { label: '广东', value: '广东' }, +]; + +const channelOptions = [ + { label: '请选择', value: '' }, + { label: '行北-移动-山东有限公司-上海XXXXXXX-22j', value: '行北-移动-山东有限公司-上海XXXXXXX-22j' }, + { label: '行北-移动-河南有限公司-上海XXXX-22j', value: '行北-移动-河南有限公司-上海XXXX-22j' }, + { label: '三网行北-黄峰-三网-编号3.3', value: '三网行北-黄峰-三网-编号3.3' }, + { label: '移动映华北-上海富煌C60289-移动2.7', value: '移动映华北-上海富煌C60289-移动2.7' }, +]; + +const priorityOptions = [ + { label: '请选择', value: '' }, + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + { label: '4', value: '4' }, + { label: '5', value: '5' }, +]; + +const carrierLabels: Record = { + mobile: '移动', + unicom: '联通', + telecom: '电信', +}; + +const statusLabels: Record = { + normal: '链接正常', + stopped: '通道停用', +}; + +const statusTones: Record = { + normal: 'success', + stopped: 'neutral', +}; + +const defaultProvinceRoutes: ProvinceRoute[] = [ + { id: 'p-shandong', province: '山东', channel: '行北-移动-山东有限公司-上海XXXXXXX-22j', status: 'normal' }, + { id: 'p-henan', province: '河南', channel: '行北-移动-河南有限公司-上海XXXX-22j', status: 'stopped' }, +]; + +const defaultNationalRoutes: NationalRoute[] = [ + { id: 'n-1', priority: 1, channel: '三网行北-黄峰-三网-编号3.3', status: 'normal' }, + { id: 'n-2', priority: 2, channel: '三网行北-黄峰(循环号用)-三网-编号3.4', status: 'normal' }, + { id: 'n-3', priority: 3, channel: '移动映华北-上海富煌C60289-移动2.7', status: 'stopped' }, +]; + +function StatusTag({ status }: { status: ChannelStatus }) { + return {statusLabels[status]}; +} + +function RouteConfigModal({ + modal, + onClose, + onSubmit, +}: { + modal: RouteModalState; + onClose: () => void; + onSubmit: (route: ProvinceRoute | NationalRoute) => void; +}) { + const provinceRoute = modal.type === 'province' ? modal.route as ProvinceRoute | undefined : undefined; + const nationalRoute = modal.type === 'national' ? modal.route as NationalRoute | undefined : undefined; + const [province, setProvince] = useState(provinceRoute?.province ?? ''); + const [priority, setPriority] = useState(nationalRoute ? String(nationalRoute.priority) : ''); + const [channel, setChannel] = useState(modal.route?.channel ?? ''); + + function submit() { + if (modal.type === 'province') { + onSubmit({ + id: provinceRoute?.id ?? `p-${Date.now()}`, + province: province || '山东', + channel: channel || channelOptions[1].value, + status: provinceRoute?.status ?? 'normal', + }); + return; + } + + onSubmit({ + id: nationalRoute?.id ?? `n-${Date.now()}`, + priority: Number(priority || 1), + channel: channel || channelOptions[1].value, + status: nationalRoute?.status ?? 'normal', + }); + } + + return ( + + + + + )} + onClose={onClose} + open + title={modal.mode === 'edit' ? '编辑通道' : '添加通道'} + > +
+ {modal.type === 'province' ? ( + setPriority(event.target.value)} options={priorityOptions} value={priority} /> +
+ + 失败补发会跳往下一优先级的通道,不会在同优先级的通道去补发 +
+ + )} + setGroupName(event.target.value)} placeholder="请输入通道组名称" value={groupName} /> +
+ * 运营商 + {(Object.keys(carrierLabels) as Carrier[]).map((item) => ( + + ))} +
+
+ * 失败补发 + +
+
+ + +
+

省网分流配置

+
+ + + +
+

全国通道配置

+
+ + + +
+ + +
+ + {modal ? setModal(null)} onSubmit={saveRoute} /> : null} + + ); +} diff --git a/src/apps/admin/AdminChannelGroupsPage.tsx b/src/apps/admin/AdminChannelGroupsPage.tsx new file mode 100644 index 0000000..9afd28c --- /dev/null +++ b/src/apps/admin/AdminChannelGroupsPage.tsx @@ -0,0 +1,157 @@ +import { useMemo, useState } from 'react'; +import { Edit3, Layers3, Plus, Search, Trash2, UsersRound } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { Button, Input, Pagination } from '@/components/ui'; + +type ChannelGroup = { + id: string; + name: string; + count: number; + channels: string[]; +}; + +const initialGroups: ChannelGroup[] = [ + { + id: 'medical-a', + name: '学医三网专用群', + count: 16, + channels: [ + '三网行北-黄峰-三网-编号3.3', + '三网行北-黄峰(循环号用)-三网-编号3.4', + '移动映华北-上海富煌C60289-移动2.7', + '三网行北-北京富惠互联-三网-编号3.5', + '移动行北-上海悉斯4pp0131-移动2.8', + ], + }, + { + id: 'medical-b', + name: '学医三网专用群', + count: 5, + channels: [ + '三网行北-黄峰-三网-编号3.3', + '三网行北-黄峰(循环号用)-三网-编号3.4', + '移动映华北-上海富煌C60289-移动2.7', + ], + }, + { + id: 'group-a', + name: 'XXXX通道组', + count: 3, + channels: [ + '三网行北-黄峰-三网-编号3.3', + '三网行北-黄峰(循环号用)-三网-编号3.4', + ], + }, + { + id: 'group-b', + name: 'XXXX通道组', + count: 1, + channels: ['三网行北-黄峰-三网-编号3.3'], + }, + { + id: 'test-a', + name: '测试通道组A', + count: 8, + channels: [ + '三网行北-黄峰-三网-编号3.3', + '移动映华北-上海富煌C60289-移动2.7', + ], + }, + { + id: 'test-b', + name: '测试通道组B', + count: 12, + channels: ['三网行北-黄峰-三网-编号3.3'], + }, + { + id: 'test-c', + name: '测试通道组C', + count: 6, + channels: [ + '三网行北-黄峰-三网-编号3.3', + '三网行北-黄峰(循环号用)-三网-编号3.4', + ], + }, + { + id: 'test-d', + name: '测试通道组D', + count: 2, + channels: ['三网行北-黄峰-三网-编号3.3'], + }, +]; + +export function AdminChannelGroupsPage() { + const navigate = useNavigate(); + const [groupName, setGroupName] = useState(''); + const [channelKeyword, setChannelKeyword] = useState(''); + const [groups, setGroups] = useState(initialGroups); + + const filteredGroups = useMemo(() => groups.filter((group) => { + const nameMatched = group.name.includes(groupName.trim()); + const channelMatched = group.channels.some((channel) => channel.includes(channelKeyword.trim())); + return nameMatched && channelMatched; + }), [channelKeyword, groupName, groups]); + + function resetFilters() { + setGroupName(''); + setChannelKeyword(''); + } + + function removeGroup(id: string) { + setGroups((current) => current.filter((group) => group.id !== id)); + } + + return ( +
+
+
+
通道管理 / 短信通道组管理
+

短信通道组管理

+
+ +
+ +
+ setGroupName(event.target.value)} placeholder="请输入通道组名称" value={groupName} /> + setChannelKeyword(event.target.value)} placeholder="请输入包含的通道" value={channelKeyword} /> +
+ + +
+
+ +
+
+ {filteredGroups.map((group) => ( +
+
+
+ + {group.name} +
+ {group.count} +
+
+ {group.channels.slice(0, 5).map((channel) => ( +

{channel}

+ ))} + {group.count > group.channels.length ? 还有 {group.count - group.channels.length} 个通道... : null} +
+
+ + +
+
+ ))} +
+ +
+
+ ); +} diff --git a/src/apps/admin/AdminChannelReportPage.tsx b/src/apps/admin/AdminChannelReportPage.tsx new file mode 100644 index 0000000..412b63e --- /dev/null +++ b/src/apps/admin/AdminChannelReportPage.tsx @@ -0,0 +1,565 @@ +import { useMemo, useState } from 'react'; +import { + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronUp, + Eye, + FileSliders, + GripVertical, + Pencil, + Plus, + Search, + Settings2, + Trash2, +} from 'lucide-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button, DateRangeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui'; +import type { DateRangeValue } from '@/components/ui'; + +type ReportStatus = 'success' | 'failed' | 'reporting' | 'unreported' | 'withdrawn' | 'abandoned'; + +type DeliveryStats = { + successRate: number; + successCount: number; + unknownRate: number; + unknownCount: number; + failureRate: number; + failureCount: number; +}; + +type DrainageReport = { + id: string; + value: string; + status: ReportStatus; + submittedAt: string; + reportedAt?: string; + lastSentAt?: string; + stats: DeliveryStats; + remark?: string; +}; + +type SignatureReport = { + id: string; + name: string; + status: ReportStatus; + submittedAt: string; + reportedAt?: string; + lastSentAt?: string; + stats: DeliveryStats; + drainage: DrainageReport[]; + details: SignatureDetails; + remark?: string; +}; + +type SignatureDetails = { + basis: string; + companyName: string; + creditCode: string; + legalName: string; + legalId: string; + contactName: string; + contactPhone: string; + contactId: string; +}; + +type ReportDetail = + | { kind: 'signature'; report: SignatureReport } + | { kind: 'drainage'; title: string; status: ReportStatus; submittedAt: string; reportedAt?: string; lastSentAt?: string }; + +type ReportField = { + id: string; + label: string; + type: '文本' | '图片' | '文件'; +}; + +type SelectedReportField = ReportField & { + required: boolean; + mapping: string; +}; + +const channelNames: Record = { + '88827': '行北-集市三甲医院-39', + '77': '移动-行北-上海甲医院-38', + '78': '联通-行政-杭州甲医院-37', + '67': '联通-行政-上海甲医院-34', +}; + +const statusOptions = [ + { label: '全部状态', value: 'all' }, + { label: '报备成功', value: 'success' }, + { label: '报备失败', value: 'failed' }, + { label: '报备中', value: 'reporting' }, + { label: '未报备', value: 'unreported' }, + { label: '被清退', value: 'withdrawn' }, + { label: '放弃报备', value: 'abandoned' }, +]; + +const statusMeta: Record = { + success: { label: '报备成功', tone: 'success' }, + failed: { label: '报备失败', tone: 'danger' }, + reporting: { label: '报备中', tone: 'warning' }, + unreported: { label: '未报备', tone: 'neutral' }, + withdrawn: { label: '被清退', tone: 'danger' }, + abandoned: { label: '放弃报备', tone: 'warning' }, +}; + +const statusChoices: Array<{ value: ReportStatus; label: string; className: string }> = [ + { value: 'unreported', label: '未报备', className: 'is-neutral' }, + { value: 'reporting', label: '报备中', className: 'is-info' }, + { value: 'success', label: '报备成功', className: 'is-success' }, + { value: 'failed', label: '报备失败', className: 'is-danger' }, + { value: 'withdrawn', label: '被清退', className: 'is-danger' }, + { value: 'abandoned', label: '放弃报备', className: 'is-warning' }, +]; + +const drainageFieldPool: ReportField[] = [ + { id: 'businessScope', label: '营业范围', type: '文本' }, + { id: 'legalName', label: '法人姓名', type: '文本' }, + { id: 'legalPhone', label: '法人手机号', type: '文本' }, + { id: 'legalIdImage', label: '法人身份证图片', type: '图片' }, + { id: 'managerIdImage', label: '经办人身份证图片', type: '图片' }, + { id: 'managerPhone', label: '经办人手机号', type: '文本' }, + { id: 'managerName', label: '经办人姓名', type: '文本' }, + { id: 'creditCode', label: '统一社会信用代码', type: '文本' }, + { id: 'licenseImage', label: '营业执照图片', type: '图片' }, + { id: 'brandName', label: '品牌名称', type: '文本' }, + { id: 'drainageInfo', label: '引流信息', type: '文本' }, + { id: 'companyAddress', label: '公司地址', type: '文本' }, + { id: 'authorization', label: '授权证明', type: '文件' }, +]; + +const initialSelectedDrainageFields: SelectedReportField[] = [ + { id: 'companyName', label: '公司名称', type: '文本', required: false, mapping: '' }, + { id: 'legalId', label: '法人身份证号', type: '文本', required: false, mapping: '' }, +]; + +const emptyStats: DeliveryStats = { + successRate: 0, + successCount: 0, + unknownRate: 0, + unknownCount: 0, + failureRate: 0, + failureCount: 0, +}; + +const initialReports: SignatureReport[] = [ + { + id: 'sig-1', + name: '中华长城签名1', + status: 'failed', + submittedAt: '2025-12-28 18:08:08', + reportedAt: '2025-12-29 12:03:01', + lastSentAt: '2025-12-30 08:13:21', + stats: { successRate: 88.2, successCount: 1130200, unknownRate: 29.1, unknownCount: 372940, failureRate: 8, failureCount: 102480 }, + details: { basis: '企业自用签名', companyName: '示例科技有限公司', creditCode: '91110000XXXXXXXXXX', legalName: '张三', legalId: '110101199001011234', contactName: '李四', contactPhone: '13800138000', contactId: '110101199002021234' }, + drainage: [ + { id: 'flow-1', value: '400-123-4567', status: 'success', submittedAt: '2025-12-28 18:08:08', reportedAt: '2025-12-28 18:08:08', lastSentAt: '2025-12-28 18:08:08', stats: { ...emptyStats, failureRate: 100, failureCount: 50 } }, + { id: 'flow-2', value: 'www.example.com', status: 'success', submittedAt: '2025-12-28 18:08:08', reportedAt: '2025-12-28 18:08:08', lastSentAt: '2025-12-28 18:08:08', stats: { ...emptyStats, failureRate: 100, failureCount: 54 } }, + { id: 'flow-3', value: 'service@example.com', status: 'success', submittedAt: '2025-12-28 18:08:08', reportedAt: '2025-12-28 18:08:08', lastSentAt: '2025-12-28 18:08:08', stats: { ...emptyStats, failureRate: 100, failureCount: 50 } }, + { id: 'flow-4', value: '18912345678', status: 'unreported', submittedAt: '2025-12-28 18:08:08', stats: emptyStats }, + ], + }, + { + id: 'sig-2', + name: '医长长长长长长长长长长', + status: 'success', + submittedAt: '2025-12-28 18:08:08', + reportedAt: '2025-12-29 12:03:01', + stats: emptyStats, + details: { basis: '企业自用签名', companyName: '上海医长信息科技有限公司', creditCode: '91310000XXXXXXXXXX', legalName: '王强', legalId: '310101198805061234', contactName: '赵敏', contactPhone: '13900139000', contactId: '310101199006081234' }, + drainage: [ + { id: 'flow-5', value: '18912345678', status: 'unreported', submittedAt: '2025-12-28 18:08:08', stats: emptyStats }, + ], + }, + { + id: 'sig-3', + name: '国信委科技服务', + status: 'reporting', + submittedAt: '2025-12-28 18:08:08', + reportedAt: '2025-12-29 12:03:01', + lastSentAt: '2025-12-30 08:13:21', + stats: { successRate: 78.2, successCount: 8804, unknownRate: 1.2, unknownCount: 2046, failureRate: 5.8, failureCount: 1916 }, + details: { basis: '企事业单位全称或简称', companyName: '国信委科技服务有限公司', creditCode: '91110108XXXXXXXXXX', legalName: '陈杰', legalId: '110108198812121234', contactName: '周宁', contactPhone: '13700137000', contactId: '110108199103151234' }, + drainage: [], + }, +]; + +function DateTime({ value }: { value?: string }) { + if (!value) return -; + const [date, time] = value.split(' '); + return {date}{time}; +} + +function Stats({ stats }: { stats: DeliveryStats }) { + return ( +
+ 成功 {stats.successRate}%{stats.successCount.toLocaleString('zh-CN')} + 未知 {stats.unknownRate}%{stats.unknownCount.toLocaleString('zh-CN')} + 失败 {stats.failureRate}%{stats.failureCount.toLocaleString('zh-CN')} +
+ ); +} + +function RowActions({ onDelete, onStatus, onView }: { onDelete: () => void; onStatus: () => void; onView: () => void }) { + return ( +
+ + + +
+ ); +} + +function ReadonlyUpload({ label }: { label: string }) { + return ( +
+ {label} +
已上传
+
+ ); +} + +function SignatureDetailModal({ report, onClose }: { report: SignatureReport; onClose: () => void }) { + const details = report.details; + return ( + 关闭} + onClose={onClose} + open + size="xl" + title={

查看签名详情

查看短信签名的详细信息

} + > +
+
+

基本信息

+
+ + +
+
+ +
+

公司信息

+
+ + + + + + +
+
+ +
+

责任人信息

+
+ + + + + +
+
+
+
+ ); +} + +function DrainageFieldConfigModal({ + fields, + onChange, + onClose, +}: { + fields: SelectedReportField[]; + onChange: (fields: SelectedReportField[]) => void; + onClose: () => void; +}) { + const [draft, setDraft] = useState(fields); + const [searchText, setSearchText] = useState(''); + const availableFields = useMemo(() => drainageFieldPool.filter((field) => ( + !draft.some((item) => item.id === field.id) + && (!searchText || field.label.includes(searchText)) + )), [draft, searchText]); + + function addField(field: ReportField) { + setDraft((items) => [...items, { ...field, required: false, mapping: '' }]); + } + + function updateField(id: string, patch: Partial) { + setDraft((items) => items.map((item) => item.id === id ? { ...item, ...patch } : item)); + } + + function moveField(index: number, offset: number) { + setDraft((items) => { + const target = index + offset; + if (target < 0 || target >= items.length) return items; + const next = [...items]; + [next[index], next[target]] = [next[target], next[index]]; + return next; + }); + } + + return ( + + + + + )} + onClose={onClose} + open + size="xl" + title={

配置报备字段

从字段池中选择字段,并配置是否必填

} + > +
+
+
+

字段池

+ {availableFields.length} 个可选 +
+ setSearchText(event.target.value)} placeholder="搜索字段..." prefix={} value={searchText} /> +
+ {availableFields.map((field) => ( + + ))} + {availableFields.length === 0 ?

没有可添加的字段

: null} +
+
+ +
+
+

已选择字段

可调整顺序和设置必填项

+ {draft.length} 个 +
+
+ {draft.map((field, index) => ( +
+
+ {index + 1} + + {field.label} + {field.type} +
+ + +
+
+
+ + + +
+ updateField(field.id, { mapping: event.target.value })} placeholder="请输入映射字段名" value={field.mapping} /> +
+ ))} + {draft.length === 0 ?
请从左侧添加报备字段
: null} +
+
+
+
+ ); +} + +export function AdminChannelReportPage() { + const navigate = useNavigate(); + const { channelId = '88827' } = useParams(); + const [reports, setReports] = useState(initialReports); + const [keyword, setKeyword] = useState(''); + const [status, setStatus] = useState('all'); + const [dateRange, setDateRange] = useState({}); + const [expanded, setExpanded] = useState>(() => new Set(['sig-1'])); + const [selectedIds, setSelectedIds] = useState>(() => new Set()); + const [statusTarget, setStatusTarget] = useState<{ signatureId: string; drainageId?: string } | null>(null); + const [nextStatus, setNextStatus] = useState('success'); + const [nextRemark, setNextRemark] = useState(''); + const [detail, setDetail] = useState(null); + const [fieldConfigOpen, setFieldConfigOpen] = useState(false); + const [drainageFields, setDrainageFields] = useState(initialSelectedDrainageFields); + + const filteredReports = useMemo(() => reports.filter((report) => { + const matchesKeyword = !keyword || report.name.includes(keyword) || report.drainage.some((item) => item.value.includes(keyword)); + const matchesStatus = status === 'all' || report.status === status; + const date = report.submittedAt.slice(0, 10); + const matchesStart = !dateRange.start || date >= dateRange.start; + const matchesEnd = !dateRange.end || date <= dateRange.end; + return matchesKeyword && matchesStatus && matchesStart && matchesEnd; + }), [dateRange.end, dateRange.start, keyword, reports, status]); + + function toggleExpanded(id: string) { + setExpanded((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + } + + function toggleSelected(id: string) { + setSelectedIds((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + } + + function removeItem(signatureId: string, drainageId?: string) { + setReports((items) => drainageId + ? items.map((item) => item.id === signatureId ? { ...item, drainage: item.drainage.filter((flow) => flow.id !== drainageId) } : item) + : items.filter((item) => item.id !== signatureId)); + } + + function applyStatus() { + if (!statusTarget) return; + setReports((items) => items.map((item) => { + if (item.id !== statusTarget.signatureId) return item; + if (!statusTarget.drainageId) return { ...item, status: nextStatus, remark: nextRemark }; + return { ...item, drainage: item.drainage.map((flow) => flow.id === statusTarget.drainageId ? { ...flow, status: nextStatus, remark: nextRemark } : flow) }; + })); + setStatusTarget(null); + setNextRemark(''); + } + + function openStatus(signatureId: string, currentStatus: ReportStatus, drainageId?: string, remark = '') { + setNextStatus(currentStatus); + setNextRemark(remark); + setStatusTarget({ signatureId, drainageId }); + } + + return ( +
+
+
短信通道管理/通道报备详情
+
+ +

{channelNames[channelId] ?? `短信通道 ${channelId}`}

+ +
+ + +
+
+
+ +
+
+ setKeyword(event.target.value)} placeholder="请输入签名、网址、电话或邮箱" value={keyword} /> + toggleSelected(report.id)} type="checkbox" /> +
+ + 【{report.name}】引流 {report.drainage.length} +
+ {statusMeta[report.status].label} + + + + + removeItem(report.id)} onStatus={() => openStatus(report.id, report.status, undefined, report.remark)} onView={() => setDetail({ kind: 'signature', report })} /> +
+ {isExpanded ? report.drainage.map((flow) => ( +
+ +
{flow.value}
+ {statusMeta[flow.status].label} + + + + + removeItem(report.id, flow.id)} onStatus={() => openStatus(report.id, flow.status, flow.id, flow.remark)} onView={() => setDetail({ kind: 'drainage', title: flow.value, ...flow })} /> +
+ )) : null} +
+ ); + })} + {filteredReports.length === 0 ?
暂无符合条件的报备记录
: null} + + + {statusTarget ? ( + } + onClose={() => setStatusTarget(null)} + open + size="md" + title={

更改报备状态

选择新的报备状态并添加备注。

} + > +
+
+ {statusChoices.map((choice) => ( + + ))} +
+