feat: integrate platform backend and application interfaces

This commit is contained in:
Qiufeng
2026-09-02 21:09:47 +08:00
parent cd9ac1af70
commit b16390dd41
648 changed files with 102488 additions and 17737 deletions
+32
View File
@@ -0,0 +1,32 @@
# Public/local defaults only. Never put provider keys, payment signatures, or
# SMTP/SMS credentials in this file.
MIRAGENFLOW_HOST=127.0.0.1
MIRAGENFLOW_PORT=3100
MIRAGENFLOW_ACCESS_TOKEN_SECRET=replace-with-a-random-secret
MIRAGENFLOW_ACCESS_TOKEN_TTL_SECONDS=900
MIRAGENFLOW_REFRESH_COOKIE=miragenflow_refresh
MIRAGENFLOW_COOKIE_SECURE=false
MIRAGENFLOW_ADMIN_EMAIL=admin@admin.com
MIRAGENFLOW_ADMIN_PASSWORD=admin
MIRAGENFLOW_ADMIN_MFA_REQUIRED=false
MIRAGENFLOW_ADMIN_MFA_SECRET=
MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED=false
# Development only: let the local super administrator execute its own approval.
# Production always requires another financial administrator.
MIRAGENFLOW_ADMIN_SELF_APPROVAL=true
MIRAGENFLOW_USER_MFA_REQUIRED=false
MIRAGENFLOW_USER_VERIFICATION_REQUIRED=false
MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED=false
MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED=false
MIRAGENFLOW_CAPTCHA_REQUIRED=false
# Development only: allow provider emulators on localhost/private networks.
# Keep false in production (the production launcher forces this closed).
MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS=true
MIRAGENFLOW_PERSISTENCE_ADAPTER=memory
MIRAGENFLOW_QUEUE_ADAPTER=memory
DATABASE_URL=
REDIS_URL=
MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY=replace-with-a-separate-random-key
MIRAGENFLOW_PAYMENT_WEBHOOK_SECRET=replace-with-a-random-webhook-secret
# Local fixture only: comma-separated channel ids to force retryable failures.
MIRAGENFLOW_FIXTURE_FAIL_CHANNELS=
+20 -6
View File
@@ -21,8 +21,8 @@
## 前端规范
- 前端使用 Vite、React、React Router、TypeScript、Ant Design、Tailwind、Zustand。
- 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。
- 前端使用 Vite、React、React Router、TypeScript、TDesign、Tailwind、Zustand。
- 编写 TDesign 相关代码时,优先结合项目当前锁定版本、共享 `packages/ui` Provider 和既有适配层写法;不要重新引入 Ant Design 或其他普通控件库。
- 外部服务请求统一放在 `web/src/services/api/`,由浏览器前端直连,不假设存在项目后端。
- 全局或跨页面状态优先放在 `web/src/stores/`。
- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。
@@ -34,11 +34,24 @@
- 不要新增只做简单转发的组件,例如只 `return <X>{children}</X>` 或只换个名字透传 props;直接在使用处使用真实组件或把逻辑写进当前文件。
- 页面私有 hook 放在对应页面目录下,例如 `admin/assets/use-admin-assets.ts`;只有多个页面真实复用的 hook 才放到外层 `hooks/`。
- 管理后台页面私有组件放到各自页面目录的 `components/` 下,例如 `admin/assets/components/`、`admin/prompts/components/`;不要为了单页面使用放到 `admin/components/` 共享目录。
- 管理后台主题、背景、卡片阴影、表格配色等统一在 `web/src/lib/app-theme.ts`、`AppProviders` 或必要的全局 CSS 作用域中配置;页面私有组件不要自己写 `dark ? ...` 主题分支。
- Ant Design 的 Dropdown、Menu、Select、Cascader、TreeSelect 等弹层背景、悬停态和选中态颜色统一通过 `web/src/lib/app-theme.ts` 的全局 Alias Token 与组件 Token 配置;不要在业务组件内为单个弹层覆盖颜色。
- 前后台主题、背景、卡片阴影、表格配色等统一在 `web/src/lib/tdesign-theme.ts`、`AppProviders` 或必要的全局 CSS 作用域中配置;页面私有组件不要自己写 `dark ? ...` 主题分支。
- TDesign 的 Dropdown、Menu、Select、Cascader、TreeSelect 等弹层背景、悬停态和选中态颜色统一通过 `web/src/lib/tdesign-theme.ts` 的主题变量与组件 Token 配置;不要在业务组件内为单个弹层覆盖颜色。
- 组件优先使用函数组件和现有 hooks,不新增大型状态管理方案。
- UI 图标优先使用 `lucide-react` 或项目已经使用的 Ant Design 图标。
- 前后台通用 UI 图标统一使用项目内本地化的 Lineicons 子集;只收录实际使用的图标,禁止引入 CDN、WebFont、完整图标目录或其他业务图标库。品牌标识、模型标识、媒体内容和画布连线等具有独立语义的视觉资产不按通用 UI 图标替换。
- 用户端业务下拉不得使用浏览器原生 `<select>`;统一使用项目自有的主题化 GUI 下拉组件,必须保留键盘选择、当前选中态、禁用/空状态、点击外部关闭、Escape 关闭和移动端可用性。需要搜索或多选的复杂场景也必须使用带主题的 GUI 组件。
- 用户端主导航、用户菜单和侧栏账户操作必须显式设置 `justify-content: flex-start` 与 `text-align: left`;仅设置文字对齐不能覆盖通用按钮的居中布局。
- 管理后台的菜单、页头、登录和业务操作图标统一使用 `admin/src/components/LineIcons.jsx` 中本地化的 Lineicons 子集;禁止重新引入 CDN、WebFont、全量图标目录或 `tdesign-icons-react` 业务引用。菜单、页头和主要业务操作图标默认 24px,并同步保留至少 40px 的稳定点击区域。
- 管理台全屏登录路由必须直接挂载登录主体,不得继承后台 `Layout/Content` 壳层或新增移动端卡片容器;手机端沿用桌面左侧内容轨道,仅按视口缩放,不另起一套布局。
- 页面文案保持中文。
- 管理后台的业务字段必须经过中文映射后再展示;对象、数组和余额等结构化数据使用字段化组件呈现,禁止直接把原始 JSON 输出到页面。用户列表的 `id` 在表头为 `ID` 时按当前列表顺序显示为从 1 开始的纯数字;模型、套餐、渠道等其他资源标识保留其真实语义,不得擅自改成连续编号。
- 渠道模型映射必须明确区分平台显示模型 ID 与供应商请求模型 ID;显示 ID 用于匹配公开模型产品,请求 ID 只用于发送给上游,不能在管理台或服务端把两者合并成一个字段。
- 渠道请求地址表单必须给出完整 URL 示例(如 `https://example.com`、`https://example.com/v1`);API Key 只校验去除首尾空白后非空,不得设置固定最小长度,编辑留空表示沿用原凭证。
- 渠道模型列表只能由用户点击按钮主动拉取,新增或编辑保存不得隐式发起上游请求;动态模型映射行必须使用创建时稳定的 React key,不得使用正在编辑的输入值作为 key,避免输入过程中组件重建而丢失焦点。
- 渠道模型列表与模型映射必须独立:拉取结果只能作为候选模型供用户勾选并添加到渠道模型列表,禁止自动创建映射;模型映射是可选的,仅用于平台显示模型 ID 与供应商请求模型 ID 不同时的转换。
- 管理后台使用受控字段时,不得用无 `name` 的 TDesign `FormItem` 直接包裹控件,避免其内部状态覆盖外部 `value`;要么完整使用 `Form` 作为唯一数据源,要么使用普通布局容器保持单一状态源。
- 管理后台所有新增、编辑、确认和提示弹窗都必须支持取消、右上角关闭、遮罩关闭和 ESC 关闭;异步操作成功后自动关闭,失败时保留表单并显示中文错误,不得依赖刷新页面清除弹窗。
- 用户金币调整必须提供独立的增加、扣减图标入口;弹窗只让管理员填写正整数数量和原因,并显示当前可用金币与审批后预计余额,禁止要求管理员用正负号表达方向。
- 管理后台对外部供应商的探活或模型拉取必须显示独立加载状态、实际请求路径和请求是否已发起;失败原因要用中文区分地址解析、网络连接、HTTP 权限/状态和返回内容问题,不得只显示笼统的“探测未通过”。
- 不要在组件里堆太多无关逻辑;复杂逻辑优先抽成同目录工具函数或小组件。
- 样式优先由组件自己管理;组件私有样式优先使用 Tailwind className 或少量内联 style,不要为单个组件新增大量全局 CSS。
- 全局 CSS 只放基础变量、全局重置、跨页面通用样式和少量第三方组件必要覆盖;不要在 `globals.css` 堆页面私有样式。
@@ -48,7 +61,7 @@
## 画布 UI 规范
- 做 canvas 前端 UI 时必须遵循当前画布主题。
- 优先使用 `canvasThemes`、`useThemeStore` 或 Ant Design `ConfigProvider` token。
- 优先使用 `canvasThemes`、`useThemeStore`、共享 `TDesignProvider` 或 TDesign 主题变量。
- 不要硬编码黑白、stone、slate 等颜色导致浅色/深色主题不一致。
- 新增画布按钮、弹窗、浮层时,尽量复用已有工具栏、节点面板、Modal 的视觉风格。
- 画布顶部工具栏和状态信息优先采用极简扁平风格:无边框、无阴影、无胶囊背景,融入整体背景,弱化按钮感,仅保留轻微 hover 反馈,保持简洁现代、低视觉重量。
@@ -86,3 +99,4 @@
- Agent 对话消息必须同时按 `threadId`、`turnId` 和 `itemId` 归属;实时事件只用于补充未物化的 turn,历史快照成为权威后不得重复合并同一条消息。
- Agent 通信协议版本与消息存储版本必须独立管理;消息存储格式升级时必须先备份再迁移,遇到未知版本、损坏清单或冲突备份时拒绝覆盖原文件,不得按记录数量或文件大小静默裁剪历史元数据。
- 本地启动或浏览器验收时不要关闭用户已经打开的浏览器窗口或标签页;需要自动化验证时使用独立测试页面,避免打断用户当前页面和对话状态。
- 开发环境不得自动创建展示用 mock 渠道;需要渠道数据时由用户显式新增,自动化测试专用 fixture 不受此限制。
+521 -217
View File
@@ -2,330 +2,634 @@
## Unreleased
- [修复] 管理台请求在会话刷新后会复用原幂等键,渠道生成测试会将平台显示模型 ID 转换为对应的供应商请求模型 ID,避免写操作重复或上游模型路由错误。
- [修复] 用户端画布裁剪、切分、旋转和放大遇到损坏图片时会在超时后失败并显示中文错误,不再永久卡在处理中。
- [修复] 用户端账户密码与两步验证请求现在会在弹窗关闭或返回上一步后失效,错误保留在对应弹窗内;找回密码验证码响应绑定发起时的账户目标,避免切换方式后串用结果。
- [修复] 用户端用户菜单改为由 TDesign 下拉组件维护唯一可见状态,路由切换后不会因外部状态与内部状态分叉而无法重新打开;两步验证长内容在窄屏弹窗内可滚动查看。
- [修复] 用户端画布裁剪/蒙版、资产文件选择和认证验证码请求现在会忽略过期异步响应;文件读取或上传失败会保留当前草稿并显示中文错误。
- [修复] 用户端画布放大和切分编辑器现在会忽略过期图片尺寸响应,快速切换图片时不会串用上一张图片的比例或网格尺寸。
- [调整] 图片创作和管理台对账的文件上传入口改用项目 TDesign Button,隐藏文件输入仅保留为浏览器文件 API 适配,上传校验与预览流程不变。
- [修复] 管理台和用户端窄屏抽屉/导航遮罩现在真正可点击关闭;管理台抽屉在手机宽度下保留遮罩区域,关闭后焦点回到触发按钮。
- [修复] 管理员与普通用户的认证引导请求可恢复缺少 CSRF Cookie 的旧会话,刷新成功后补发 CSRF Cookie,同时继续拒绝没有 CSRF 证明的业务写请求。
- [修复] 用户端命令式确认弹窗改用受控 TDesign Dialog 挂载,修复 React 19 下会话撤销、任务取消和结果删除确认弹窗不显示的问题,并保留取消、遮罩、ESC、加载和焦点回收行为。
- [修复] 共享 TDesign Button 适配器为普通按钮显式保留 `button` 标签,禁用态不再退化为无按钮语义的 `div`,同时继续保留链接按钮的自动标签行为。
- [修复] 管理台通用确认操作增加异常兜底,失败时保留确认弹窗并显示中文错误,避免冻结、重试和取消等操作出现未处理请求。
- [修复] 用户端确认弹窗适配器统一捕获异步回调异常,失败时保留弹窗并显示中文错误,避免会话撤销、任务取消和结果删除产生未处理 Promise。
- [修复] 用户端本地存储统计读取失败时显示中文错误并提供加载状态,避免浏览器存储异常产生未处理请求。
- [修复] 管理台编辑、审批、调账、退款和对账弹窗统一捕获异步失败并保留草稿,显示中文错误提示,避免请求异常导致操作无反馈。
- [修复] 用户端首页轮播不再展示旧产品或未纳入 V1 的 3D 宣传横幅,改用当前创作工作台的角色、场景和资产参考图并保留轮播切换。
- [调整] 普通开发环境不再自动注入模拟支付适配器,支付 fixture 仅在测试模式或显式测试模式下启用,避免管理台把模拟业务误认为真实配置。
- [修复] 用户端共享 TDesign Switch 不再生成嵌套 label,带文字开关继续支持文字点击、可读名称和受控状态。
- [修复] 管理台日志大板和详细日志在存在审计权限时按实际动作显示日志详情操作列,并保持右侧固定列宽。
- [优化] 用户端画布项目卡片补齐可聚焦的链接语义和 Enter 键打开行为,管理台品牌 Logo 改用共享 TDesign Button 并保留首页跳转。
- [新增] 用户端主工作台头部补齐浅色/深色主题切换入口,与画布使用同一主题状态和动画按钮。
- [修复] 用户端共享 TDesign 文本输入适配器补齐受控 Input/TextArea 的原生输入事件回写,避免认证、资产、设置和画布草稿输入后回弹为空。
- [调整] 用户端开发组件中心移除未纳入 V1 的三维渲染规划卡,避免展示超出当前产品范围的能力占位。
- [修复] 用户端画布互斥选项组的 ARIA 容器语义与实际的 TDesign 按钮状态保持一致,避免将 `aria-pressed` 按钮错误声明为 `radiogroup`。
- [优化] 用户端画布资源提及菜单补齐 Home/End 导航和垂直 listbox 语义,继续通过 `aria-activedescendant` 保持输入框与活动项同步。
- [优化] 管理台渠道分组的模型产品和供应渠道多选列表补齐 listbox 键盘导航,支持方向键、Home/End、Enter/空格和跳过禁用项,并保持多选状态与 Checkbox 一致。
- [优化] 管理台页面设置的主题色、主题模式和导航布局选项补齐 radio 语义、方向键、Home/End 和焦点轮转,颜色选择器支持键盘打开。
- [修复] 管理台主题色选择器改用明确的受控 `value` 属性,重新打开页面设置时会始终跟随当前主题色。
- [优化] 管理台页面设置抽屉关闭后会将焦点回收到触发按钮,登录页和后台布局的 ESC、遮罩及关闭按钮行为保持一致。
- [修复] 管理台全屏登录页现在也挂载页面设置抽屉,主题模式和布局设置不再因登录路由绕过后台外壳而失效。
- [修复] 共享 UI 包补齐 `@miragenflow/ui/button` 子路径,确保 Button 适配器可按约定的独立入口解析。
- [调整] 用户端任务历史列表改用共享 TDesign Table,保留任务链接、状态标签、模型名称、创建时间和移动端横向浏览行为。
- [优化] 用户端 Select 调用统一改用结构化 options,焦点管理器移除原生 select 分支,继续由共享 TDesign 适配层渲染下拉并保留键盘与受控值行为。
- [修复] 管理台结构化详情现在只渲染已登记的中文字段,未知嵌套字段不会显示为未映射内容。
- [修复] 管理台用户详情各 Tab 的动态表格同步过滤未登记字段,避免异常响应生成“其他信息”列。
- [修复] 管理台登录提交按钮改用普通布局容器,不再作为无名表单字段参与 TDesign Form 状态管理。
- [修复] 用户端资产 ZIP 导入会在取得文件后立即清空选择值,导入进行中或失败后重新选择同一文件也能再次触发。
- [修复] 本地文件快照的同步与异步保存现在使用统一原子写入和唯一临时文件,避免并发保存导致 `/api/ready` 进入不可用状态。
- [修复] 渠道分组任务模式选择器不再展示 `image`、`text` 等内部能力值,仅保留中文模式名称并继续提交真实能力标识。
- [调整] 管理台登录、业务表格、导航布局和设置网格改用共享 TDesign 入口,保持权限、动态列、拖拽排序和表单实例行为不变。
- [修复] 用户端主题化 Select 现在补齐 combobox/listbox 的 ARIA 关系、Enter/空格打开与选择、方向键/Home/End 导航、Escape 关闭和活动项反馈;管理端低风险 Dropdown、Space、Breadcrumb、Popup、日期范围与颜色选择器改用共享 UI 入口。
- [新增] 共享 UI 包新增 Card、Statistic 适配器,管理台看板、业务图表和统计卡片改用共享入口并保留现有样式契约。
- [调整] 管理台错误页、登录页、导航、移动菜单和业务页按钮统一改用共享 TDesign Button 适配器,并保留危险描边与原生表单提交语义。
- [优化] 共享 UI 包新增 Alert、Loading 适配器并迁移管理台告警与加载态;同时修复 Vite 开发模式下 TDesign 动画依赖的 CommonJS 解析,避免页面白屏。
- [修复] 管理台页面设置的主题与布局选项现在整项可点击并支持 radio 键盘操作,跟随系统主题在浅色偏好下也能正确生效。
- [优化] 渠道分组保存后同步刷新模型产品与渠道候选关系,分辨率配置列表改为紧凑矩阵,手机端横向表格更易扫描。
- [优化] 管理台渠道分组改为按“任务模式 → 上层模型产品 → 下层渠道池”配置,列表拆分模型 ID、产品名称、基础价格和分辨率矩阵,编辑时可直接勾选产品与渠道并查看归属、健康状态和层级关系。
- [修复] 渠道分组与模型产品绑定增加能力和归属一致性校验,模式切换不再静默移除产品,跨分组产品也不会被无提示迁移。
- [修复] 管理台模型、套餐、金币单位、存储策略和消息供应商表单现在在提交前校验数值边界与接口地址,非法输入会保留草稿并显示中文提示。
- [调整] 管理台登录页手机端与桌面共用透明左侧内容轨道,标题、表单和页脚按同一左边距构图,不再增加移动端内容容器。
- [修复] 管理台用户冻结、解冻和安全编辑现在使用用户资源版本校验并在事务内递增版本,旧版本提交返回 409,避免并发操作静默覆盖。
- [修复] 用户端 Tabs 的方向键、Home 和 End 切换现在会把真实焦点同步到新的选项,受控值未更新时也不会残留待聚焦目标。
- [修复] 画布图片、媒体和项目导入文件选择器会在读取文件后立即清空值,重复选择同一文件可以再次触发导入。
- [优化] 用户端插件式确认弹窗补齐对话框语义、打开时焦点进入、Tab 循环和关闭后焦点回收。
- [优化] 画布图片、音频和文本设置浮层统一补齐触发状态、对话框语义、焦点进入、Tab 循环及关闭回焦。
- [修复] 画布设置浮层不会再把 TDesign 下拉选项误判为外部点击,选择模型参数后浮层保持可用。
- [优化] 画布节点创建与连线创建菜单补齐方向键、Home、End 的循环焦点导航,并默认聚焦第一个可执行菜单项。
- [修复] 用户端复合字段改用带可访问名称的容器,不再用 `<label>` 包裹提示词引用菜单中的交互按钮。
- [修复] 管理台保存认证策略后会同步刷新当前管理员状态,MFA 启停判断不再依赖旧会话配置。
- [修复] 管理台登录路由现在直接挂载全屏登录主体,桌面与移动端都沿用左侧内容轨道,不再继承后台 Layout/Content 外层容器。
- [修复] 用户端受保护路由在认证初始化和会话失效期间不再短暂渲染旧导航壳层,统一显示完整中文加载/跳转状态。
- [修复] PostgreSQL 快照重载会等待排队中的持久化写入,并在可重复读事务中读取多表状态,避免旧快照覆盖新写入或拼接出混合版本。
- [修复] 管理台任务调用记录请求失败时保留详情抽屉,显示中文错误并提供可重试入口,不再只闪现提示后卸载。
- [优化] 用户端画布资源遮罩、侧栏悬停和设置入口改用当前画布主题令牌,快捷键与实时任务连接错误统一中文显示,并将复制节点的英文副本后缀归一化为中文。
- [修复] 用户端任务详情不再回退显示内部模型产品 ID,账户撤销登录设备增加确认弹窗;首页“我的资产”标签改为展示真实本地图片资产,空态入口与实际功能一致。
- [优化] 用户端首页轮播在手机端始终显示并垂直居中切换按钮,作品卡片补齐键盘焦点反馈,弹层遮罩改用浅深色主题令牌。
- [优化] 用户端登录、账户、任务、创作和预览页面统一移除负字距,独立首页示例改用真实创作素材替代渐变球体装饰。
- [修复] 首页作品灵感列表移除原型演示作品、虚构作者和统计数据,只展示当前用户真实资产并在空库时显示中文空态;审计日志分类改为准确区分 HTTP 小于 400 与异常/业务错误,消息供应商和模板配置写入补齐持久化。
- [修复] 画布创建菜单移到未缩放的覆盖层,平移或缩放画布时不再重复变换位置和尺寸,边界钳制按实际画布视口计算。
- [修复] 管理台页面设置抽屉关闭改为幂等状态更新,主题色选择器改用受控值,重复关闭或切换主题后不再回弹或残留旧色。
- [优化] 画布普通提示词和节点资源提及菜单补齐列表框、活动项和中英文无障碍标签,保持键盘选择与焦点关联一致。
- [新增] 管理台登录页接入本地优化的科技背景图,桌面沿用左侧表单构图,移动端去除多余容器并修正表单宽度。
- [修复] 管理台登录移动端内容层改为纯定位布局,继续沿用桌面左侧主体并保持窄屏左右留白对称,不再命中旧卡片容器样式。
- [修复] 管理台详情请求失败时保留详情抽屉并提供中文重试入口,页面设置与任务调用抽屉显式支持右上角、遮罩和 ESC 关闭。
- [优化] 画布音频/文字设置面板按视口宽度收缩,创建菜单与右键菜单补齐菜单语义、ESC 关闭和窄屏宽度约束。
- [优化] 管理台登录页移动端沿用桌面左侧内容轨道,移除顶部模糊带,短屏允许纵向滚动并让背景固定展示左侧构图。
- [调整] 用户端账户菜单改用 TDesign Dropdown 承载,保留资料视觉、登录态入口和游客态提示,并统一菜单项宽度、ESC 与外部点击关闭行为。
- [修复] 用户端受保护路由认证初始化时显示明确的中文加载状态,避免壳层短暂只剩导航而被误认为空白页。
- [修复] 渠道分组顺序改用 TDesign 支持的整行拖拽,调整优先级后会正确写入分组草稿。
- [修复] 管理台详情与任务调用抽屉增加加载态及关闭时的请求取消/代次保护,避免慢请求在关闭后重新打开抽屉。
- [修复] 用户端路由加载期间显示统一中文加载态,渲染异常提供重新加载与返回首页入口;设备标识在存储受限环境下改用内存兜底,避免认证页白屏。
- [修复] 开发预览路径显式绕过正式用户壳层,桌面和手机预览菜单统一左起布局;移动端账户、同步和任务详情列表取消强制宽度并支持内容换行,避免小屏横向溢出。
- [优化] 图片创作的高级设置改用主题化受控折叠栏,移除浏览器原生 details 控件,移动端展开与收起保持统一交互。
- [修复] 首页作品筛选现在会随本地资产列表变化重新计算,新增或删除资产后不会继续显示旧列表。
- [修复] 用户端资产同步失败时保留编辑弹窗和本地草稿,重试会复用同一条待同步资产;任务等待增加十分钟上限,超时会尝试取消并提示到任务历史查看最终状态。
- [优化] 详细日志收紧桌面列宽,状态、分类、行为、操作人和对象可在首屏完整查看,窄屏仍支持横向浏览。
- [修复] 用户端头像菜单与侧栏底部入口改为左对齐,避免继承通用按钮的居中布局。
- [修复] 用户端菜单与标题保持左对齐,组件中心隔离为开发预览;手机端画布工具栏允许换行、业务列表保持紧凑横滑,认证页适配动态视口,并补齐中文目录错误、任务错误码、文件格式和图片用量展示。
- [修复] 用户端资产页标题统一左对齐,画布与资产页移除内层滚动容器;滚动条和独立组件中心业务卡改用主题变量,避免浅色模式出现近黑色块或双滚动。
- [修复] 开发者组件中心独立挂载用户端主题变量并补齐根级路由加载占位,开发预览不再带正式侧栏,路由启动不再输出 HydrateFallback 警告。
- [修复] 共享用户端 Dialog/Drawer 增加显式 ESC 关闭,移动导航增加遮罩和 ESC 关闭,避免弹层或抽屉打开后无法退出并误操作底层页面。
- [优化] 用户端开关、图标按钮和错误提示改用主题语义色,浅色模式不再出现深灰轨道或白色悬停图标。
- [修复] 用户端侧栏导航强制使用左对齐布局;组件中心仅保留为开发环境独立预览,不再嵌入正式用户壳层或导航。
- [修复] 管理台列表、详情、调用记录、渠道探活和生图测试增加视图代次与主动取消保护,旧页面的异步响应不会覆盖新路由、关闭后的弹窗或当前选中的记录。
- [优化] 用户端画布资源提及输入、节点/侧栏操作、图片创作参考图、资产入口、参数选项和主题切换按钮,以及管理台移动菜单遮罩统一使用 TDesign 适配层,保留选区、拖拽、键盘和主题行为;可见业务源码不再直接渲染原生按钮或文本域。
- [修复] 用户端作品卡片移除无实际播放行为的伪按钮,浅色/深色主题改用媒体语义色,并让移动端收藏、分类和“一键同款”入口无需悬停即可使用;管理台渠道生成测试失败时保留请求路径、状态和中文错误,用户安全编辑后同步刷新已打开的详情。
- [修复] 用户端任务历史列表增加请求代次保护,快速切换状态或定时刷新时过期响应不会覆盖当前列表、加载状态或错误提示。
- [修复] 管理台用户序号与充值目标统一使用同一批次映射,充值改为主题化用户选择器;渠道优先级可编辑、支付适配器启用字段改为开关,布尔筛选统一显示中文,审计日志按时间可靠倒序。
- [修复] 共享 TDesign 开关补齐 `aria-checked` 状态,用户端与管理台弹窗增加遮罩点击兜底,确保点击遮罩、右上角、取消和 ESC 都能结束弹层。
- [调整] 将本轮长期目标收敛为唯一的“前后台真实可用与统一 TDesign”执行入口,明确用户端自定义视觉、管理端独立边界、OpenAI 三 Tab 渠道流程、阶段门槛和证据要求,并消除旧版分步渠道描述冲突。
- [修复] 共享 TDesign 文本输入适配器会过滤负数、非有限和小数长度属性,避免浏览器写入非法 `minLength`/`maxLength` 时触发异常并造成白屏。
- [调整] 本地一体化开发启动默认关闭后台演示数据,只有显式设置 `MIRAGENFLOW_DEMO_DATA=true` 才注入展示记录,避免真实渠道和业务测试被 mock 数据干扰。
- [修复] 用户端用户菜单改为受控弹层,支持点击展开、外部点击和 ESC 关闭,并在路由切换后自动收起;画布库、节点工具栏、快捷键、项目卡片和节点选中态改用画布主题令牌,避免浅深色硬编码不一致。
- [优化] 用户端顶部导航、移动导航、资产选择、生成中状态和 404 页面改用统一主题语义色,减少 Stone/黑白固定颜色在浅深主题之间的偏差。
- [调整] 将本轮前后台统一 TDesign、中文化、真实后台交互、OpenAI 渠道、ECharts 与响应式回归整理为可持续长任务目标,并补充分阶段验收索引;未完成人工验证的页面仍保持待测试状态。
- [调整] 长任务验收新增管理台列表请求代次保护要求,防止快速切换页面时旧响应串入当前业务列表。
- [修复] 管理台列表读取加入请求代次和取消保护,快速切换页面时过期响应不会串入当前业务列表或覆盖加载状态。
- [修复] 系统概览对缺失核心字段的响应改为中文空态,延迟无样本时不再绘制伪零值圆环,避免把不完整数据误显示为真实统计。
- [优化] 前端埋点改为本地空实现,不再注入第三方统计脚本、运行期配置脚本或外部页面访问上报,降低外联请求和首屏资源负担。
- [修复] 审计日志操作人映射统一为普通用户显示用户名、管理员直接显示邮箱,并兼容历史 `admin:` 身份标识。
- [调整] 管理台菜单隐藏尚未接入的消息供应商和消息模板页面,保留认证消息状态与后端适配边界,不把未完成配置暴露给管理员。
- [调整] 用户端与管理端接入同一锁定版本的共享 TDesign Provider 与主题变量桥接,用户端画布项目库开始使用共享 TDesign 按钮适配器,业务路由和管理台视觉保持独立。
- [优化] 用户端工作台、任务筛选和画布图片设置统一改用本地 Lineicons 与自定义下拉弹层,移除浏览器原生下拉框,并保留键盘、关闭和移动端交互。
- [优化] 用户端图片、音频、认证和账户页面的简单文本域统一改用共享 TDesign 文本域适配器,并暴露底层文本框引用,保留图片提示词的光标定位能力。
- [优化] 画布图片尺寸/数量和音频语速输入统一改用共享 TDesign 数字输入组件,并保留步长对齐、范围限制、空值和主题样式。
- [优化] 画布项目选择复选框改用共享 TDesign Checkbox 子路径并沿用画布主题;文件选择器和提及编辑器继续保留原生底座以支持浏览器文件 API、选区、滚动和输入法行为。
- [优化] 画布节点标题编辑框改用共享 TDesign TextInput,并通过适配层暴露真实输入节点,保留自动聚焦、全选、回车/ESC 和外部点击提交行为。
- [优化] 用户端导航、账户菜单、首页轮播控制、快捷入口和作品收藏按钮统一改用共享 TDesign 按钮适配层,并保留原有主题样式、语义图标和键盘焦点行为。
- [优化] 画布资产选择卡、项目标题入口、右键菜单和节点创建菜单统一改用共享 TDesign 按钮适配层,保留画布主题、坐标交互和关闭行为。
- [修复] 用户端与管理台登录、首页和画布的左上角品牌标题、主标题及副标题统一左对齐,避免文字标识居中造成视觉错位。
- [优化] 渠道健康单行测试改为独立结果弹窗,显示加载状态、实际请求路径、请求是否发起、模型数量和中文失败原因,并将最近一次测试结果持久显示在列表中;修复 ECharts 标题文字样式弃用警告。
- [修复] 总览 P50/P95 延迟改为双环图并正确显示无样本状态;渠道生图测试无论成功或失败都会返回真实请求状态、路径和测试时间,编辑渠道不会误清除独立的模型配置。
- [优化] 渠道未执行过测试时显示“未测试”,不再误显示“降级”;总览图表颜色跟随当前主题正确渲染。
- [新增] 余额账本、充值订单和支付事件页面增加基于真实列表数据的统计卡与 ECharts 图表,并补齐财务日期范围筛选。
- [优化] 批量渠道健康检查保留实际请求路径、是否发起和测试时间,健康检查与生图测试使用不同中文动作名称。
- [优化] 管理台请求增加超时、主动取消和网络错误中文提示,供应商异常时不会无限显示加载状态。
- [修复] 管理台日期筛选兼容日期组件返回的 Date、时间戳和日期字符串,避免选择日期后筛选失效。
- [修复] 画布项目页移除冲突的 `vh/svh` 最小高度约束,统一使用动态视口并隔离目录同步副作用;历史项目的异常视口值会自动归一化,避免热更新残留或无效缩放导致白屏和下半屏裁切。
- [优化] 窄屏画布侧栏遮罩不再压住底部工具栏,抽屉打开时仍可看见并使用画布控制项。
- [修复] 画布项目页在加载完成后重新连接尺寸观察器并补齐根节点视口高度,避免下半屏塌陷;窄屏侧栏、小地图、工具栏和缩放控件分层布局,顶栏折叠入口保持可用,画布操作更新增加无变化短路以避免渲染循环。
- [修复] 画布区域补齐父子层级的显式全高和尺寸帧合并,客户端初始化请求只执行一次,降低动态视口下半屏空白和会话刷新重复引起的渲染抖动。
- [修复] 用户端壳层改用动态视口与可收缩内容区,图片工具预览同步增加无变化短路,避免顶部留白叠加或尺寸抖动再次裁切画布并触发更新循环。
- [修复] 画布切换项目时会取消旧的资源恢复与生成请求并阻止旧状态回写新项目,持久化与视口保存只针对当前已加载项目,避免异步更新造成内容串线或重复渲染。
- [修复] 资产平台同步改为只在合并结果实际变化时写入,并避免提示上下文变化触发重复同步,降低资产页的更新深度风险。
- [修复] 管理台设置页的权限提示改为准确显示安全管理范围,存储策略更新增加版本校验,空数字输入不再误显示为 0。
- [修复] 模型产品下架时清空渠道组会显式解除旧绑定,避免停用后仍残留不可见的渠道配置。
- [优化] 账户中心的登录设备列表改为显示“当前浏览器 / 设备 N”等可读名称,保留服务端真实标识用于撤销会话。
- [优化] 管理台侧栏折叠后完全隐藏,窄屏菜单改为遮罩层覆盖;手机端业务列表继续使用紧凑表格并支持横向滑动,同时缩小页面与卡片左右间距。
- [修复] AI 创作与我的资产页面移除硬编码黑色业务容器并统一使用主题语义色;游客侧栏不再显示个人主页和退出登录。
- [优化] 用户端账户、任务和同步设置共用面板、统计卡与弹窗改用主题表面,浅色模式不再出现深色容器。
- [修复] 画布图片裁剪/切分预览改用当前主题背景,节点信息移除原始 JSON 视图并改为字段化展示;首页、样式预览和组件验收页去除非必要英文,演示按钮均提供明确交互。
- [优化] 前后台通用 UI 图标统一迁移为本地化 Lineicons 精选子集,校正撤销、停止、侧栏、分组和收藏等图标语义,放大图形与点击区域并移除 Lucide 及直接业务图标依赖,避免加载远程字体或全量图标目录。
- [修复] 用户金币调账表单补齐输入标签关联;本地自动审批失败时保留原申请并进入审批中心继续处理,避免重复创建调账申请。
- [优化] 用户列表、金币调整页和用户详情把增加/扣减入口直接放在余额旁;本地超级管理员确认后自动完成审批并刷新余额,正式环境继续保留双人审批。
- [修复] 金币调账、人工充值、退款与审批状态在同一事务内持久化,补齐金币批次、MFA 防重放、批准/驳回并发和幂等结果;本地 Docker 仅绑定回环地址并允许超级管理员自审批。
- [修复] 财务管理员新增独立的用户金币调整入口,并使用仅包含脱敏身份与余额的专用列表,避免调账能力被安全管理权限阻断。
- [新增] 管理台用户列表增加独立的增加金币和扣减金币操作,弹窗展示当前余额与审批后预计余额;本地超级管理员可执行自己的调账申请,生产环境继续要求双人审批。
- [修复] 管理台新增与编辑弹窗移除无名称 `FormItem` 对受控字段的错误接管,已保存的渠道基础信息、模型列表和模型映射现在会完整回填。
- [修复] 修复出站 DNS 固定地址回调在 `all: true` 模式下返回格式错误导致真实请求抛出 `ERR_INVALID_IP_ADDRESS` 的问题;现在可正常访问 OpenAI 兼容的 `/v1/models`。
- [调整] 渠道新增和编辑不再隐式请求上游模型列表,保存只校验基础信息与渠道模型;模型列表仅在用户点击“拉取模型”时请求。
- [调整] 渠道模型列表与模型映射完全拆分:拉取结果通过复选框批量添加为渠道模型,映射改为可选别名配置;开发环境不再自动生成展示用 mock 渠道。
- [修复] 模型映射输入行改用稳定的 React 行标识,连续输入不会因组件重建而失去焦点;请求地址示例统一使用 `example.com`。
- [修复] 渠道探活只在真正调用上游 `fetch` 前标记请求已发起;通用供应商和地址校验失败不再虚报健康,关闭或重开弹窗也不会被旧探活结果覆盖。
- [修复] 渠道模型探活增加独立加载状态、实际请求路径和中文失败原因,明确区分未发起请求、地址解析失败与上游接口失败。
- [修复] 渠道请求地址字段补充完整 URL 示例,API Key 只要求非空并取消 16 位最小长度限制,探测、新增、编辑和轮换凭证保持一致。
- [修复] 渠道弹窗 Tab 改为自由切换,基础信息、模型探测结果和模型映射由统一草稿状态保存,切换后不再清空,底部始终使用“保存/取消”。
- [调整] 管理台渠道新增与编辑改为三个 Tab 的统一弹窗:基础信息、拉取模型、显示模型与请求模型映射;非生产环境默认可联调本机/私网地址,显式设为 `false` 可关闭,生产默认继续拦截私网出站。
- [调整] 管理台新增用户弹窗收敛为用户名、邮箱、手机号和初始密码四项,套餐、可用模型、欢迎消息、状态及额度改为创建后的独立配置。
- [测试] 本轮完成服务端全量回归 84 项、用户端与管理台类型检查、管理台 lint 及前后台生产构建;构建仅保留分包体积提示。
- [修复] OpenAI 图片比例矩阵补齐 1.91:1、2.35:1 和 21:9 的 1K 尺寸校验,管理台新建模型的 4K 默认预设改为符合平台上限的 3840×2160。
- [测试] OpenAI 图片端点新增 69 项服务端回归覆盖,包含比例换算、参考对象过期/撤销、异常图片 URL、模型能力声明、幂等请求头和未知任务人工对账元数据。
- [修复] OpenAI 图片、文本和音频请求统一使用标准 `Idempotency-Key`,未知结果会在用户端显示为“待对账”并保留任务历史入口,避免重复提交或误报普通失败。
- [修复] OpenAI 图片供应商适配层补齐参考图序号、缺失引用、50MB 大小和默认参考图提示的兜底校验;仅上传参考图时也允许提交任务。
- [修复] 管理台安全设置入口、审批自审判断和审计分类/操作人中文兜底修正,退出登录不再误跳转到旧路由。
- [优化] 管理台登录、加载和写入失败提示统一中文兜底,认证请求不再对登录接口误触发会话刷新,登录背景移除模板英文装饰。
- [修复] 管理台图表空数据、深色主题文字和模型名称显示;首页统计卡统一传入数值,避免指标被误显示为 0。
- [新增] 管理台系统概览、渠道健康、任务中心、实时指标和日志大板统一接入 ECharts 图表;审计拆分为日志大板与详细日志两页,并增加正常/异常状态统计。
- [优化] 管理台左侧菜单改用语义化差异图标,单项分组直接显示页面入口,减少重复展开层级。
- [优化] 审计日志改为按业务分类展示中文行为和可读操作人名称,管理员仅显示邮箱、普通用户显示用户名,并移除没有实际按钮的操作列。
- [优化] 审计日志默认按最新时间倒序展示,分类、对象和来源字段统一使用中文;业务标识保留原值。
- [调整] 用户列表的 ID 表头仅显示当前列表内的纯数字序号;模型、套餐、渠道等其他标识保留真实值,避免把业务 ID 误显示成编号。
- [优化] 管理台列表补充可用模型名称、余额图标和中文状态兜底;未知状态与权限统一使用中文兜底。
- [修复] 管理台新增资源不再提交空 ID,套餐、用户和模型表单增加服务端约束对应的中文校验,财务配置入口与权限一致,供应商和模板编辑增加版本保护。
- [新增] 渠道管理支持 OpenAI 图片端点供应商方式、模型探测和 1K/2K/4K 分辨率模型映射,图片任务支持文生图、参考图生图、透明背景、格式选择和结构化结果信息。
- [修复] OpenAI 图片请求固定使用渠道配置的模型映射,避免任务参数覆盖分辨率对应的上游模型;开发脚本启动时预留端口,确保前台、管理台和提示地址一致。
- [修复] OpenAI 图片参考图上传改用 multipart,中文长序号引用按完整名称解析,画布比例不再写入非法尺寸,参考图保真度默认改为高。
- [修复] OpenAI 图片端点现在按 `image[]` 顺序提交参考图,并校验 Base64/URL 返回的真实图片格式;未知任务结算失败会保留可对账 attempt,避免预留金币与任务状态脱节。
- [优化] 管理台渠道编辑支持复用已保存凭证探测 `/v1/models`,新增模型产品分辨率矩阵,人工成功对账改用真实图片 multipart 上传并自动关闭成功弹窗。
- [修复] 管理台全页面统一使用中文字段和结构化数据展示,余额不再输出原始 JSON,内部资源编号按列表顺序显示为数字,并过滤嵌套凭证、哈希和幂等标识。
- [优化] 修复管理台限流窗口百分比误显示、设置面板英文文案和审计动作/状态未翻译问题,补齐所有业务字段的中文名称。
- [修复] 管理台系统概览统计卡在指标缺失时不再向 TDesign Statistic 传入 undefined,避免登录后 Dashboard Base 渲染白屏。
- [优化] 管理台分页隐藏无意义的总条数文案,新增和编辑弹窗显式支持遮罩、ESC、取消及右上角关闭,渠道密钥字段统一使用中文。
- [优化] 桌面端登录/注册表单区域改为对称留白并整体上移,品牌中英文标题、表单和底部安全提示在视口中更接近水平垂直居中,减少顶部空白。
- [调整] 用户菜单下拉在未登录状态也显示资料占位容器,展示访客提示和“登录工作台”入口;登录后切换为用户信息与账户操作。
- [优化] 用户菜单下拉改为精致的资料占位容器,保留未来图片插槽、用户身份信息和账户/任务/退出操作;移除直接铺满的头像大图,适配雾紫玻璃风格与窄屏宽度。
- [优化] 通知悬浮窗改为响应式短面板,限制最大高度并适配视口剩余空间;无通知和有通知时均不再占据过高页面区域,原有圆角保持不变。
- [优化] 用户端侧边导航统一加宽至 224px,并轻微压缩导航项与底部链接高度;桌面端内容区和移动端抽屉同步使用同一宽度。
- [调整] 密码登录改为单一“账号”输入框,自动兼容邮箱、手机号和用户名;邮箱注册/手机号注册与手机号验证码登录仍保留各自的方式切换。
- [修复] 管理台路由从绝对站点地址构建时将 Vite base URL 规范化为路径 basename,修复 `/admin/dashboard/base` 在 site 构建下路由不匹配导致的白屏。
- [新增] 用户认证支持密码登录(邮箱/手机号/用户名)与手机号验证码登录;注册支持邮箱/手机号分流并在后台独立控制两种注册验证开关。
- [新增] 迁移 `MiragenFlow-front` 用户控制台视觉基础:新增深色工作台壳层、响应式侧栏、Hero/作品灵感组件和作用域 token;首页作品流改为读取真实资产库,模型广场入口暂不进入用户侧导航。
- [调整] 用户端移除作品搜索框与模型广场入口,登录/注册/验证/重置密码及图片/音频 AI 创作页统一改用组件中心的表单、反馈、状态和结果卡视觉,真实认证与任务 API 保持不变。
- [调整] 登录注册页收敛品牌展示,仅保留紧凑标识,避免宣传文案压过认证表单。
- [调整] 登录奖励入口文案统一改为“登录免费送积分”,恢复原有动效与奖励卡样式,仅替换奖励文字,避免继续使用“算力”表述。
- [调整] 登录注册页紧凑品牌标识恢复为“MIRAGEN FLOW / 元境幻生”,保留原品牌识别同时不恢复大段宣传语。
- [调整] 恢复登录页“MIRAGEN FLOW / 元境幻生”原有的蓝色小字字体与字距样式,避免紧凑布局改变品牌视觉。
- [调整] 登录与注册页移除 MF 图标及顶部品牌容器,将品牌文字放到“欢迎回来/开始你的创作”标题上方并适度放大。
- [优化] 登录奖励 SVG 将“免费”单独使用高亮渐变并拉开字距,扫光动画下文字占位更完整、重点更清晰。
- [优化] 登录奖励按钮重新分配三段文字的宽度与左右间距,并放大“登录 / 免费 / 送积分”,确保整块光效区域被完整填充。
- [优化] 登录奖励“免费”前后采用对称间距和固定文字宽度,保证高亮词在扫光动效中保持居中、填充协调。
- [调整] 登录奖励入口移除大尺寸悬浮奖励卡与透明点击覆盖层,按钮改为透明底白色扫光,移动端按比例缩小,避免黑色底和遮挡内容。
- [调整] 用户头像访客浮图和“加入设计交流群”容器暂时移除,移动端隐藏左侧品牌 logo,仅保留折叠菜单按钮,避免窄屏头部拥挤。
- [调整] 登录奖励按钮取消 GIF 扫光,仅保留 CSS 渐变边框与透明背景,并增加文字阴影提升浅色主题下的可读性。
- [修复] 登录奖励渐变改用遮罩边框实现,确保渐变只出现在边框区域,按钮中心保持完全透明。
- [优化] 登录奖励文字改为与“作品灵感”标签一致的品牌字体、字号和字重,仅保留“免费”高亮,避免粗体导致文字拥挤。
- [调整] 自由画布与一键成稿入口改为使用图片铺满卡片,保留文字叠加和原有跳转,取消原来的纯 CSS 渐变卡片底。
- [修复] 移动端轮播下方的两个工具入口恢复与上方横幅相同的宽高比例,避免四个容器宽度一致但高度不齐。
- [调整] 首页移动端广告轮播和活动图片改用与桌面端一致的原始宽高比例,取消固定高度裁切,保证手机端完整显示素材。
- [新增] 增加 `/style-preview` 用户端配色候选预览页,提供暮光蓝灰、暖纸钴蓝、雾紫玻璃和松石石墨四套方案,暂不写入正式主题。
- [调整] 用户端正式采用“雾紫玻璃”配色:以淡紫背景、白色玻璃卡片和紫色/珊瑚渐变为主,统一首页、登录注册、创作、画布和资产界面的明暗主题。
- [优化] 管理台列表增加按业务维度、状态和日期范围筛选;系统概览增加近 7 日任务趋势与运行告警;数据请求权限不足时进入带角色、所需 scope 和 requestId 的可恢复 403 页面,错误页区分重新登录与切换管理员。
- [修复] PostgreSQL 任务 dispatch worker 在跨实例本地缺少任务投影时按 task_id 回载持久任务后再入队,避免 outbox 长期停留在待重试状态。
- [修复] PostgreSQL 快照写入现在携带捕获时的不可变 revision,避免 direct lease/outbox 更新后旧的异步快照读取新 revision 并回退租约状态。
- [修复] 消息 outbox 支持业务幂等键、Provider `Idempotency-Key` 和 PostgreSQL 发送期租约续期,降低重启窗口的重复发送风险。
- [新增] WebDAV PG worker 增加远端请求期间的 job 租约续期,避免长时间 PUT/DELETE 被其他实例重复认领。
- [修复] PostgreSQL 消息 outbox 在供应商请求期间增加带 owner 的租约续期,避免长请求被第二个 worker 抢占。
- [修复] 任务 worker 心跳现在同时续租 Redis/内存队列与 PostgreSQL generation_tasks 行,过期任务恢复按事务重载并进行租约校验;内存队列拒绝过期 ack/nack,WebDAV 保留期扫描改用 PostgreSQL 行锁入队并按终态重试。
- [修复] PostgreSQL dispatch、消息 outbox、WebDAV 完成接口增加 lease_expires_at fencing;manifest 写入按版本递增,旧 worker 不能覆盖新 manifest 或继续确认过期租约。
- [优化] 管理台供应商消息页面不再注册可直达路由,统一进入 404;列表增加客户端分页,用户调账按钮仅对 finance scope 展示,渠道创建正确提交数值优先级。
- [修复] PostgreSQL WebDAV worker 完成任务时在同一租约事务持久化文件、manifest、保留状态、归档状态和冲突副本,重启后不再丢失远端同步结果;任务 dispatch 支持并发槽并避免缺失本地投影时误确认。
- [优化] 管理台模型产品新增“创建后上架”开关、批量探活按渠道汇总成功/失败、只读管理员可查看任务详情和调用记录;错误页重新登录/切换管理员会清理当前会话。
- [调整] 管理员两步验证设置/验证接口仅在系统增强开关开启后可用;关闭开关会同步清理账户恢复状态,预览登录继续只显示邮箱和密码。
- [新增] 管理台渠道、渠道分组和模型产品增加编辑抽屉,渠道健康支持全部探活,审批列表增加状态标签;列表首列固定、认证消息目标哈希不再返回,管理请求错误保留 409/422/503 上下文并允许重试。
- [修复] 认证增强开关现在严格控制登录挑战:管理员 CAPTCHA 使用独立开关,关闭普通用户 MFA 后会清理遗留挑战;刷新会话、审批 MFA、审计保留和普通用户界面均与开关状态一致。
- [修复] 修复队列并发 claim 串任务、Redis 活跃租约被重复入队、PostgreSQL 验证码快照加载崩溃和消息第五次重试卡死;管理员审计与用户详情继续保持脱敏。
- [新增] 管理台任务中心增加脱敏任务详情抽屉(输入、输出、attempt、事件时间线),审批中心增加带原因和版本校验的驳回流程。
- [新增] 管理台系统设置独立于存储策略,支持版本化 If-Match、认证增强开关、限流、队列和审计保留配置;用户详情新增账户、会话、任务、账本、权益和审计分 Tab。
- [修复] 管理员登录接口同时接受管理台表单使用的 `account` 和 API 契约的 `email` 字段,修复前端填写邮箱密码却返回“账号或密码不正确”的问题。
- [修复] 管理台新增/配置弹窗统一接入套餐、金币单位、换算、支付适配器、存储、消息模板和管理员安全操作,避免确认后静默无动作;用户创建支持套餐、并发、每日上限、可见模型和欢迎消息,渠道健康增加单项探活。
- [新增] 管理台用户详情支持一次性重置密码、重发认证消息、会话与套餐权益查看;系统概览补充 P50/P95、渠道/模型、消息和支付异常指标。
- [优化] 管理台失败写操作保留当前弹窗内容,409/503 等错误可直接重试;认证消息与套餐描述改用多行输入,能力和延迟字段统一摘要显示。
- [调整] 充值订单新增失败标记和退款申请交互;高风险审批仅在管理员 MFA 增强开关开启时要求动态码,默认邮箱密码登录不再被额外字段阻断。
- [新增] 管理台开始落地七组二级菜单、可恢复权限页、用户/渠道/分组/模型/价格新增弹窗及认证消息详情重试。
- [修复] 本地预览会以配置覆盖旧管理员快照,确保 `admin@admin.com` / `admin` 登录不会被旧密码、角色或 MFA 状态阻断;旧会话缺少 scope 时 `/admin/me` 会按角色补齐权限。
- [新增] 管理员可从管理台新增普通用户,初始密码只在创建成功响应中显示一次并写入审计。
- [新增] 新增 `goal.md`,明确管理台重构的页面、布局、业务闭环、实施阶段、验收标准和目标引导提示词。
- [规划] 新增管理台一级/二级菜单、页面 SSII 线框、权限状态、渠道到模型产品交互和逐页验收基线,详见 `admin-plan.md`。
- [调整] 本地预览默认管理员账号改为 `admin@admin.com`,密码改为 `admin`,管理员 MFA 与人机验证码仍默认关闭并可由环境变量重新开启。
- [调整] 普通用户默认关闭邮箱/短信验证、人机验证码和 MFA,注册登录仅需账号密码;相关能力保留为后台可配置增强开关。
- [修复] 本地一体化预览默认使用受保护的文件快照保存账户状态,避免服务端热重载后已创建用户被清空并误报“账号或密码不正确”。
- [新增] 任务结算、释放、未知对账和支付订单/webhook 统一经过 Store 事务边界;新增支付请求指纹迁移,PostgreSQL 重载后继续执行幂等校验。
- [调整] 管理员登录默认仅需邮箱和密码;管理员 MFA 与人机验证码改为默认关闭的后台增强开关,启用后才进入额外验证流程。
- [新增] 账户中心增加已登录修改密码、其他会话撤销和两步验证配置入口;账户注销请求进入异步匿名化清理流程。
- [测试] 增加账户密码修改、旧会话撤销和注销匿名化回归测试,服务端回归测试达到 49 项。
- [修复] 管理台创建已上架模型时校验渠道组绑定并修复请求体重复读取,前台登录、注册和密码重置保留安全 redirect。
- [修复] WebDAV 保留期延长会取消尚未执行的过期删除任务,worker 派发前再次校验最新保留时间。
- [修复] PostgreSQL dispatch outbox 在 worker 事务重载 Store 后重新读取行,避免使用旧对象把已发送任务恢复为 leased;重载 smoke 已确认最终状态为 sent。
- [修复] PostgreSQL 快照 revision 在空库首次写入时启用 fencing,持久化失败后队列可恢复,并修正 bucket 关系表投影;新增公网出站地址解析、DNS 固定、禁止重定向和 provider 基础路径保持。
- [修复] WebDAV 条件写入、冲突副本和归档/删除任务携带明确 intent,服务端与前端可区分同步排队状态;Docker 文档移除旧的单容器无限画布镜像说明。
- [测试] 新增出站 URL 私网拒绝、固定公网地址、WebDAV 到期清理、Store 事务回滚和部分结算 release 测试,当前服务端回归测试达到 50 项。
- [新增] 增加可视化套餐完整配置字段、organization/team nullable scope 预留字段,并在 WebSocket 心跳和消息路径实时校验会话族撤销。
- [修复] 任务首次提交等待持久化快照完成后再投递队列,幂等重放不重复 enqueue;worker 在确认终态持久化后才 ack Redis,并补齐 PostgreSQL 余额版本、任务 lease、attempt 对账和输出计费字段投影。
- [修复] 上传直传、presign 和 complete 接口统一要求 Idempotency-Key,重复请求返回原上传对象;上传幂等键写入 PostgreSQL 唯一索引。
- [新增] PostgreSQL 增加任务 snapshot 与 dispatch outbox 迁移,队列投递状态和任务完整输入快照可在重启后恢复。
- [新增] 生产 PostgreSQL 任务创建路径使用余额账户/ bucket 行锁、原子 reserve ledger、任务事件和 dispatch outbox 事务,公开幂等键仍由任务 snapshot 校验。
- [修复] 验证码挑战改为服务端哈希、IP/设备/目标绑定、失败计数和一次性消费;用户 MFA、管理员 MFA 与人机验证码均由后台增强开关控制,预览默认关闭。
- [调整] 首页删除演示作品、视频/动画/3D宣传入口和假对话框,改为实时公开模型目录与图片、音频、画布、任务工作流入口。
- [新增] 资产库补齐音频文件创建、编辑、播放、导入导出和本地媒体刷新回载;WebDAV 离线写入显示队列状态并支持冲突副本、归档和删除任务。
- [调整] 管理员退款改为二次审批并要求审批 MFA,审计 before/after 递归脱敏,新增管理员 MFA 管理页面和统一权限校验。
- [测试] 新增 10 项 V1 回归测试,覆盖 MFA recovery 接口、会话撤销、套餐权益、审批调账、资产归属去重、WebDAV 保留期、过期 bucket、渠道路由快照、管理契约别名和充值订单状态转换。
- [修复] access token 绑定会话族,撤销设备会话后旧 token 与 WebSocket 订阅立即失效。
- [新增] 补齐管理端金币单位、换算规则、价格规则、存储策略、用户和渠道组的契约写方法及充值订单详情查询。
- [修复] WebDAV 文件写入先缓存并复用单次请求体,确保合法写入路径和内容按预期进入同步记录。
- [修复] 过期 bucket 即使可用余额已被其他流水耗尽也会写入实际扣减额,并保持余额与账本一致。
- [修复] 统一导航文案键并补齐设备绑定验证码流程,注册验证、密码重置和 MFA 登录可在同一浏览器设备完成。
- [新增] 补齐用户套餐查询与登录设备会话撤销 API,返回稳定会话标识并保持会话族撤销审计。
- [修复] 资产保存校验任务及输出归属,重复保存同一任务输出返回原资产而不创建副本。
- [修复] 渠道分组顺序写入任务路由快照,套餐权益按实际 plan bucket 余额校验,过期 bucket 追加幂等 adjustment 流水。
- [新增] WebDAV manifest 支持策略上限内的用户保留期延长,并在服务端保存延长状态。
- [测试] 完成临时 PostgreSQL/Redis 生产配置 smoke、迁移、持久化重载、队列租约和同源路由核验。
- [修复] 用户充值订单响应不再暴露内部支付适配器标识。
- [修复] Docker 前端与管理台构建阶段补齐共享 contracts 包,避免生产镜像构建时找不到本地依赖。
- [优化] PostgreSQL 仓储在同一事务中投影核心用户、余额、目录、渠道、任务、账单、上传、资产、消息和 WebDAV 配置关系表,并保留兼容快照用于未映射扩展字段。
- [优化] PostgreSQL 运行时投影扩展到认证挑战、会话族、管理员幂等/审批、消息与支付供应商、WebDAV 文件/重试任务、对象和密钥引用,新增第十二个 V1 迁移。
- [修复] 管理员 MFA 密钥、恢复码摘要和启停状态纳入加密持久化,服务重启后保持独立管理员认证状态。
- [修复] WebDAV 设置刷新后从服务端恢复脱敏状态,已保存连接可继续测试和同步且不会把密文凭证写入浏览器持久化。
- [新增] 套餐购买订单接入 mock/adapter 签名 webhook,按购买时权益快照幂等到账并写入独立套餐支付事件表。
- [修复] 支付 adapter 支持失败/退款状态校验,退款按原到账 bucket 写入幂等 refund 流水;管理员 MFA 二次登录不再重复要求已消费 CAPTCHA。
- [新增] 用户套餐目录详情和权益查询返回公开模型 ID,任务响应不再回显内部模型产品 ID。
- [修复] 管理台支付适配器凭证改为服务端密文保存,列表和写入响应只返回配置状态。
- [修复] 管理员 MFA challenge 的 CAPTCHA 豁免严格绑定管理员会话,阻止伪造 challengeId 绕过人机校验。
- [修复] 收紧任务未知结果、余额释放/结算、staging 保留期和渠道 SSRF 校验,避免未知任务冻结金币或提前删除结果。
- [调整] 管理台补齐模型产品、价格、渠道顺序、消息供应商/模板和存储策略的真实写入入口;生产 Compose 改为缺少密钥时直接拒绝启动。
- [优化] 公开任务请求优先使用后台发布的公开模型 ID,统一构建脚本加入服务端与管理台类型检查,模板快照移至设计参考目录。
- [修复] 收紧生产 CAPTCHA、用户 MFA、WS refresh 会话、3D 目录、WebDAV 路径和管理路由错误处理,并让持久化适配器在成功响应前完成提交。
- [新增] WebDAV manifest 增加版本、checksum、tombstone 和冲突副本;消息 outbox 支持加密目标与通用 HTTPS provider adapter。
- [修复] 补齐租约心跳、Redis Lua claim/renew/ack/nack 与死信、未知结果自动对账、WebDAV 离线重试、保留期操作和上传扫描外部适配边界。
- [新增] 扩展管理台真实业务入口:渠道探活/凭证轮换、模型分辨率、套餐发布回滚、支付适配器、存储策略、用户详情/调账、WS 指标和共享管理员 DTO。
- [修复] 修正 PostgreSQL 专用连接事务、文件快照原子写/版本校验、provider 输出 MIME 安全、结算失败终态和过期任务重试状态。
- [清理] 删除后台模板 mock/README 残留和前台供应商/自定义脚本死配置文案,更新当前实现摘要与验收状态。
- [修复] 完善 PostgreSQL 领域行投影、迁移互斥锁、revision 冲突检测、持久化故障可观测性,以及 WebDAV manifest ETag 冲突、删除重试和归档队列。
- [调整] 用户端注册验证完成后展示两步验证导入信息;移除前台版本更新面板和管理台未使用的模板结果资源。
- [修复] provider 已提交任务支持调用取消适配器,迟到响应保持取消终态;unknown attempt 保存 provider request ID 以便后续对账。
- [测试] 新增管理员 RBAC/敏感字段脱敏与 WebDAV If-Match 冲突自动化测试,服务端专项测试达到 16 项。
- [新增] 完成 V1 服务端网关关键闭环:独立管理员 audience、多管理员 MFA、审批式人工充值、套餐购买/权益快照、上传 quarantine 扫描、WebDAV manifest 版本和异步拆分/放大任务。
- [调整] 清理 TDesign mock/示例管理页面与用户端扩展插件入口,统一使用真实业务 API、公开模型目录和同源任务 WebSocket。
- [修复] 修正余额 bucket 按 reserve allocation 精确释放/结算/退款,防止管理员重试免费生成、用户自助伪充值和重复管理写入。
- [新增] 补充一期用户端、管理端、系统请求、数据关系和整体架构的 ASCII(SSI)与 Mermaid 图示,统一维护于 `system-diagrams.md`。
- [调整] 收敛一期决策:验证码防刷、用户/管理员两步验证、金币单位与换算、人工/mock 充值、异步队列与 WebSocket、本地+WebDAV、套餐权益、V2 内容治理和模板目录边界。
- [新增] 建立 `packages/contracts` 公共 API/任务事件契约与 `server` 同源网关基础,提供认证、模型目录、余额预扣、任务幂等和 WebSocket 事件骨架。
- [调整] 开发编排新增服务端进程,前后台 `/api` 请求统一代理到本地网关,移除 TDesign 模板对外部示例 API 的开发环境依赖。
- [调整] 前台生成链路统一改为平台任务网关,删除供应商配置、自定义调用脚本和远程模型配置入口,保留 WebDAV/本地存储设置。
## v0.0.1
+ [新增] 拉取 TDesign React 后台模板至 `admin/`,通过统一的 3000 端口提供前台 `/` 和后台 `/admin/`,并补充一体化开发与构建脚本。
+ [调整] 移除右上角 GitHub 入口及其前端组件代码。
+ [调整] 项目品牌统一为 MiragenFlow / 元境幻生,同步更新前端标题、配置与导出标识、WebDAV 默认目录、本地存储命名空间和内部插件 SDK scope。
+ [调整] 收缩产品范围,移除独立图片/视频工作台、全部视频能力、远程提示词、Local Agent/Codex/MCP/Skill、Codex App 与远程插件市场,保留首页、项目库、无限画布、资产库、图文音生成、图片工具、WebDAV 和内部插件 SDK。
+ [优化] 同步收缩后的文档导航、插件 SDK 说明和前端依赖,移除已下线功能的死链与构建依赖。
+ [修复] 统一开发启动时强制重建后台 Vite 依赖缓存,避免后台入口因过期预构建依赖返回 504 进入白屏。
- [新增] 拉取 TDesign React 后台模板至 `admin/`,通过统一的 3000 端口提供前台 `/` 和后台 `/admin/`,并补充一体化开发与构建脚本。
- [调整] 移除右上角 GitHub 入口及其前端组件代码。
- [调整] 项目品牌统一为 MiragenFlow / 元境幻生,同步更新前端标题、配置与导出标识、WebDAV 默认目录、本地存储命名空间和内部插件 SDK scope。
- [调整] 收缩产品范围,移除独立图片/视频工作台、全部视频能力、远程提示词、Local Agent/Codex/MCP/Skill、Codex App 与远程插件市场,保留首页、项目库、无限画布、资产库、图文音生成、图片工具、WebDAV 和内部插件 SDK。
- [优化] 同步收缩后的文档导航、插件 SDK 说明和前端依赖,移除已下线功能的死链与构建依赖。
- [修复] 统一开发启动时强制重建后台 Vite 依赖缓存,避免后台入口因过期预构建依赖返回 504 进入白屏。
## v0.16.0 - 2026-08-18
+ [新增] 提示词来源新增 Freestylefly GPT Image 2 内置来源。
+ [调整] 移除火山方舟协议及 Seedance 专用适配。
+ [调整] 画布默认使用移动工具,可直接拖动元素或空白画布,按住 Control 可临时框选节点。
+ [调整] 双击多图节点统一放大预览图片,仅通过右上角张数按钮展开或收起图片组。
+ [修复] 文本节点和生成配置新增独立的文本生成次数,默认一次且不再误用生图数量。
+ [修复] 节点缩放期间暂停渲染提示词面板,避免导入图片后立即缩放导致页面崩溃。
+ [优化] 生图、视频和音频请求遇到跨域拦截时明确提示需要通过自己的服务转发。
+ [优化] 图片工具条默认仅显示图标并移除图片、文本编辑入口,单击图片、文本或视频节点直接打开下方提示词面板。
+ [优化] 画布多图展开状态改为由用户主动控制,操作画布其他区域时不再自动收起。
+ [优化] 展开的多图子图支持双击直接放大预览,无需先设为主图。
- [新增] 提示词来源新增 Freestylefly GPT Image 2 内置来源。
- [调整] 移除火山方舟协议及 Seedance 专用适配。
- [调整] 画布默认使用移动工具,可直接拖动元素或空白画布,按住 Control 可临时框选节点。
- [调整] 双击多图节点统一放大预览图片,仅通过右上角张数按钮展开或收起图片组。
- [修复] 文本节点和生成配置新增独立的文本生成次数,默认一次且不再误用生图数量。
- [修复] 节点缩放期间暂停渲染提示词面板,避免导入图片后立即缩放导致页面崩溃。
- [优化] 生图、视频和音频请求遇到跨域拦截时明确提示需要通过自己的服务转发。
- [优化] 图片工具条默认仅显示图标并移除图片、文本编辑入口,单击图片、文本或视频节点直接打开下方提示词面板。
- [优化] 画布多图展开状态改为由用户主动控制,操作画布其他区域时不再自动收起。
- [优化] 展开的多图子图支持双击直接放大预览,无需先设为主图。
## v0.15.1 - 2026-08-07
+ [调整] 项目开源协议更换为MIT,允许所有人免费用于开源、闭源和商业场景。
- [调整] 项目开源协议更换为MIT,允许所有人免费用于开源、闭源和商业场景。
## v0.15.0 - 2026-08-07
+ [新增] 画布节点提示词支持弹窗放大编辑和实时同步。
+ [新增] 配置页新增本地存储、站点配额与对象仓库占用信息。
+ [新增] 画布多图支持失败项重试与删除、创建副本和单图下载。
+ [新增] Agent 输入框支持通过 `/` 快速选择 Skill,并通过 `@` 将画布素材按光标位置插入正文,输入及发送后均支持悬浮预览。
+ [优化] 画布新增选择与移动模式,并支持 Control 或空格临时反转工具。
+ [优化] 画布多图合并为可展开的单节点,支持实时状态、主图切换和四列布局。
+ [优化] 从左侧元素列表定位节点时,自动缩放上限调整为 100%。
+ [优化] Agent 用户消息元数据改为独立的版本化文件存储,刷新或重启后仍可恢复附件、Skill、画布引用及本地缩略图,未知存储格式不会被覆盖。
+ [修复] 关闭输入面板后保留图片与文本节点的编辑提示词。
+ [修复] 点击画布空白处时同步收起节点工具条和输入面板。
+ [修复] 空格键切换工具时不再误触当前聚焦的按钮。
+ [修复] 清理本地图片时保留工作台生成历史引用的文件。
+ [修复] 修复画布多图切换主图、重试和调整尺寸时的缩放与状态异常。
+ [修复] 统一画布多图操作按钮在不同主题下的颜色与对比度。
- [新增] 画布节点提示词支持弹窗放大编辑和实时同步。
- [新增] 配置页新增本地存储、站点配额与对象仓库占用信息。
- [新增] 画布多图支持失败项重试与删除、创建副本和单图下载。
- [新增] Agent 输入框支持通过 `/` 快速选择 Skill,并通过 `@` 将画布素材按光标位置插入正文,输入及发送后均支持悬浮预览。
- [优化] 画布新增选择与移动模式,并支持 Control 或空格临时反转工具。
- [优化] 画布多图合并为可展开的单节点,支持实时状态、主图切换和四列布局。
- [优化] 从左侧元素列表定位节点时,自动缩放上限调整为 100%。
- [优化] Agent 用户消息元数据改为独立的版本化文件存储,刷新或重启后仍可恢复附件、Skill、画布引用及本地缩略图,未知存储格式不会被覆盖。
- [修复] 关闭输入面板后保留图片与文本节点的编辑提示词。
- [修复] 点击画布空白处时同步收起节点工具条和输入面板。
- [修复] 空格键切换工具时不再误触当前聚焦的按钮。
- [修复] 清理本地图片时保留工作台生成历史引用的文件。
- [修复] 修复画布多图切换主图、重试和调整尺寸时的缩放与状态异常。
- [修复] 统一画布多图操作按钮在不同主题下的颜色与对比度。
## v0.14.0 - 2026-08-05
+ [新增] 文档站支持英文默认与简体中文切换,正文、导航、搜索及 LLM 文档接口按语言独立展示。
+ [新增] 前端搭建中英文国际化框架,支持从右上角一键持久化切换全局导航、首页、配置中心、Ant Design 和日期组件语言。
+ [修复] 统一 Ant Design 下拉菜单与选择器在浅色、深色主题下的弹层及交互状态颜色。
+ [修复] Agent 顶栏在英文模式或可用宽度不足时自适应隐藏标签文字,避免内容被裁切。
+ [新增] Agent 支持查看、创建、编辑、删除、启停和显式调用本地 Codex Skill,并同步多标签页变更。
+ [新增] Agent 支持从当前对话或当前画布生成可编辑的 Skill 草稿,并在用户确认后保存。
+ [调整] Agent 面板连接状态、内容标签和新对话统一排版,窄面板仅隐藏文字并保留图标与数量。
+ [优化] Agent 输入区控件随面板宽度在图标与图标加文字之间自适应。
+ [修复] Agent Skill 草稿期间保持 MCP 活动画布隔离,支持停止临时生成任务,并禁止并发修改 Skill。
+ [修复] Agent 新建或恢复对话时统一同步版本化会话与 MCP 初始化状态,避免多页面竞态误触 `409`、提前发送或任务后残留初始化卡片。
+ [优化] 提示词中心缩小卡片、每行展示四张,移除“加入我的资产”按钮边框。
+ [优化] 缩小生图与视频工作台的提示词库弹窗,精简卡片。
- [新增] 文档站支持英文默认与简体中文切换,正文、导航、搜索及 LLM 文档接口按语言独立展示。
- [新增] 前端搭建中英文国际化框架,支持从右上角一键持久化切换全局导航、首页、配置中心、Ant Design 和日期组件语言。
- [修复] 统一 Ant Design 下拉菜单与选择器在浅色、深色主题下的弹层及交互状态颜色。
- [修复] Agent 顶栏在英文模式或可用宽度不足时自适应隐藏标签文字,避免内容被裁切。
- [新增] Agent 支持查看、创建、编辑、删除、启停和显式调用本地 Codex Skill,并同步多标签页变更。
- [新增] Agent 支持从当前对话或当前画布生成可编辑的 Skill 草稿,并在用户确认后保存。
- [调整] Agent 面板连接状态、内容标签和新对话统一排版,窄面板仅隐藏文字并保留图标与数量。
- [优化] Agent 输入区控件随面板宽度在图标与图标加文字之间自适应。
- [修复] Agent Skill 草稿期间保持 MCP 活动画布隔离,支持停止临时生成任务,并禁止并发修改 Skill。
- [修复] Agent 新建或恢复对话时统一同步版本化会话与 MCP 初始化状态,避免多页面竞态误触 `409`、提前发送或任务后残留初始化卡片。
- [优化] 提示词中心缩小卡片、每行展示四张,移除“加入我的资产”按钮边框。
- [优化] 缩小生图与视频工作台的提示词库弹窗,精简卡片。
## v0.13.0 - 2026-08-03
+ [优化] Agent 进入空白对话或点击新对话后后台预热 Codex 与 MCP。
+ [优化] Agent 排查日志改为筛选栏结构化滚动列表,支持筛选、展开详情并折叠连续重复事件。
+ [优化] Agent 日志统一使用居中的回到底部入口,向上浏览时暂停跟随并可快速返回最新内容。
+ [优化] Agent 对话将同一轮连续命令合并为按数量折叠的命令组,默认隐藏冗长命令预览。
+ [修复] MCP 初始化期间禁用 Agent 按钮与回车发送,避免首条消息提前清除服务加载状态。
+ [修复] Agent 对话代码块关闭行号后多行内容被拼接为一行的问题。
+ [修复] 统一 Agent 实时事件与线程历史的消息归属,避免多窗口或刷新后出现过程重复等问题。
+ [修复] 节点缩放期间暂停渲染跟随节点的浮层工具条高频更新导致页面崩溃。
- [优化] Agent 进入空白对话或点击新对话后后台预热 Codex 与 MCP。
- [优化] Agent 排查日志改为筛选栏结构化滚动列表,支持筛选、展开详情并折叠连续重复事件。
- [优化] Agent 日志统一使用居中的回到底部入口,向上浏览时暂停跟随并可快速返回最新内容。
- [优化] Agent 对话将同一轮连续命令合并为按数量折叠的命令组,默认隐藏冗长命令预览。
- [修复] MCP 初始化期间禁用 Agent 按钮与回车发送,避免首条消息提前清除服务加载状态。
- [修复] Agent 对话代码块关闭行号后多行内容被拼接为一行的问题。
- [修复] 统一 Agent 实时事件与线程历史的消息归属,避免多窗口或刷新后出现过程重复等问题。
- [修复] 节点缩放期间暂停渲染跟随节点的浮层工具条高频更新导致页面崩溃。
## v0.12.1 - 2026-07-31
+ [新增] 画布右侧Agent支持按当前账号可用范围选择Codex 模型与推理强度。
+ [优化] Canvas Agent 升级至最新 Codex,启动时检查版本更新,
+ [优化] 统一通过公共日志输出 Info 以上信息,美化转发日志时间。
+ [优化] Agent高速流式回复取消逐词动画排队和当前消息离屏占位。
+ [修复] Agent首次发送消息时立即保留并展示用户内容,不再晚于思考状态。
- [新增] 画布右侧Agent支持按当前账号可用范围选择Codex 模型与推理强度。
- [优化] Canvas Agent 升级至最新 Codex,启动时检查版本更新,
- [优化] 统一通过公共日志输出 Info 以上信息,美化转发日志时间。
- [优化] Agent高速流式回复取消逐词动画排队和当前消息离屏占位。
- [修复] Agent首次发送消息时立即保留并展示用户内容,不再晚于思考状态。
## v0.12.0 - 2026-07-30
+ [新增] 画布 Agent 支持三档 Codex 权限,并可在对话中审批操作。
+ [新增] Canvas Agent 支持 `--debug` 日志并按日期保存。
+ [新增] Agent 新增可折叠任务进度,实时显示各步骤处理状态。
+ [调整] Agent 工具确认模式移至输入框并默认自动确认。
+ [调整] 进入画布后默认开启空白对话,历史对话改为主动恢复。
+ [修复] Agent 默认通过节点生成,明确指定时才使用 Codex 内置生图。
+ [修复] Agent 本地路径改为在文件管理器定位,不再拼接 localhost。
+ [修复] Agent 启用 Codex 思考摘要,并修复历史恢复后过程记录缺失。
+ [修复] 修复 Agent 图片预览打开后产生额外空行的问题。
+ [修复] Canvas Agent 停止任务仅中断 turn,不终止服务进程。
+ [修复] Agent 修复运行提示闪退和回复需切换标签页才显示的问题。
+ [修复] Agent 任务失败时立即结束等待,并在对话和历史中显示中文原因。
+ [修复] Agent 修复图片附件偏移,历史恢复保留缩略图且不回显内部说明。
+ [修复] 修复画布复制快捷键拦截 Agent 对话和节点详情文本复制。
+ [优化] Agent「新对话」立即清空并后台同步,不再等待创建 Codex 线程。
+ [优化] Agent「读取画布」按节点类型和连线分类展示内容概览。
+ [优化] Agent 发送后立即清空输入框并展示消息,避免内容滞留或草稿被清空。
+ [优化] Agent 动态工具卡片显示名称、状态和失败原因,替代「工具操作」。
+ [优化] Agent 面板标题、功能标签和新对话操作合并为单行顶栏。
+ [优化] Agent Markdown 代码块与外链确认弹窗改为紧凑中文样式。
+ [优化] Agent 思考摘要支持 Markdown 渲染,改善列表和代码阅读。
+ [优化] Agent 思考摘要和命令改为无边框折叠行,保留实时返回内容。
+ [优化] Agent 用户图片改为紧凑缩略图并支持单击放大。
+ [优化] Canvas Agent 的 Codex CLI 升级至 0.145.0。
+ [优化] Canvas Agent Debug 日志改为纯文本单行格式。
+ [优化] 精简 Agent HTTP 诊断日志,过滤轮询、流式重复事件和原始响应。
+ [优化] Agent 对话移除头像,工具活动紧凑展示,Token 用量支持数字动画。
+ [优化] Canvas Agent 流式回复改为增量传输,减少长对话重复渲染。
+ [优化] Agent 新增过程时间线,工具卡片改为中文摘要和可读详情。
+ [优化] Agent 长时间等待时显示阶段、时长和停止提示,避免空会话读取失败。
+ [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
+ [优化] Agent 优先操作当前画布,不再无故查询列表或重复导航。
+ [优化] Canvas Agent 独立维护工作区指令,不再重复拼接提示词。
+ [优化] Canvas Agent 目录按 Agent、画布、服务和工具拆分职责。
- [新增] 画布 Agent 支持三档 Codex 权限,并可在对话中审批操作。
- [新增] Canvas Agent 支持 `--debug` 日志并按日期保存。
- [新增] Agent 新增可折叠任务进度,实时显示各步骤处理状态。
- [调整] Agent 工具确认模式移至输入框并默认自动确认。
- [调整] 进入画布后默认开启空白对话,历史对话改为主动恢复。
- [修复] Agent 默认通过节点生成,明确指定时才使用 Codex 内置生图。
- [修复] Agent 本地路径改为在文件管理器定位,不再拼接 localhost。
- [修复] Agent 启用 Codex 思考摘要,并修复历史恢复后过程记录缺失。
- [修复] 修复 Agent 图片预览打开后产生额外空行的问题。
- [修复] Canvas Agent 停止任务仅中断 turn,不终止服务进程。
- [修复] Agent 修复运行提示闪退和回复需切换标签页才显示的问题。
- [修复] Agent 任务失败时立即结束等待,并在对话和历史中显示中文原因。
- [修复] Agent 修复图片附件偏移,历史恢复保留缩略图且不回显内部说明。
- [修复] 修复画布复制快捷键拦截 Agent 对话和节点详情文本复制。
- [优化] Agent「新对话」立即清空并后台同步,不再等待创建 Codex 线程。
- [优化] Agent「读取画布」按节点类型和连线分类展示内容概览。
- [优化] Agent 发送后立即清空输入框并展示消息,避免内容滞留或草稿被清空。
- [优化] Agent 动态工具卡片显示名称、状态和失败原因,替代「工具操作」。
- [优化] Agent 面板标题、功能标签和新对话操作合并为单行顶栏。
- [优化] Agent Markdown 代码块与外链确认弹窗改为紧凑中文样式。
- [优化] Agent 思考摘要支持 Markdown 渲染,改善列表和代码阅读。
- [优化] Agent 思考摘要和命令改为无边框折叠行,保留实时返回内容。
- [优化] Agent 用户图片改为紧凑缩略图并支持单击放大。
- [优化] Canvas Agent 的 Codex CLI 升级至 0.145.0。
- [优化] Canvas Agent Debug 日志改为纯文本单行格式。
- [优化] 精简 Agent HTTP 诊断日志,过滤轮询、流式重复事件和原始响应。
- [优化] Agent 对话移除头像,工具活动紧凑展示,Token 用量支持数字动画。
- [优化] Canvas Agent 流式回复改为增量传输,减少长对话重复渲染。
- [优化] Agent 新增过程时间线,工具卡片改为中文摘要和可读详情。
- [优化] Agent 长时间等待时显示阶段、时长和停止提示,避免空会话读取失败。
- [优化] Agent 历史记录支持多选批量删除,点击记录可直接进入对话。
- [优化] Agent 优先操作当前画布,不再无故查询列表或重复导航。
- [优化] Canvas Agent 独立维护工作区指令,不再重复拼接提示词。
- [优化] Canvas Agent 目录按 Agent、画布、服务和工具拆分职责。
## v0.11.0 - 2026-07-28
+ [新增] 画布文本生成新增推理强度设置,支持默认调用与自定义调用脚本读取所选档位。
+ [新增] 生图与视频工作台的参考资产区域支持直接拖动文件上传。
+ [新增] 配置与用户偏好支持通过配置文件导入和导出。
+ [新增] 模型渠道新增火山方舟协议。
+ [优化] 画布左侧元素列表新增图片放大预览操作。
+ [优化] 图片遮罩、切图与裁剪编辑支持常用快捷操作并修复高分辨率图片预览闪烁。
+ [优化] 提示词详情弹窗改为上下布局并限制显示尺寸。
+ [修复] 画布组装提示词浮层限制正文高度,长提示词改为区域内滚动查看。
+ [修复] 修复画布节点提示词过长时无法使用鼠标滚轮滚动的问题。
+ [修复] 修复画布重试时组装提示词重复以及切换类型调用模型不一致的问题。
+ [修复] 修复生成完成后再次选中画布节点时提示词不再回显的问题。
- [新增] 画布文本生成新增推理强度设置,支持默认调用与自定义调用脚本读取所选档位。
- [新增] 生图与视频工作台的参考资产区域支持直接拖动文件上传。
- [新增] 配置与用户偏好支持通过配置文件导入和导出。
- [新增] 模型渠道新增火山方舟协议。
- [优化] 画布左侧元素列表新增图片放大预览操作。
- [优化] 图片遮罩、切图与裁剪编辑支持常用快捷操作并修复高分辨率图片预览闪烁。
- [优化] 提示词详情弹窗改为上下布局并限制显示尺寸。
- [修复] 画布组装提示词浮层限制正文高度,长提示词改为区域内滚动查看。
- [修复] 修复画布节点提示词过长时无法使用鼠标滚轮滚动的问题。
- [修复] 修复画布重试时组装提示词重复以及切换类型调用模型不一致的问题。
- [修复] 修复生成完成后再次选中画布节点时提示词不再回显的问题。
## v0.10.0 - 2026-07-25
+ [新增] 提示词来源新增BananaPromptQuicker,并支持添加自定义标准 JSON 来源。
+ [新增] Agent 新增统一状态查询,支持查看画布、生图工作台和视频工作台任务进度。
+ [调整] 内置提示词来源改为读取ImagePrompts 统一JSON 数据,不再由画布端分别解析。
+ [调整] 提示词来源配置保留卡片式交互,并展示来源数量、同步状态和上次成功时间。
+ [优化] 提示词来源支持独立缓存和更新,更新失败时保留上一次成功内容。
+ [优化] Agent 对话区分用户与 AI 消息,并优化会话切换和回到最新消息的交互。
+ [优化] 画布生成完成后保留提示词输入,并改善提示词库和资产面板的交互性能。
+ [优化] 提示词中心标题居中、搜索输入增加防抖,并采用左侧筛选、右侧内容的双栏布局。
+ [修复] 本地 Agent 完善多标签页请求隔离、结果归属、焦点回退和 Codex 会话状态同步。
+ [修复] 本地 Agent 上传的图片附件可正确创建画布图片节点并连接生成流程。
+ [修复] 画布提示词库支持跨来源搜索,插入节点时保留提示词标题。
- [新增] 提示词来源新增BananaPromptQuicker,并支持添加自定义标准 JSON 来源。
- [新增] Agent 新增统一状态查询,支持查看画布、生图工作台和视频工作台任务进度。
- [调整] 内置提示词来源改为读取ImagePrompts 统一JSON 数据,不再由画布端分别解析。
- [调整] 提示词来源配置保留卡片式交互,并展示来源数量、同步状态和上次成功时间。
- [优化] 提示词来源支持独立缓存和更新,更新失败时保留上一次成功内容。
- [优化] Agent 对话区分用户与 AI 消息,并优化会话切换和回到最新消息的交互。
- [优化] 画布生成完成后保留提示词输入,并改善提示词库和资产面板的交互性能。
- [优化] 提示词中心标题居中、搜索输入增加防抖,并采用左侧筛选、右侧内容的双栏布局。
- [修复] 本地 Agent 完善多标签页请求隔离、结果归属、焦点回退和 Codex 会话状态同步。
- [修复] 本地 Agent 上传的图片附件可正确创建画布图片节点并连接生成流程。
- [修复] 画布提示词库支持跨来源搜索,插入节点时保留提示词标题。
## v0.9.0 - 2026-07-17
+ [新增] 左侧面板「资产」Tab 支持上传添加图片/视频资产、卡片悬停移除资产。
+ [新增] 左侧画布面板支持拖拽调整宽度、展开/收起(带动画),顶栏菜单左侧新增面板开关按钮。
+ [新增] 顶栏菜单新增「导出当前画布」,导出为包含全部资源的压缩包。
+ [新增] 左侧画布元素列表支持多选并批量导出选中元素为压缩包。
+ [新增] 可选的网站统计分析:支持 Google Analytics 4 与百度统计。
+ [调整] 画布节点名称默认不再显示,仅在选中/悬停/编辑时出现,画布更简洁。
+ [优化] 左侧面板「画布/资产」切换改为带滑动下划线的动画,移除非图片元素图标的灰色底色。
+ [优化] 画布节点提示词面板 `@` 引用图片时,输入框内直接显示真实缩略图。
+ [优化] 移除画布节点右上角的「图片1/文本1」资源角标,引用改在对话面板 `@` 直接选取。
+ [修复] 连接本地 Codex Agent 后,拖拽画布节点边框缩放等高频编辑导致页面崩溃。
- [新增] 左侧面板「资产」Tab 支持上传添加图片/视频资产、卡片悬停移除资产。
- [新增] 左侧画布面板支持拖拽调整宽度、展开/收起(带动画),顶栏菜单左侧新增面板开关按钮。
- [新增] 顶栏菜单新增「导出当前画布」,导出为包含全部资源的压缩包。
- [新增] 左侧画布元素列表支持多选并批量导出选中元素为压缩包。
- [新增] 可选的网站统计分析:支持 Google Analytics 4 与百度统计。
- [调整] 画布节点名称默认不再显示,仅在选中/悬停/编辑时出现,画布更简洁。
- [优化] 左侧面板「画布/资产」切换改为带滑动下划线的动画,移除非图片元素图标的灰色底色。
- [优化] 画布节点提示词面板 `@` 引用图片时,输入框内直接显示真实缩略图。
- [优化] 移除画布节点右上角的「图片1/文本1」资源角标,引用改在对话面板 `@` 直接选取。
- [修复] 连接本地 Codex Agent 后,拖拽画布节点边框缩放等高频编辑导致页面崩溃。
## v0.8.2 - 2026-07-16
+ [新增] 图像设置新增「透明背景」开关,开启后生成无背景的透明图像。
+ [优化] 画布节点输入区域移除灰色底色与边框、美化样式。
+ [修复] 画布节点提示词输入框补上悬停文本光标。
- [新增] 图像设置新增「透明背景」开关,开启后生成无背景的透明图像。
- [优化] 画布节点输入区域移除灰色底色与边框、美化样式。
- [修复] 画布节点提示词输入框补上悬停文本光标。
## v0.8.1 - 2026-07-16
+ [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。
+ [优化] 3D 全景(1.1.0)支持上传与 AI 生成并升级查看器;
+ [优化] HTML 节点(1.2.0)迁移到统一交互开关;
+ [优化] 便利贴(1.1.0)可拖动移动、自选颜色、移除资源角标与衍生功能;
+ [优化] SVG 节点(1.1.0)透明背景融入画布、可拖动、去除默认值;
+ [优化] 画布节点渲染性能:memo 化节点回调,交互时不再全量重渲染;
+ [修复] 修复 Markdown 节点在点击/移动视角时重复渲染导致图片反复请求的问题;
+ [修复] 修复插件版本号显示不更新,插件面板新增可升级绿点提醒。
- [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。
- [优化] 3D 全景(1.1.0)支持上传与 AI 生成并升级查看器;
- [优化] HTML 节点(1.2.0)迁移到统一交互开关;
- [优化] 便利贴(1.1.0)可拖动移动、自选颜色、移除资源角标与衍生功能;
- [优化] SVG 节点(1.1.0)透明背景融入画布、可拖动、去除默认值;
- [优化] 画布节点渲染性能:memo 化节点回调,交互时不再全量重渲染;
- [修复] 修复 Markdown 节点在点击/移动视角时重复渲染导致图片反复请求的问题;
- [修复] 修复插件版本号显示不更新,插件面板新增可升级绿点提醒。
## v0.8.0 - 2026-07-15
+ [新增] 画布节点插件系统:支持通过 URL 动态安装/启用/更新/卸载远程节点插件。
+ [新增] 插件开发 SDK,可用 TypeScript 开发画布节点插件。
+ [新增] 新增 Markdown、SVG、HTML、3D 全景、便利贴等示例插件。
+ [新增] 官方插件注册表:节点插件面板可从项目仓库读取官方插件列表并一键安装。
+ [新增] 在画布右上角工具栏新增「节点插件」入口。
+ [新增] 支持自定义生图/视频接口调用方式以适配不同中转站。
- [新增] 画布节点插件系统:支持通过 URL 动态安装/启用/更新/卸载远程节点插件。
- [新增] 插件开发 SDK,可用 TypeScript 开发画布节点插件。
- [新增] 新增 Markdown、SVG、HTML、3D 全景、便利贴等示例插件。
- [新增] 官方插件注册表:节点插件面板可从项目仓库读取官方插件列表并一键安装。
- [新增] 在画布右上角工具栏新增「节点插件」入口。
- [新增] 支持自定义生图/视频接口调用方式以适配不同中转站。
## v0.7.1 - 2026-07-15
+ [修复] 修复页面白屏报错的问题。
- [修复] 修复页面白屏报错的问题。
## v0.7.0 - 2026-07-14
+ [新增] Agent 对话消息改用 streamdown 流式渲染,提升Markdown 内容展示效果。
+ [新增] Agent 新增画布、工作台、提示词库和素材等站点级工具。
+ [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容。
+ [新增] Agent 新增 `site_navigate` 工具,支持页面跳转。
+ [新增] Agent 对话运行中支持一键停止,中断当前 Codex turn。
+ [新增] 画布节点支持统一维护名称字段,默认显示在节点上方,并可直接双击名称编辑。
+ [新增] 画布新增组节点,支持节点拖入/拆出分组、拖拽高亮吸附和移动组时带动子节点。
+ [新增] 画布空白区域支持双击打开节点选择菜单,并在点击位置创建节点。
+ [调整] Codex 会话改为站点级连续线程,跨页面和跨画布保持同一上下文。
+ [调整] 移除仅前端调用 OpenAI responses 接口,统一走 MCP + 本地 Codex 链路。
+ [调整] 画布节点顶部工具条改为点击选中节点后显示,避免鼠标经过节点时频繁弹出。
+ [优化] 本地 Agent 连接说明明确区分插件 / 手动 MCP 才会增加 Codex token 消耗。
+ [优化] 优化本地 Agent 连接说明,区分 Codex 插件启动和直接运行 Agent 两种方式。
+ [修复] 修复 Gemini 格式生图时因内置比例列表触发误报导致生成失败的问题。
- [新增] Agent 对话消息改用 streamdown 流式渲染,提升Markdown 内容展示效果。
- [新增] Agent 新增画布、工作台、提示词库和素材等站点级工具。
- [新增] Agent 面板改为全站常驻右侧栏,开关时同步推动顶栏和页面内容。
- [新增] Agent 新增 `site_navigate` 工具,支持页面跳转。
- [新增] Agent 对话运行中支持一键停止,中断当前 Codex turn。
- [新增] 画布节点支持统一维护名称字段,默认显示在节点上方,并可直接双击名称编辑。
- [新增] 画布新增组节点,支持节点拖入/拆出分组、拖拽高亮吸附和移动组时带动子节点。
- [新增] 画布空白区域支持双击打开节点选择菜单,并在点击位置创建节点。
- [调整] Codex 会话改为站点级连续线程,跨页面和跨画布保持同一上下文。
- [调整] 移除仅前端调用 OpenAI responses 接口,统一走 MCP + 本地 Codex 链路。
- [调整] 画布节点顶部工具条改为点击选中节点后显示,避免鼠标经过节点时频繁弹出。
- [优化] 本地 Agent 连接说明明确区分插件 / 手动 MCP 才会增加 Codex token 消耗。
- [优化] 优化本地 Agent 连接说明,区分 Codex 插件启动和直接运行 Agent 两种方式。
- [修复] 修复 Gemini 格式生图时因内置比例列表触发误报导致生成失败的问题。
## v0.6.0 - 2026-07-09
+ [新增] 新增Codex App插件支持。
+ [新增] 配置与用户偏好新增独立页面和 Codex 连接配置 Tab。
+ [新增] 新增GitHub Pages 前端静态站点发布 workflow。
+ [新增] 图片切图支持等分线直接拖拽调整,并可新增、删除和重置横向 / 纵向切图线。
+ [调整] Docker 运行镜像改为 nginx 静态托管。
+ [调整] 移除网站Agent模式,专注于连接Codex Agent操作画布
+ [修复] 修复生图工作台重试成功结果刷新后丢失的问题。
+ [修复] 修复 Gemini 调用格式生图未传递尺寸比例配置的问题。
+ [修复] 修复前端 TypeScript 构建报错。
+ [修复] 修复画布生成配置切换文本/视频/音频模式时模型仍显示为生图模型的问题。
+ [修复] 兼容中转站视频任务直接返回视频 URL 且没有 `/content` 接口的情况,并优化失败原因展示。
- [新增] 新增Codex App插件支持。
- [新增] 配置与用户偏好新增独立页面和 Codex 连接配置 Tab。
- [新增] 新增GitHub Pages 前端静态站点发布 workflow。
- [新增] 图片切图支持等分线直接拖拽调整,并可新增、删除和重置横向 / 纵向切图线。
- [调整] Docker 运行镜像改为 nginx 静态托管。
- [调整] 移除网站Agent模式,专注于连接Codex Agent操作画布
- [修复] 修复生图工作台重试成功结果刷新后丢失的问题。
- [修复] 修复 Gemini 调用格式生图未传递尺寸比例配置的问题。
- [修复] 修复前端 TypeScript 构建报错。
- [修复] 修复画布生成配置切换文本/视频/音频模式时模型仍显示为生图模型的问题。
- [修复] 兼容中转站视频任务直接返回视频 URL 且没有 `/content` 接口的情况,并优化失败原因展示。
## v0.5.0 - 2026-07-05
+ [新增] 渠道兼容Gemini格式。
+ [调整] 前端从 Next.js 迁移到 Vite,项目改为静态前端构建。
+ [调整] 移除已 404 的 EvoLinkAI 提示词来源。
- [新增] 渠道兼容Gemini格式。
- [调整] 前端从 Next.js 迁移到 Vite,项目改为静态前端构建。
- [调整] 移除已 404 的 EvoLinkAI 提示词来源。
## v0.4.0 - 2026-06-16
+ [新增] 新增网页版Agent Loop模式。
+ [新增] 支持Vercel一键部署。
+ [调整] 移除后端,项目定位为个人画布工具。
- [新增] 新增网页版Agent Loop模式。
- [新增] 支持Vercel一键部署。
- [调整] 移除后端,项目定位为个人画布工具。
## v0.3.0 - 2026-06-15
+ [新增] 新增canvas-agent通过codex操作画布。
- [新增] 新增canvas-agent通过codex操作画布。
## v0.2.5 - 2026-06-08
+ [新增] 新增图片切图功能。
+ [新增] 支持webdav同步数据。
+ [修复] 修复画布文字节点错误问题。
- [新增] 新增图片切图功能。
- [新增] 支持webdav同步数据。
- [修复] 修复画布文字节点错误问题。
## v0.2.4 - 2026-06-04
+ [新增] 新增图片反推提示词功能。
- [新增] 新增图片反推提示词功能。
## v0.2.3 - 2026-06-04
+ [新增] 新增图片蒙版局部修改功能。
+ [优化] 优化配置节点@图片功能。
- [新增] 新增图片蒙版局部修改功能。
- [优化] 优化配置节点@图片功能。
## v0.2.2 - 2026-06-04
+ [新增] 新增图片放大工具。
+ [优化] 优化图片工具条,增加自定义功能。
+ [修复] 修复端口冲突问题、pg/mysql未初始化问题。
- [新增] 新增图片放大工具。
- [优化] 优化图片工具条,增加自定义功能。
- [修复] 修复端口冲突问题、pg/mysql未初始化问题。
## v0.2.1 - 2026-06-03
+ [新增] 新增文档站点页面。
+ [优化] 优化画布连线交互。
+ [优化] 优化模型选择用户偏好。
- [新增] 新增文档站点页面。
- [优化] 优化画布连线交互。
- [优化] 优化模型选择用户偏好。
## v0.2.0 - 2026-06-01
+ [新增] 支持通过火山方舟AgentPlan接入。
+ [新增] 视频生成支持声音、水印及图片/视频/音频参考输入。
+ [新增] 画布新增音频节点。
+ [优化] 图片/视频素材支持 `图片1`编号注入提示词。
- [新增] 支持通过火山方舟AgentPlan接入。
- [新增] 视频生成支持声音、水印及图片/视频/音频参考输入。
- [新增] 画布新增音频节点。
- [优化] 图片/视频素材支持 `图片1`编号注入提示词。
## v0.1.1 - 2026-05-30
+ [新增] 支持New API跳转并自动填入Base URL和API Key配置。
- [新增] 支持New API跳转并自动填入Base URL和API Key配置。
## v0.1.0 - 2026-05-26
+ [优化] 优化我的画布、我的素材导出功能
+ [修复] 修复画布撤销,配置节点等bug问题
- [优化] 优化我的画布、我的素材导出功能
- [修复] 修复画布撤销,配置节点等bug问题
## v0.0.9 - 2026-05-26
+ [新增] 新增视频创作台页面。
+ [修复] 修复图片节点size参数传递问题。
- [新增] 新增视频创作台页面。
- [修复] 修复图片节点size参数传递问题。
## v0.0.8 - 2026-05-24
+ [新增] 新增用户账号与算力点体系,支持账号密码注册登录、Linux.do OAuth。
+ [新增] 管理后台公开配置支持设置模型算力点、支持计费查询。
+ [新增] 画布右上角展示用户算力点余额,生成按钮会展示本次预计消耗算力点。
+ [新增] 新增视频生成节点。
- [新增] 新增用户账号与算力点体系,支持账号密码注册登录、Linux.do OAuth。
- [新增] 管理后台公开配置支持设置模型算力点、支持计费查询。
- [新增] 画布右上角展示用户算力点余额,生成按钮会展示本次预计消耗算力点。
- [新增] 新增视频生成节点。
## v0.0.7 - 2026-05-23
+ [新增] 管理后台提示词管理支持多选批量删除。
+ [新增] 新增定义拉取GitHub提示词源功能。
+ [新增] 新增awesome-gpt-image2-prompts提示词来源。
+ [优化] 优化模型下拉选择样式、优化生图编辑设置
- [新增] 管理后台提示词管理支持多选批量删除。
- [新增] 新增定义拉取GitHub提示词源功能。
- [新增] 新增awesome-gpt-image2-prompts提示词来源。
- [优化] 优化模型下拉选择样式、优化生图编辑设置
## v0.0.6 - 2026-05-22
+ [新增] 管理后台支持配置模型渠道,前端当前无需鉴权即可直接使用后端渠道能力。
+ [优化] 统一整理后端错误提示、AI 代理、图片节点生成与重试、参考图缺失处理等细节。
+ [优化] 后端模型代理路径调整为 OpenAI 风格。
- [新增] 管理后台支持配置模型渠道,前端当前无需鉴权即可直接使用后端渠道能力。
- [优化] 统一整理后端错误提示、AI 代理、图片节点生成与重试、参考图缺失处理等细节。
- [优化] 后端模型代理路径调整为 OpenAI 风格。
## v0.0.5 - 2026-05-20
+ [新增] 右上角版本号支持点击查看版本更新弹窗,展示当前版本、最新版本和按时间线整理的更新日志。
+ [新增] 设置弹窗支持配置系统提示词,AI 生图、编辑图和文本请求会自动携带。
- [新增] 右上角版本号支持点击查看版本更新弹窗,展示当前版本、最新版本和按时间线整理的更新日志。
- [新增] 设置弹窗支持配置系统提示词,AI 生图、编辑图和文本请求会自动携带。
## v0.0.4 - 2026-05-20
+ [调整] Docker 运行入口改为 Next.js 对外提供页面,`/api/*` 由 Next.js 代理到内部 Go 服务。
+ [修复] 文本复制在局域网 IP 访问时可能失败的问题。
- [调整] Docker 运行入口改为 Next.js 对外提供页面,`/api/*` 由 Next.js 代理到内部 Go 服务。
- [修复] 文本复制在局域网 IP 访问时可能失败的问题。
## v0.0.3 - 2026-05-19
+ [修复] 更新 nanoid 依赖并修改 ID 生成方式,防止其他ip无法使用crypto模块导致的ID生成失败问题。
- [修复] 更新 nanoid 依赖并修改 ID 生成方式,防止其他ip无法使用crypto模块导致的ID生成失败问题。
## v0.0.2 - 2026-05-19
+ [新增] 增加生图工作台功能,支持文生图、图生图、查看历史记录,并增加移动端适配。
+ [修复] 画布生成尺寸控件支持选择更多常用比例,并可直接输入自定义比例。
+ [修复] 生成配置节点恢复拖拽操作,避免面板控件拦截整块节点拖动。
+ [文档] 增加 Render 部署说明。
- [新增] 增加生图工作台功能,支持文生图、图生图、查看历史记录,并增加移动端适配。
- [修复] 画布生成尺寸控件支持选择更多常用比例,并可直接输入自定义比例。
- [修复] 生成配置节点恢复拖拽操作,避免面板控件拦截整块节点拖动。
- [文档] 增加 Render 部署说明。
## v0.0.1 - 2026-05-19
+ [新增] 首次开源版本,包含无限画布能力:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。
+ [新增] AI 创作能力:支持 OpenAI 兼容接口的文生图、图生图、参考图编辑和文本问答。
+ [新增] 画布助手能力:支持围绕选中节点和上游节点对话、生图,并把结果插回画布。
+ [新增] 提示词库能力:抓取多个 GitHub 开源项目,按案例整理数百个图片提示词。
- [新增] 首次开源版本,包含无限画布能力:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。
- [新增] AI 创作能力:支持 OpenAI 兼容接口的文生图、图生图、参考图编辑和文本问答。
- [新增] 画布助手能力:支持围绕选中节点和上游节点对话、生图,并把结果插回画布。
- [新增] 提示词库能力:抓取多个 GitHub 开源项目,按案例整理数百个图片提示词。
+5 -3
View File
@@ -3,6 +3,8 @@ FROM oven/bun:1.3.13 AS web-build
WORKDIR /app/web
COPY web/package.json web/bun.lock ./
COPY packages/contracts/package.json /app/packages/contracts/package.json
COPY packages/contracts /app/packages/contracts
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --cache-dir=/root/.bun/install/cache
COPY VERSION /app/VERSION
COPY CHANGELOG.md /app/CHANGELOG.md
@@ -14,17 +16,17 @@ FROM oven/bun:1.3.13 AS admin-build
WORKDIR /app/admin
COPY admin/package.json admin/package-lock.json ./
COPY packages/contracts/package.json /app/packages/contracts/package.json
COPY packages/contracts /app/packages/contracts
RUN npm ci --ignore-scripts
COPY admin ./
RUN npm run build
# 运行镜像:只启动静态前端,AI 请求由浏览器前台直连用户自己的接口。
# 运行镜像:提供前台和管理台静态资源;API 网关由同一 compose 中的 server 服务通过 Nginx 同源反代。
FROM nginx:1.27-alpine
COPY --from=web-build /app/web/dist /usr/share/nginx/html
COPY --from=admin-build /app/admin/dist /usr/share/nginx/html/admin
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY web/docker-entrypoint.sh /docker-entrypoint.d/40-runtime-config.sh
RUN chmod +x /docker-entrypoint.d/40-runtime-config.sh
EXPOSE 3000
+14 -31
View File
@@ -6,18 +6,11 @@
<p align="center">
<a href="https://linux.do/"><img src="https://img.shields.io/badge/Linux.do-Community-2b6de8?style=flat-square" alt="Linux.do"></a>
<a href="https://render.com/deploy?repo=https://github.com/basketikun/infinite-canvas"><img src="https://img.shields.io/badge/Render-Deploy-46e3b7?style=flat-square&logo=render&logoColor=111111" alt="Deploy to Render"></a>
<a href="https://github.com/basketikun/infinite-canvas"><img src="https://img.shields.io/github/stars/basketikun/infinite-canvas?style=flat-square&logo=github" alt="GitHub stars"></a>
<a href="https://github.com/basketikun/infinite-canvas/tags"><img src="https://img.shields.io/github/v/tag/basketikun/infinite-canvas?style=flat-square&label=version" alt="Version"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-f97316?style=flat-square" alt="License"></a>
<a href="https://vite.dev/"><img src="https://img.shields.io/badge/Vite-7-646cff?style=flat-square&logo=vite&logoColor=white" alt="Vite"></a>
<a href="https://reactrouter.com/"><img src="https://img.shields.io/badge/React_Router-7-ca4245?style=flat-square&logo=reactrouter&logoColor=white" alt="React Router"></a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/50077?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-50077" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/50077" alt="basketikun%2Finfinite-canvas | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<a href="docs/content/docs/overview/quick-start.mdx">快速开始</a> · <a href="docs/content/docs/overview/features.mdx">功能介绍</a> · <a href="docs/content/docs/overview/render.mdx">Render 部署</a> · <a href="docs/content/docs/overview/docker.mdx">Docker 部署</a> · <a href="docs/content/docs/canvas/canvas-node-manual.mdx">画布节点操作手册</a> · <a href="docs/content/docs/canvas/canvas-shortcuts.mdx">画布快捷键</a> · <a href="SECURITY.md">漏洞提交</a> · <a href="docs/content/docs/progress/todo.mdx">待办事项</a>
</p>
@@ -62,11 +55,11 @@
- 首页与项目库:保留网站首页、多画布项目管理、导入导出和项目删除。
- 无限画布:节点拖拽缩放、连线、分组、小地图、撤销重做和多种画布背景。
- AI 创作:浏览器直连用户配置的接口,支持图片、文本和音频生成以及参考图编辑。
- AI 创作:通过同源平台网关提交图片、文本和音频异步任务,支持参考图编辑与 WebSocket 进度。
- 图片工具:反推提示词、多角度生成、裁剪、遮罩编辑、切分和本地放大。
- 资产与同步:本地资产库支持文本和图片;WebDAV 可同步画布、资产及图片/音频文件。
- 模型配置:支持多渠道、默认模型、配置导入导出,以及图片、文本和音频自定义调用脚本。
- 内部插件能力:保留画布节点注册、运行时和 TypeScript SDK,供项目内部构建扩展节点。
- 资产与同步:本地资产库支持文本、图片和音频;WebDAV 可同步画布、资产及媒体文件。
- 平台目录:用户只选择公开模型产品、能力、分辨率和金币价格,供应商渠道由管理台配置。
- 管理后台:提供用户、认证消息、渠道、模型、套餐、账本、任务、存储和审计管理。
完整功能说明见 [功能介绍](docs/content/docs/overview/features.mdx)。
@@ -74,13 +67,13 @@
## 快速开始
AI API Key、Base URL、画布和资产默认保存在浏览器本地,也可以使用 WebDAV 保存画布资产文件。
画布和资产默认保存在浏览器 IndexedDB/localForage;认证由同源服务端处理,provider 凭证只保存在服务端密文中。WebDAV 可保存画布、资产和媒体文件。
### 本地开发
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
npm --prefix admin install --ignore-scripts
cd web
bun install
@@ -92,16 +85,16 @@ bun run dev:all
### Docker 运行
```bash
git clone git@github.com:basketikun/infinite-canvas.git
cd infinite-canvas
docker compose up -d
git clone https://git.awaioi.com/awaioi/MiragenFlow.git
cd MiragenFlow
cp .env.example .env
# Fill every production secret in .env before starting the stack.
docker compose up -d --build
```
运行后默认只使用 3000 端口,可访问 `http://localhost:3000`;后台管理台位于 `http://localhost:3000/admin/`。
该 Compose 文件会同时构建前台/后台静态资源、API 服务,并启动 PostgreSQL 与 Redis;首次启动需要完成镜像构建和数据库迁移。运行后默认只使用 3000 端口,可访问 `http://localhost:3000`;后台管理台位于 `http://localhost:3000/admin/`。
首次打开后进入右上角配置,填入自己的 OpenAI 兼容 `Base URL` 和 `API Key`。
如果默认的 OpenAI 接口调用方式与您的 API 不同,可自定义图片、文本或音频脚本调用。
首次打开后注册或登录元境幻生账户。模型、渠道、余额和任务由平台服务端统一管理;用户端不保存供应商 `Base URL`、`API Key` 或自定义调用脚本。WebDAV 可在「存储设置」中配置,用于保存个人生成数据。
## 效果展示
@@ -145,13 +138,3 @@ docker compose up -d
## 开源协议
本项目使用 [MIT License](LICENSE)。任何人都可以免费使用、复制、修改、分发、再授权和商业使用本项目,也可以用于闭源产品。
## Star History
<a href="https://www.star-history.com/?repos=basketikun%2Finfinite-canvas&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=basketikun/infinite-canvas&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=basketikun/infinite-canvas&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=basketikun/infinite-canvas&type=date&legend=top-left" />
</picture>
</a>
+504
View File
@@ -0,0 +1,504 @@
# 元境幻生管理台一期菜单与交互图示规划
> 本文是管理台历史页面蓝本。当前长期目标的唯一权威入口是 [`docs/miragenflow-long-task-goal-prompt.md`](docs/miragenflow-long-task-goal-prompt.md);如旧蓝本与综合目标冲突,以综合目标为准,尤其是渠道只支持 OpenAI、使用统一弹窗和三个可自由切换 Tab。
> 本文是管理台重构的交互基线。本轮只规划,不改变后台业务接口。
> ASCII 图即 SSII 页面图;Mermaid 图用于菜单、权限和跨页面状态流转。
> 实现同步:当前代码已接入渠道/渠道组/模型产品编辑抽屉、渠道健康全部探活、审批状态筛选与驳回、任务详情/调用记录;认证消息列表不返回目标哈希。详情请求失败会保留抽屉并提供重试,设置与调用记录抽屉显式支持关闭按钮、遮罩和 ESC。管理台请求在 401 刷新后的重试会复用原始请求头和幂等键,渠道生成测试会按模型映射把平台显示模型 ID 转为供应商请求模型 ID。上述交互仍需浏览器逐页验收,未验证项保留在 `pending-test.mdx`。
## 1. 本轮结论与边界
### 1.1 必须修复的问题
| 问题 | 规划结论 |
| --- | --- |
| 系统概览 403、白板无权限 | 统一角色到权限映射;进入后台后先加载 `me` 和权限,再决定页面;无权限页必须提供返回总览/重新登录按钮。 |
| 用户管理没有新增用户 | 增加“新增用户”按钮和抽屉;管理员创建的用户默认状态由表单选择,密码只显示一次并强制首次修改。 |
| 认证消息换行 | 内容列使用多行预览抽屉,模板编辑使用 Textarea;表格不显示超长原文,只显示摘要和“查看详情”。 |
| 消息供应商 | 本期不接入、不显示在主菜单;保留后端适配器和页面占位,不提供未完成的配置表单。 |
| 渠道无法新增 | 页面只展示列表和“新增渠道”按钮;点击后打开弹窗/抽屉,不在首屏铺开全部字段。 |
| 渠道健康英文 | `healthy/degraded/open/disabled` 统一映射为“健康/降级/熔断/已停用”,状态用中文标签和颜色。 |
| 渠道分组、模型产品 | 分组先选渠道再排序;模型产品先选公开 ID、档位、能力、分辨率、价格和渠道组,未绑定渠道组时默认下架。 |
| capability 列异常换行 | 能力使用中文标签和固定宽度;超过宽度显示 `+N`,悬停或详情抽屉显示完整能力,不让 JSON 撑高表格。 |
| 价格规则不能调整 | 价格表显示版本、当前生效状态和 If-Match 版本;“调整价格”弹窗提交后刷新列表,冲突时显示当前版本并要求重载。 |
### 1.2 一期不进入可见菜单
- 消息供应商、消息模板:只保留认证消息列表和发送状态;供应商接入等待第三方邮件/SMS 文档。
- 视频、动画、真实 3D、团队、内容治理、插件市场、Local Agent、远程 Prompt:不在管理台或用户端出现入口。
- 3D 仅保留后台能力开关字段,关闭时返回 `CAPABILITY_NOT_ENABLED`。
## 2. 一级/二级菜单规划
后台左侧采用可折叠的二级菜单。一级标题只负责分组,不直接承载业务表格;二级菜单才是可访问页面。
```text
元境幻生管理台
├─ 01 总览
│ └─ 系统概览 /admin/dashboard/base admin:read
├─ 02 用户与认证
│ ├─ 用户管理 /admin/users admin:security
│ ├─ 认证消息 /admin/auth-messages admin:security
│ └─ 管理员安全 /admin/security/mfa admin:security
├─ 03 渠道与模型
│ ├─ 渠道管理 /admin/channels admin:read
│ ├─ 渠道健康 /admin/channel-health admin:read
│ ├─ 渠道分组 /admin/channel-groups admin:read
│ ├─ 模型产品 /admin/model-products admin:read
│ └─ 价格规则 /admin/pricing admin:read
├─ 04 计费与套餐
│ ├─ 金币单位与换算 /admin/billing/units admin:finance
│ ├─ 余额与账本 /admin/billing admin:finance
│ ├─ 套餐管理 /admin/plans admin:read
│ ├─ 充值订单 /admin/recharges admin:finance
│ ├─ 支付事件 /admin/payments admin:finance
│ ├─ 支付适配器 /admin/payment-providers admin:finance
│ └─ 审批中心 /admin/approvals admin:finance
├─ 05 任务与运行
│ ├─ 任务中心 /admin/tasks admin:read
│ └─ 实时指标 /admin/ws-metrics admin:read
├─ 06 存储与系统
│ ├─ 存储策略 /admin/storage admin:read
│ └─ 系统设置 /admin/settings admin:security
└─ 07 审计
├─ 日志大板 /admin/audit admin:audit
└─ 详细日志 /admin/audit-logs admin:audit
```
### 2.1 菜单交互规则
1. 一级菜单支持展开/收起;收起时完全隐藏侧栏,不保留窄图标栏。
2. 当前二级路由高亮;刷新后根据 URL 自动展开对应一级菜单。
3. 没有权限的二级项不显示,不允许先显示后跳 403。
4. 页面接口 403 时保留页面壳,显示“权限不足”卡片,不出现白板。
5. 侧栏底部固定放“帮助/退出登录”;供应商消息不出现在本期导航。
6. `admin:read` 只允许查看;新增、编辑、启停、调价等操作额外检查 `admin:write` 或 `admin:finance`。
7. 所有写操作统一使用弹窗/抽屉,成功后刷新对应列表并保留筛选条件。
## 3. 通用页面交互规范
```text
+--------------------------------------------------------------------------------+
| 一级菜单 / 二级菜单 面包屑 刷新 帮助 管理员菜单 |
+--------------------------------------------------------------------------------+
| 页面标题 + 描述 [搜索] [筛选] [批量操作] [新增/配置] |
+--------------------------------------------------------------------------------+
| 标签/统计卡片 |
+--------------------------------------------------------------------------------+
| 表格:选择 | 关键字段 | 状态 | 更新时间 | 操作(详情/编辑/启停/更多) |
| 固定列宽、中文状态、长文本摘要、分页、空态、错误态、重试 |
+--------------------------------------------------------------------------------+
| 右侧抽屉/弹窗:字段分组 | 校验提示 | 取消 | 保存/提交 |
+--------------------------------------------------------------------------------+
```
页面必须具备:加载态、空态、接口错误态、403 态、提交中态、成功 Toast、版本冲突态;禁止把所有配置字段直接堆在首屏。
## 4. 管理员登录与错误页 SSII
### 4.1 `/admin/login`
```text
+-----------------------------------------------+
| 元境幻生管理台 |
| 管理员登录 |
| 邮箱 [admin@admin.com.......................] |
| 密码 [••••••••••••••••......................] |
| [登录] |
| 登录失败:账号或密码不正确 / 账户已冻结 |
+-----------------------------------------------+
```
交互:登录按钮提交中禁用;MFA/CAPTCHA 开关开启时才动态插入对应步骤;成功进入 `/admin/dashboard/base`,不直接进入上一次无权限页面。
### 4.2 `/403` 与 `/404`
```text
+-------------------------------------------------------------+
| 403 你没有权限访问此页面 |
| 当前角色:{role} 需要权限:{scope} |
| [返回系统概览] [重新登录] [切换管理员] |
+-------------------------------------------------------------+
```
## 5. 业务页面 SSII 图示
### 5.1 `/admin/dashboard/base` 系统概览
```text
+--------------------------------------------------------------------------------+
| 系统概览 [时间范围▼] [刷新] [导出摘要] |
+--------------------------------------------------------------------------------+
| 用户总数 | 活跃用户 | 今日任务 | 今日消耗 | 队列等待 | 渠道告警 |
+--------------------------------------------------------------------------------+
| 任务趋势折线图 | 渠道健康卡片 |
| 图片/音频/工具任务 · 成功率 · 平均耗时 | 健康 12 降级 2 熔断 1 |
+--------------------------------------------------------------------------------+
| 待处理事项:渠道异常 / unknown 任务 / 审批 / WebDAV 失败 [查看全部] |
+--------------------------------------------------------------------------------+
```
交互:卡片点击进入带筛选条件的二级页面;无权限时显示权限卡片而非白板;接口失败可单独重试。
### 5.2 `/admin/users` 用户管理
```text
+--------------------------------------------------------------------------------+
| 用户管理 [新增用户] [导出] |
| [邮箱/手机号/ID 搜索........] [状态▼] [创建时间▼] [筛选] |
+--------------------------------------------------------------------------------+
| 选择 | 用户 | 联系方式 | 状态 | MFA | 余额 | 最近登录 | 操作 |
| □ | ... | 脱敏 | 正常 | 关 | ... | ... | [详情] [冻结] [更多] |
+--------------------------------------------------------------------------------+
| 新增用户抽屉:邮箱/手机号、昵称、初始密码、状态、是否发送欢迎消息 [保存] |
+--------------------------------------------------------------------------------+
```
交互:新增成功显示一次性初始密码;冻结/解冻和密码重置需要确认并写入审计;详情抽屉分账户、任务、账本、权益和审计 Tab。
### 5.3 `/admin/auth-messages` 认证消息
```text
+--------------------------------------------------------------------------------+
| 认证消息 [状态▼] [通道▼] [时间▼] [刷新] |
+--------------------------------------------------------------------------------+
| 目标(脱敏) | 类型 | 模板 | 状态 | 重试次数 | 创建时间 | 操作 |
| ... | 邮件 | 注册 | 成功 | 0 | ... | [查看详情] [重试] |
+--------------------------------------------------------------------------------+
| 消息详情抽屉:目标、用途、状态时间线、错误摘要、内容预览(保留换行) |
+--------------------------------------------------------------------------------+
```
交互:长文本只在抽屉中 `pre-wrap` 展示;重试需二次确认;不展示供应商密钥和未接入的供应商配置。
### 5.4 `/admin/security/mfa` 管理员安全
```text
+---------------------------------------------------------------+
| 管理员安全 |
| 当前管理员 | 角色 | MFA 状态 | 全局策略 |
| [启用 MFA] [验证设备] [关闭 MFA] |
| 恢复码:仅在生成后显示一次;关闭需要当前 TOTP + 原因 |
+---------------------------------------------------------------+
```
### 5.5 `/admin/channels` 渠道管理
```text
+--------------------------------------------------------------------------------+
| 渠道管理 [新增渠道] [批量启停] [刷新] |
| [名称/供应方式/模型 ID 搜索] [状态▼] [健康▼] |
+--------------------------------------------------------------------------------+
| 渠道名称 | 供应方式 | 能力/状态 | 健康 | 优先级 | 更新时间 | 操作 |
| ... | OpenAI | 图片 | 健康 | 1 | ... | [详情] |
+--------------------------------------------------------------------------------+
| 新增/编辑渠道弹窗: |
| [基础信息] [模型列表] [模型映射] |
| 基础信息:名称、完整请求地址示例、API Key;模型列表:主动拉取后勾选加入; |
| 模型映射:平台显示模型 ID ↔ 供应商请求模型 ID(可选) |
| [取消] [保存] |
+--------------------------------------------------------------------------------+
```
交互:首屏只显示列表和新增按钮;当前只支持 OpenAI 图片端点;拉取模型是管理员主动点击的独立动作,不在新增/编辑保存时隐式发起;模型列表与模型映射独立;密钥只写服务端密文;支持编辑回填、Tab 草稿保留、启停、凭证轮换、手动探活和版本冲突提示。渠道生成测试提交时,若存在平台显示模型 ID 到供应商请求模型 ID 的映射,必须使用供应商请求模型 ID 调用上游;无映射时沿用平台显示模型 ID。弹窗必须支持取消、右上角、遮罩和 ESC 关闭,异步成功自动关闭,失败保留表单。
### 5.6 `/admin/channel-health` 渠道健康
```text
+--------------------------------------------------------------------------------+
| 渠道健康 [全部探活] [自动刷新○] |
| [状态▼] [渠道组▼] [最近窗口▼] |
+--------------------------------------------------------------------------------+
| 渠道 | 中文状态 | 最近探活 | 延迟 | 连续失败 | 熔断剩余 | 操作 |
| ... | 健康/降级/熔断/已停用 | ... | ... | ... | ... | [单项探活] [查看日志] |
+--------------------------------------------------------------------------------+
```
### 5.7 `/admin/channel-groups` 渠道分组
```text
+--------------------------------------------------------------------------------+
| 渠道分组 [新增分组] [刷新] |
| 分组 | 用途/档位 | 状态 | 渠道数 | 重试预算 | 版本 | 操作 |
+--------------------------------------------------------------------------------+
| 旗舰图像组 | 旗舰 | 启用 | 4 | 3 | v5 | [配置顺序] [启停] [详情] |
+--------------------------------------------------------------------------------+
| 分组编辑抽屉:可用渠道池 → 已选渠道排序(拖拽) → 重试预算 → 保存 |
| 渠道 1 ──> 渠道 2 ──> 渠道 3 ──> 渠道 4 [模拟故障切换] |
+--------------------------------------------------------------------------------+
```
### 5.8 `/admin/model-products` 模型产品
```text
+--------------------------------------------------------------------------------+
| 模型产品 [新增产品] [发布/下架] |
| 公开 ID | 档位 | 能力 | 分辨率 | 绑定分组 | 价格 | 状态 | 版本 | 操作 |
| basic-image-v1 | 基础 | 文生图/图生图 | 低/中 | 基础组 | 10 | 上架 | v3 | [编辑] |
+--------------------------------------------------------------------------------+
| 产品抽屉:公开 ID/名称/档位 → 能力多选 → 分辨率矩阵 → 渠道组 → 价格预览 |
| 能力标签超出宽度显示 +N,点击“查看全部”打开能力详情,不撑高表格。 |
+--------------------------------------------------------------------------------+
```
### 5.9 `/admin/pricing` 价格规则
```text
+--------------------------------------------------------------------------------+
| 价格规则 [新增规则] [刷新] |
| 产品 | 分辨率 | 数量阶梯 | 基础价格 | 倍率 | 生效版本 | 状态 | 操作 |
+--------------------------------------------------------------------------------+
| ... | 中 | 1-4 | 10 | 1.0 | v7 | 生效 | [调整价格] |
+--------------------------------------------------------------------------------+
| 调价弹窗:新价格/倍率/生效时间/原因/预览;提交 If-Match;冲突时显示当前版本。 |
+--------------------------------------------------------------------------------+
```
### 5.10 `/admin/billing/units` 金币单位与换算
```text
+--------------------------------------------------------------------------+
| 金币单位 [编辑单位] |
| 图标 🪙 | 名称 金币 | 短码 COIN | 精度 0 | 换算比例 1:1 | 版本 v2 |
+--------------------------------------------------------------------------+
| 换算规则 [编辑单位] |
| 支付货币 | 金币比例 | 舍入方式 | 生效版本 | [编辑] |
+--------------------------------------------------------------------------+
```
### 5.11 `/admin/plans` 套餐管理
```text
+--------------------------------------------------------------------------------+
| 套餐管理 [新增套餐] [上架/下架] |
| 名称 | 时长 | 包含金币 | 售价 | 并发 | 队列级别 | 可用渠道组 | 状态 | 操作 |
+--------------------------------------------------------------------------------+
| ... | 30天 | 1000 | ¥99 | 2 | 1 | 基础/高级 | 上架 | [编辑] |
+--------------------------------------------------------------------------------+
| 套餐抽屉:名称/介绍/时长/包含金额/购买金额/并发/队列优先级/保留期/渠道组权益 |
+--------------------------------------------------------------------------------+
```
### 5.12 `/admin/billing`、`/admin/recharges`、`/admin/payments`、`/admin/approvals`
```text
+--------------------------------------------------------------------------------+
| 余额与账本 / 充值订单 / 支付事件 / 审批中心 [Tab 切换] [筛选] [导出] |
+--------------------------------------------------------------------------------+
| 余额与账本:用户 | available | reserved | reserve/settle/release/refund | 时间 |
| 充值订单:订单 | 用户 | 金额 | 状态 | mock/人工 | 创建时间 | [详情] |
| 支付事件:eventId | 订单 | 签名 | 金额 | 状态 | 收到时间 | [对账] |
| 审批中心:类型 | 申请人 | 审批人 | 状态 | 过期时间 | [审批] [驳回] |
+--------------------------------------------------------------------------------+
```
### 5.13 `/admin/payment-providers` 支付适配器
```text
+--------------------------------------------------------------------------------+
| 支付适配器 [新增适配器] [刷新] |
| 名称 | 模式(mock/adapter) | 状态 | 密钥状态 | 版本 | 更新时间 | 操作 |
+--------------------------------------------------------------------------------+
| ... | mock | 启用 | 已配置 | v2 | ... | [编辑] [探活] |
+--------------------------------------------------------------------------------+
| 适配器抽屉:名称/模式/回调地址/签名密钥(仅服务端保存)/启用状态 |
| [测试 webhook] [保存] |
+--------------------------------------------------------------------------------+
```
交互:仅保留 mock、人工充值和支付 adapter 接口;第三方支付文档到位后再接入具体 provider。
### 5.14 `/admin/tasks` 任务中心
```text
+--------------------------------------------------------------------------------+
| 任务中心 [状态▼] [类型▼] [模型▼] [刷新] |
| taskId | 用户 | 类型 | 公开模型 | 状态 | 预扣/实扣 | attempt | 更新时间 | 操作 |
+--------------------------------------------------------------------------------+
| ... | ... | 图片 | basic-v1 | 运行中 | 10/-- | 2 | ... | [详情] [取消] |
+--------------------------------------------------------------------------------+
| 任务详情抽屉:输入快照 → attempt 时间线 → 输出 → 账本 → 脱敏错误 → [重试/对账] |
+--------------------------------------------------------------------------------+
```
### 5.15 `/admin/ws-metrics`
```text
+--------------------------------------------------------------------------+
| 实时指标 [时间窗▼] [自动刷新○] [断线重连] |
| 在线 WS | 活跃订阅 | 事件吞吐 | 轮询降级 | 断线率 | 心跳超时 | 队列深度 |
+--------------------------------------------------------------------------+
| 时间序列:WS 连接 / 任务事件 / fallback 轮询 / Redis 租约 |
+--------------------------------------------------------------------------+
```
### 5.16 `/admin/storage` 与 `/admin/settings`
```text
+--------------------------------------------------------------------------------+
| 存储策略 [保存策略] [立即 GC] |
| 本地 staging TTL | WebDAV 保留期 | 最大延长 | 归档模式 | 扫描策略 | 版本 |
+--------------------------------------------------------------------------------+
| WebDAV 连接 | manifest | queued/running/conflict | 归档/删除任务 | [详情] |
| 系统设置 Tab:功能开关、用户认证增强、管理员安全增强、默认限流、审计保留期 |
+--------------------------------------------------------------------------------+
```
### 5.17 `/admin/audit` 与 `/admin/audit-logs`
```text
+--------------------------------------------------------------------------------+
| 审计日志 [导出摘要] [校验链] |
| 时间 | 操作者 | 角色 | 动作 | 对象 | requestId | 结果 | 操作 |
+--------------------------------------------------------------------------------+
| ... | admin@... | super_admin | pricing.update | ... | ... | 成功 | [详情] |
+--------------------------------------------------------------------------------+
| 详情抽屉:before/after 脱敏摘要、IP、关联任务、hash、前后链校验状态 |
+--------------------------------------------------------------------------------+
```
`/admin/audit` 是日志大板,负责分类、HTTP 结果和趋势概览;`/admin/audit-logs` 是详细日志列表,负责按业务分类查看具体操作人、请求来源和详情抽屉。两者共用 `audit-logs` 读取接口,但必须作为两个可见路由分别验收。
### 5.18 延期页面(不出现在菜单)
```text
消息供应商 /message-providers、消息模板 /message-templates
┌─────────────────────────────────────────────────────────────┐
│ 本期暂不接入第三方邮件/SMS provider │
│ 认证消息仍可查看 outbox 状态;供应商配置待文档确认后启用。 │
└─────────────────────────────────────────────────────────────┘
```
## 6. 管理台 Mermaid 图示
### 6.1 菜单与权限
~~~mermaid
flowchart TB
LOGIN[管理员邮箱+密码登录] --> SESSION[admin audience 会话]
SESSION --> POLICY{增强开关}
POLICY -->|MFA/CAPTCHA 关闭| RBAC[加载角色与 scopes]
POLICY -->|任一开启| CHALLENGE[按策略追加挑战]
CHALLENGE --> RBAC
RBAC --> ROOT[管理台壳层]
ROOT --> G[总览]
ROOT --> U[用户与认证]
ROOT --> C[渠道与模型]
ROOT --> B[计费与套餐]
ROOT --> T[任务与运行]
ROOT --> S[存储与系统]
ROOT --> A[审计]
U --> U1[用户管理]
U --> U2[认证消息]
U --> U3[管理员安全]
C --> C1[渠道]
C --> C2[健康]
C --> C3[分组]
C --> C4[模型产品]
C --> C5[价格规则]
B --> B1[金币/换算]
B --> B2[余额/充值/支付]
B --> B3[套餐]
B --> B4[审批]
T --> T1[任务中心]
T --> T2[WS 指标]
S --> S1[存储策略]
S --> S2[系统设置]
A --> A1[审计日志]
RBAC -.->|无 scope| FORBIDDEN[403 卡片:返回总览/重新登录]
~~~
### 6.2 渠道到模型产品的强交互流
~~~mermaid
sequenceDiagram
participant Admin as 管理员
participant UI as 管理台弹窗
participant API as Admin API
participant DB as 配置存储
Admin->>UI: 点击新增渠道
UI->>UI: 三个 Tab 共享一份草稿
Admin->>UI: 点击“拉取模型”(可选)
UI->>API: GET {baseUrl}/v1/models
API-->>UI: 候选模型列表(不自动创建映射)
Admin->>UI: 勾选模型并(可选)填写显示 ID/请求 ID 映射
UI->>API: POST/PATCH /admin/channels
API->>API: HTTPS、密钥、协议校验
API->>DB: 保存密文 + version=1
API-->>UI: 渠道摘要
Admin->>UI: 新增分组并拖拽渠道顺序
UI->>API: POST/PATCH /admin/channel-groups
API->>DB: 保存 channelIds + priorities + version
Admin->>UI: 新增模型产品
UI->>API: POST /admin/model-products
API->>API: 校验启用分组、能力、分辨率
API->>DB: 保存公开 ID/价格/路由快照
API-->>UI: 上架预览或校验错误
~~~
### 6.3 价格调整与版本冲突
~~~mermaid
stateDiagram-v2
[*] --> Loaded
Loaded --> Editing: 点击调整价格
Editing --> Submitting: 提交 If-Match(version)
Submitting --> Saved: 200 + version+1
Submitting --> Conflict: 409 VERSION_CONFLICT
Conflict --> Reloaded: 查看当前版本
Reloaded --> Editing: 重新编辑
Submitting --> Error: 422/503
Error --> Editing: 修正或重试
Saved --> Loaded: 刷新列表并保留筛选
~~~
### 6.4 后台任务监控
~~~mermaid
flowchart LR
FILTER[筛选/搜索] --> LIST[任务列表]
LIST --> DETAIL[详情抽屉]
DETAIL --> ATTEMPTS[attempt 时间线]
DETAIL --> OUTPUTS[输出/资产]
DETAIL --> LEDGER[预扣/结算/释放]
DETAIL -->|取消| CANCEL[任务取消 API]
DETAIL -->|重试| RETRY[幂等重试 API]
DETAIL -->|unknown| RECONCILE[对账 API]
CANCEL --> LIST
RETRY --> LIST
RECONCILE --> LIST
~~~
### 6.5 统一写入反馈
~~~mermaid
flowchart TD
ACTION[新增/编辑/启停/审批/调账] --> VALIDATE[前端字段校验]
VALIDATE --> API[带 If-Match + Idempotency-Key 的请求]
API -->|200/201| REFRESH[刷新当前列表/统计]
API -->|403| PERM[权限卡片]
API -->|409| VERSION[版本冲突弹窗]
API -->|422| FIELD[定位字段错误]
API -->|503| RETRY[保留表单并重试]
~~~
> 实施状态:P0 菜单/权限壳层已接入代码;用户、渠道、渠道组、模型产品和价格规则已切换为弹窗入口,认证消息详情/重试 API 已接入。管理台 401 自动刷新重试会保留原幂等键,渠道生成测试会执行显示模型 ID 到供应商请求模型 ID 的转换。完整逐页浏览器验收仍记录在 `docs/content/docs/progress/pending-test.mdx`。
## 7. 实施顺序与验收
1. **P0 壳层与权限**:修正角色 scopes、总览 403、403/404 按钮、二级菜单和面包屑。
2. **P0 渠道/模型闭环**:新增渠道弹窗、中文健康状态、分组排序、模型产品绑定、能力标签。
3. **P1 用户与认证**:新增用户抽屉、认证消息详情/多行模板展示;消息供应商页面保持隐藏。
4. **P1 计费**:金币单位、换算、价格调整、套餐和审批统一表单/版本冲突反馈。
5. **P1 任务/存储/审计**:详情抽屉、WS 指标、WebDAV 策略、审计详情。
6. **P2 视觉优化**:固定列宽、响应式抽屉、空/错/加载状态、中文标签、无障碍键盘操作。
验收必须逐页确认:
- 一级菜单可折叠,二级菜单和 URL 一致,权限不足不白屏。
- 每个新增/编辑动作都从按钮打开弹窗或抽屉,而不是把字段永久铺开。
- 渠道 → 分组 → 模型产品 → 价格规则可以连续完成并在列表中看到版本和状态。
- 用户管理可以新增、冻结、解冻、查看详情;认证消息多行内容不破坏表格布局。
- 所有状态、能力、错误码和操作按钮使用中文;长文本、能力标签、JSON 均不能撑高表格。
- 写操作成功、403、409、422、503、网络断开均有可见反馈和恢复路径。
+23 -104
View File
@@ -1,106 +1,25 @@
# 管理台变更记录
# Changelog
## 当前
## 0.3.1
### 🚀 Features
- 移除所有内置主题色相关代码,全部通过 tvision-color 计算获取 by @uyarn #144
- 卡片统一默认样式为无边框 by @uyarn #144
**Full Changelog**: https://github.com/Tencent/tdesign-react-starter/compare/0.3.0...0.3.1
## 0.3.0
❗ Breaking Changes
- 统一使用`.env`文件区分环境 生产环境改为`release` by @uyarn in https://github.com/Tencent/tdesign-react-starter/pull/136
🚀 Features
- 默认路由模式改为`history`路由 by @uyarn in https://github.com/Tencent/tdesign-react-starter/pull/136
**Full Changelog**: https://github.com/Tencent/tdesign-react-starter/compare/0.2.0...0.3.0
## 0.2.0
### ❗ Breaking Changes
- 升级组件库依赖至 0.43+ 更新主题色配色方案 by @uyarn in https://github.com/Tencent/tdesign-react-starter/pull/129
### 🚀 Features
- 新增自定义颜色面板选择 by @uyarn in https://github.com/Tencent/tdesign-react-starter/pull/129
### 🐞 Bug Fixes
- 修复卡片面板的标题丢失的问题 by @uyarn https://github.com/Tencent/tdesign-react-starter/pull/129
**Full Changelog**: https://github.com/Tencent/tdesign-react-starter/compare/0.1.6...0.2.0
## 0.1.6
### Features
- 升级组件库依赖至0.42+ 版本
### Bug Fixes
- 修复部分页面样式展示的缺陷
- 优化顶部菜单布局
## 0.1.5
## Features
- 新增卡片列表页 by @xucz in https://github.com/Tencent/tdesign-react-starter/pull/91
- 菜单路由配置`hidden`和`single`功能 by @xucz in https://github.com/Tencent/tdesign-react-starter/pull/95
## Bug Fixes
- 同步DatePicker组件升级的改动 by @uyarn in https://github.com/Tencent/tdesign-react-starter/pull/87
**Full Changelog**: https://github.com/Tencent/tdesign-react-starter/compare/0.1.4...0.1.5
## 0.1.4
## Features
- 增加面包屑导航
- 搜索框样式优化
- Layout组件命名语义化
- 图表主题功能优化
## 0.1.3
1. 升级tdesign-react
2. 替换为tdesign-react Card 组件
3. table样式优化
## 0.1.2
- 升级axios、tdesign @xucz
- fix: loss css token @uyarn
- 修复图表文字重叠 @xucz
## 0.1.1
## Features
- 新增登录页 #42 by @uyarn
- Logo点击跳转首页 #45 by @aouos
## 0.1.0
## Bug Fixes
- 修复页面主题色 by @xucz
- 修复页面布局
- 修复版本图表渲染问题
## Features
- 增加数据Mock by @xucz
- 用户端受保护路由在认证初始化和会话失效期间不再短暂渲染旧导航壳层,统一显示完整中文加载/跳转状态。
- PostgreSQL 快照重载会等待排队中的持久化写入,并在可重复读事务中读取多表状态,避免旧快照覆盖新写入或拼接出混合版本。
- 任务调用记录请求失败时保留抽屉并提供中文重试入口,避免错误后只显示一次提示就丢失上下文。
- 画布资源遮罩、侧栏悬停和设置入口改用主题令牌,快捷键与实时任务连接错误统一中文,旧英文副本标题会转换为中文后缀。
- 余额账本、充值订单和支付事件页面增加真实数据统计卡、ECharts 图表和日期范围筛选;渠道健康检查保留请求路径、发起状态和测试时间。
- 管理请求增加超时、取消和网络失败的中文反馈,健康检查与生图测试按钮使用不同动作名称。
- 渠道探活只在真正调用上游请求前标记“已发起”,通用供应商和地址校验失败不会虚报健康;关闭或重开弹窗后旧请求结果不会覆盖新表单,保存时也显示独立探活状态。
- 渠道模型探活显示独立加载状态、实际请求路径和中文失败原因,明确区分请求未发起与上游请求失败;修改地址或 API Key 后会清除旧结果。
- 渠道新增/编辑 Tab 支持自由切换,所有输入和模型映射在弹窗内保持,统一通过“保存/取消”提交或放弃。
- 登录和业务请求的未知错误统一使用中文提示,认证接口 401 不再触发无意义的会话刷新;登录页移除模板英文背景装饰。
- 图表空数据改为明确等待状态,深色主题下标题、图例和坐标轴文字自动使用可读颜色;首页统计卡统一使用数值和单位,模型成功率优先显示中文模型名称。
- 管理台系统概览、渠道健康、任务中心、实时指标和日志大板已统一使用 ECharts;审计菜单拆分为“日志大板”和“详细日志”,详细日志增加正常/异常状态列。
- 左侧菜单按业务入口使用差异化图标;只有一个子项的分组直接显示该页面,不再增加多余的展开层级。
- 用户列表的 ID 按当前列表从 1 连续显示;模型、套餐、渠道等业务标识保留真实值,状态、权限和对象类型使用中文映射,403 页面不再暴露内部权限编码。
- 审计日志按业务分类显示中文行为和可读操作人名称,管理员显示邮箱、普通用户显示用户名;没有实际按钮的列表不再显示空的“操作”列。
- 任务中心未知结果支持选择成功或失败完成对账,未知任务不再显示必然失败的取消操作;金币单位配置补齐精度和舍入方式。
- 审批列表隐藏申请人自己的审批按钮,审计日志增加可关闭的详情抽屉;新增渠道保存前自动执行探活并反馈结果。
- 管理台配置入口与服务端权限统一,财务配置仅对 finance 权限开放,避免点击后出现权限错误。
- 新增渠道、分组、模型和支付适配器由服务端生成可靠 ID;套餐、用户和模型表单在提交前校验真实业务约束,编辑类接口增加版本冲突保护。
- 管理台已接入元境幻生 V1 的用户、认证消息、渠道、模型、套餐、账本、任务、存储和审计业务。
- 页面通过同源 `/api/v1/admin/*` 请求服务端,敏感凭证只显示脱敏状态。
+2 -96
View File
@@ -1,97 +1,3 @@
<p style="display:flex; justify-content: center">
</p>
<p align="center">
<a href="https://tdesign.tencent.com/starter/react/#/dashboard/base" target="_blank">
<img alt="TDesign Logo" width="200" src="https://tdesign.gtimg.com/starter/brand-logo.svg">
</a>
</p>
<p align="center">
<a href="https://nodejs.org/en/about/releases/"><img src="https://img.shields.io/node/v/vite.svg" alt="node compatibility"></a>
<a href="https://github.com/Tencent/tdesign-react-starter/blob/develop/LICENSE">
<img src="https://img.shields.io/npm/l/tdesign-react.svg?sanitize=true" alt="License">
</a>
</p>
简体中文 | [English](./README.md)
### 项目简介
TDesign React Starter 是一个基于 tdesign-react,使用 `React`、`Vite2`开发,可进行个性化主题配置,旨在提供项目开箱即用的、配置式的中后台项目。
<img src="docs/docs-starter.png">
### 特性
- 内置多种常用的中后台页面
- 完善的目录结构
- 完善的代码规范配置
- 支持暗黑模式
- 自定义主题颜色
- 多种空间布局
- 内置 Mock 数据方案
### 开发
```bash
## 安装依赖
npm install
## 启动项目
npm run dev
## mock方式启动项目
npm run dev:mock
```
### 构建
```bash
## 构建正式环境
npm run build
## 构建测试环境
npm run build:test
```
### 其他
```bash
## 预览构建产物
npm run preview
## 代码格式检查
npm run lint
## 代码格式检查与自动修复
npm run lint:fix
```
### 如何贡献
非常欢迎您的贡献!提交您的 [Issue](https://github.com/tencent/tdesign-react-starter/issues/new/choose) 或者提交 [Pull Request](https://github.com/Tencent/tdesign-react-starter/pulls)。
#### Pull Request
1. Fork 代码!
2. 创建自己的分支: `git checkout -b feat/xxxx`
3. 提交你的修改: `git commit -a 'feat(project): describe'`
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交 `pull request`
#### 贡献提交规范
- [Angular Convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
### 兼容性
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br> IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 |
### 开源协议
TDesign 遵循 [MIT 协议](https://github.com/Tencent/tdesign-react-starter/LICENSE)。
# 元境幻生管理台
这是 MiragenFlow V1 的中文管理后台说明。请参阅 [README.md](./README.md) 获取开发、构建和权限边界说明。
+19 -85
View File
@@ -1,97 +1,31 @@
<p style="display:flex; justify-content: center">
# 元境幻生管理台
</p>
<p align="center">
<a href="https://tdesign.tencent.com/starter/react/#/dashboard/base" target="_blank">
<img alt="TDesign Logo" width="200" src="https://tdesign.gtimg.com/starter/brand-logo.svg">
</a>
</p>
`admin/` 是 MiragenFlow(元境幻生)V1 的管理员前端,基于 React、Vite 和 TDesign UI 组件构建。它只承载 `/admin/` 管理页面,不保存供应商密钥,不直接调用模型供应商接口。
<p align="center">
<a href="https://nodejs.org/en/about/releases/"><img src="https://img.shields.io/node/v/vite.svg" alt="node compatibility"></a>
<a href="https://github.com/Tencent/tdesign-react/blob/develop/LICENSE">
<img src="https://img.shields.io/npm/l/tdesign-react.svg?sanitize=true" alt="License">
</a>
</p>
English | [简体中文](./README-zh_CN.md)
### Introduction
TDesign React Starter is a TDesign-based project developed with `React`, `Vite`. It can be customized theme configuration, and aims to provide project out-of-the-box, configuration-style middle and background projects.
<p>
<a href="http://tdesign.tencent.com/starter/react/">Live Preview</a>
·
<a href="https://tdesign.tencent.com/starter/docs/react/get-started">Documentation</a>
</p>
<img src="docs/docs-starter.png">
### Features
- Various provided pages for develop
- Complete directory structure for develop
- Code specification configuration
- Support dark mode
- Custom theme colors
- Various space layouts
- Mock data scheme
### Usage
> Initialize project with our CLI tool `tdesign-starter-cli`
## 本地开发
```bash
## install tdesign-starter-cli
npm i tdesign-starter-cli@latest -g
## create project
td-starter init
```
### Develop
```bash
## install dependencies
npm install
## set up
npm run dev
npm run dev:linux -- --port 3003
```
### Build
通常使用仓库根目录的 `node scripts/dev-all.mjs` 同时启动前台、管理台和 API 网关。生产环境由同一公网 origin 提供 `/`、`/admin/`、`/api/` 和 WebSocket。
## 管理范围
- 管理员独立登录、MFA、RBAC 和会话退出
- 用户、认证消息和风控策略
- 模型产品、分辨率、价格、渠道、渠道组和健康状态
- 任务、attempt、队列、余额账本、充值订单、套餐和审批
- WebDAV/临时存储策略、消息 outbox、审计日志
所有写请求必须经过服务端 `/api/v1/admin/*` 鉴权、幂等校验、版本校验和审计。页面展示的渠道凭证始终是脱敏状态;support 角色不能读取原始提示词、文件或密钥。
## 构建与检查
```bash
## build
npm run build
## build for test
npm run build:test
npm run lint
```
### Contributing Guide
We welcome contributions to our project. Create your [Issue](https://github.com/tencent/tdesign-react-starter/issues/new/choose) or Submit your [Pull Request](https://github.com/Tencent/tdesign-react-starter/pulls).
#### Pull Request
1. Fork it!
2. Create your branch: `git checkout -b feat/xxxx`
3. Commit: `git commit -a 'feat(project): describe'`
4. Push: `git push origin feat/xxxx`
5. New Pull Request `pull request`
#### Commit Specification
- [Angular Convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
### Compatibility
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br> IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 |
### License
The MIT License. Please see [the license file](LICENSE) for more information.
管理台通过 `packages/contracts` 与前台共享公开 DTO、错误码和 WebSocket 事件类型。模板输入目录 `docs/design-reference/MiragenFlow-front/` 不属于管理台运行时依赖。
+2 -2
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -22,7 +22,7 @@
<body>
<!--[if IE]>
<h1 style="position: absolute; width: 100%; text-align: center; top: 45%">
建议使用 <a href="https://www.google.cn/chrome/">Chrome</a>
当前浏览器版本过旧,请更换现代浏览器
</h1>
<![endif]-->
<td-horizon></td-horizon>
-28
View File
@@ -1,28 +0,0 @@
const Mock = require('mockjs');
export default {
url: '/api/get-list',
method: 'get',
response: () => {
return {
code: 0,
msg: 'ok',
data: {
...Mock.mock({
'list|100': [
{
'index|+1': 1,
'status|1': '@natural(0, 4)',
no: 'BH00@natural(01, 100)',
name: '@city()办公用品采购项目',
'paymentType|1': '@natural(0, 1)',
'contractType|1': '@natural(0, 2)',
updateTime: '2020-05-30 @date("HH:mm:ss")',
amount: '@natural(10, 500),000,000',
adminName: '@cname()',
},
],
}),
},
};
},
};
-3
View File
@@ -1,3 +0,0 @@
import getList from './api/getList';
export default [getList];
+27 -288
View File
@@ -1,13 +1,15 @@
{
"name": "@tencent/tdesign-react-starter",
"name": "@miragenflow/admin",
"version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@tencent/tdesign-react-starter",
"name": "@miragenflow/admin",
"version": "0.3.1",
"dependencies": {
"@miragenflow/contracts": "file:../packages/contracts",
"@miragenflow/ui": "file:../packages/ui",
"@reduxjs/toolkit": "^1.8.5",
"axios": "^1.2.0",
"axios-jsonp": "^1.0.4",
@@ -16,20 +18,17 @@
"echarts": "^5.3.0",
"echarts-for-react": "^3.0.2",
"lodash": "^4.17.21",
"mockjs": "^1.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^7.2.4",
"react-router-dom": "^6.3.0",
"tdesign-icons-react": "^0.6.0",
"tdesign-react": "^1.15.0",
"tdesign-react": "1.15.1",
"tvision-color": "^1.5.0"
},
"devDependencies": {
"@honkhonk/vite-plugin-svgr": "^1.1.0",
"@types/echarts": "^4.9.13",
"@types/lodash": "^4.14.178",
"@types/mockjs": "^1.0.6",
"@types/node": "^20.2.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
@@ -46,8 +45,20 @@
"less": "^4.1.3",
"prettier": "^2.5.1",
"typescript": "^4.8.4",
"vite": "^2.9.15",
"vite-plugin-mock": "^2.9.6"
"vite": "^2.9.15"
}
},
"../packages/contracts": {
"name": "@miragenflow/contracts",
"version": "0.1.0"
},
"../packages/ui": {
"name": "@miragenflow/ui",
"version": "0.1.0",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"tdesign-react": "1.15.1"
}
},
"node_modules/@ampproject/remapping": {
@@ -2092,6 +2103,14 @@
"resolved": "https://mirrors.tencent.com/npm/@material/material-color-utilities/-/material-color-utilities-0.1.2.tgz",
"integrity": "sha512-uKkkx7Xo1AZJR0eMjH902rAb0vEjDbb3tgXt0cC3SHbpDGkPNhkuDKtxnw147axOM6uKuI7vywHRQlDrjDGhCA=="
},
"node_modules/@miragenflow/contracts": {
"resolved": "../packages/contracts",
"link": true
},
"node_modules/@miragenflow/ui": {
"resolved": "../packages/ui",
"link": true
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://mirrors.tencent.com/npm/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -2445,12 +2464,6 @@
"integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==",
"dev": true
},
"node_modules/@types/mockjs": {
"version": "1.0.10",
"resolved": "https://mirrors.tencent.com/npm/@types/mockjs/-/mockjs-1.0.10.tgz",
"integrity": "sha512-SXgrhajHG7boLv6oU93CcmdDm0HYRiceuz6b+7z+/2lCJPTWDv0V5YiwFHT2ejE4bQqgSXQiVPQYPWv7LGsK1g==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.17.12",
"resolved": "https://mirrors.tencent.com/npm/@types/node/-/node-20.17.12.tgz",
@@ -2894,19 +2907,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://mirrors.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://mirrors.tencent.com/npm/argparse/-/argparse-2.0.1.tgz",
@@ -3234,18 +3234,6 @@
"resolved": "https://mirrors.tencent.com/npm/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY="
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://mirrors.tencent.com/npm/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3404,42 +3392,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://mirrors.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://mirrors.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chroma-js": {
"version": "2.1.2",
"resolved": "https://mirrors.tencent.com/npm/chroma-js/-/chroma-js-2.1.2.tgz",
@@ -3482,14 +3434,6 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "13.0.0",
"resolved": "https://mirrors.tencent.com/npm/commander/-/commander-13.0.0.tgz",
"integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==",
"engines": {
"node": ">=18"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://mirrors.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz",
@@ -3502,36 +3446,6 @@
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
"dev": true
},
"node_modules/connect": {
"version": "3.7.0",
"resolved": "https://mirrors.tencent.com/npm/connect/-/connect-3.7.0.tgz",
"integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
"finalhandler": "1.1.2",
"parseurl": "~1.3.3",
"utils-merge": "1.0.1"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/connect/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://mirrors.tencent.com/npm/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/connect/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://mirrors.tencent.com/npm/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -3818,12 +3732,6 @@
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://mirrors.tencent.com/npm/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.80",
"resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz",
@@ -3836,15 +3744,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://mirrors.tencent.com/npm/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://mirrors.tencent.com/npm/errno/-/errno-0.1.8.tgz",
@@ -4395,12 +4294,6 @@
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://mirrors.tencent.com/npm/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://mirrors.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -5000,39 +4893,6 @@
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://mirrors.tencent.com/npm/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://mirrors.tencent.com/npm/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://mirrors.tencent.com/npm/find-up/-/find-up-5.0.0.tgz",
@@ -5598,18 +5458,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://mirrors.tencent.com/npm/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-boolean-object": {
"version": "1.2.1",
"resolved": "https://mirrors.tencent.com/npm/is-boolean-object/-/is-boolean-object-1.2.1.tgz",
@@ -6278,17 +6126,6 @@
"resolved": "https://mirrors.tencent.com/npm/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/mockjs": {
"version": "1.1.0",
"resolved": "https://mirrors.tencent.com/npm/mockjs/-/mockjs-1.1.0.tgz",
"integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
"dependencies": {
"commander": "*"
},
"bin": {
"random": "bin/random"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz",
@@ -6348,15 +6185,6 @@
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://mirrors.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://mirrors.tencent.com/npm/object-assign/-/object-assign-4.1.1.tgz",
@@ -6470,18 +6298,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://mirrors.tencent.com/npm/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
"dev": true,
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://mirrors.tencent.com/npm/once/-/once-1.4.0.tgz",
@@ -6594,15 +6410,6 @@
"node": ">= 0.10"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://mirrors.tencent.com/npm/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://mirrors.tencent.com/npm/path-exists/-/path-exists-4.0.0.tgz",
@@ -6635,12 +6442,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://mirrors.tencent.com/npm/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"dev": true
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://mirrors.tencent.com/npm/path-type/-/path-type-4.0.0.tgz",
@@ -6931,18 +6732,6 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://mirrors.tencent.com/npm/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://mirrors.tencent.com/npm/redux/-/redux-4.2.1.tgz",
@@ -7435,15 +7224,6 @@
"node": ">=0.10.0"
}
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://mirrors.tencent.com/npm/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/string-natural-compare": {
"version": "3.0.1",
"resolved": "https://mirrors.tencent.com/npm/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
@@ -7931,15 +7711,6 @@
"node": ">=4"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://mirrors.tencent.com/npm/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.2",
"resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
@@ -7979,15 +7750,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://mirrors.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
@@ -8033,29 +7795,6 @@
}
}
},
"node_modules/vite-plugin-mock": {
"version": "2.9.8",
"resolved": "https://mirrors.tencent.com/npm/vite-plugin-mock/-/vite-plugin-mock-2.9.8.tgz",
"integrity": "sha512-YTQM5Sn7t+/DNOwTkr+W26QGTCk1PrDkhGHslTJ90lIPJhJtDTwuSkEYMAuLP9TcVQ/qExTFx/x/GE3kxJ05sw==",
"dev": true,
"dependencies": {
"@types/mockjs": "^1.0.4",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"connect": "^3.7.0",
"debug": "^4.3.2",
"esbuild": "^0.14.36",
"fast-glob": "^3.2.7",
"path-to-regexp": "^6.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"mockjs": ">=1.1.0",
"vite": ">=2.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://mirrors.tencent.com/npm/which/-/which-2.0.2.tgz",
+8 -10
View File
@@ -1,9 +1,8 @@
{
"name": "@tencent/tdesign-react-starter",
"name": "@miragenflow/admin",
"version": "0.3.1",
"private": true,
"scripts": {
"dev:mock": "vite --open --mode mock",
"dev": "vite --open --mode development",
"dev:linux": "vite --mode development",
"build:test": "vite build --mode test",
@@ -11,8 +10,9 @@
"build:site": "vite build --mode site",
"site:preview": "npm run build && cp -r dist _site",
"preview": "vite preview --mode test",
"test": "echo \"no test specified,work in process\"",
"test:coverage": "echo \"no test specified,work in process\"",
"typecheck": "tsc --noEmit",
"test": "npm run typecheck",
"test:coverage": "npm run typecheck",
"lint": "eslint ./src --ext ts,tsx",
"lint:fix": "eslint ./src --ext ts,tsx --fix",
"prepare": "husky install"
@@ -21,7 +21,6 @@
"@honkhonk/vite-plugin-svgr": "^1.1.0",
"@types/echarts": "^4.9.13",
"@types/lodash": "^4.14.178",
"@types/mockjs": "^1.0.6",
"@types/node": "^20.2.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
@@ -38,10 +37,11 @@
"less": "^4.1.3",
"prettier": "^2.5.1",
"typescript": "^4.8.4",
"vite": "^2.9.15",
"vite-plugin-mock": "^2.9.6"
"vite": "^2.9.15"
},
"dependencies": {
"@miragenflow/contracts": "file:../packages/contracts",
"@miragenflow/ui": "file:../packages/ui",
"@reduxjs/toolkit": "^1.8.5",
"axios": "^1.2.0",
"axios-jsonp": "^1.0.4",
@@ -50,13 +50,11 @@
"echarts": "^5.3.0",
"echarts-for-react": "^3.0.2",
"lodash": "^4.17.21",
"mockjs": "^1.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^7.2.4",
"react-router-dom": "^6.3.0",
"tdesign-icons-react": "^0.6.0",
"tdesign-react": "^1.15.0",
"tdesign-react": "1.15.1",
"tvision-color": "^1.5.0"
},
"browserslist": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@@ -1,5 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" fill="#0052D9" stroke="none"/>
<path d="M23.9219 31H26.0859L22.0781 19.7266H19.8125L15.8125 31H17.8516L18.8203 28.1172H22.9688L23.9219 31ZM20.8359 21.875H20.9688L22.5 26.5234H19.2891L20.8359 21.875ZM30.6328 29.6172C29.7734 29.6172 29.1562 29.1875 29.1562 28.4688C29.1562 27.7734 29.6641 27.3828 30.75 27.3125L32.6797 27.1875V27.8672C32.6797 28.8594 31.8047 29.6172 30.6328 29.6172ZM30.0625 31.1406C31.1797 31.1406 32.1172 30.6562 32.5938 29.8281H32.7266V31H34.5938V25.1641C34.5938 23.3516 33.3594 22.2812 31.1641 22.2812C29.1328 22.2812 27.7188 23.2422 27.5625 24.75H29.3906C29.5703 24.1719 30.1797 23.8594 31.0703 23.8594C32.1172 23.8594 32.6797 24.3281 32.6797 25.1641V25.8828L30.4766 26.0156C28.3984 26.1406 27.2344 27.0312 27.2344 28.5781C27.2344 30.1406 28.4141 31.1406 30.0625 31.1406Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

@@ -1,5 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" fill="#0594FA" stroke="none"/>
<path d="M20.6641 31C23.0703 31 24.5391 29.7734 24.5391 27.7969C24.5391 26.3281 23.4922 25.2344 21.9766 25.0938V24.9531C23.1094 24.7734 23.9844 23.7266 23.9844 22.5391C23.9844 20.8047 22.6953 19.7266 20.5547 19.7266H15.8438V31H20.6641ZM17.8594 21.3281H20.0625C21.2812 21.3281 21.9922 21.9062 21.9922 22.8984C21.9922 23.9141 21.2344 24.4688 19.8047 24.4688H17.8594V21.3281ZM17.8594 29.3984V25.9219H20.125C21.6641 25.9219 22.4766 26.5156 22.4766 27.6406C22.4766 28.7891 21.6875 29.3984 20.2031 29.3984H17.8594ZM31.2812 31.1406C33.4375 31.1406 34.7891 29.4453 34.7891 26.7266C34.7891 23.9922 33.4453 22.3125 31.2812 22.3125C30.1094 22.3125 29.125 22.8828 28.6797 23.8125H28.5469V19.1484H26.6094V31H28.4766V29.6484H28.6094C29.0938 30.5859 30.0859 31.1406 31.2812 31.1406ZM30.6719 23.9609C31.9922 23.9609 32.7969 25.0078 32.7969 26.7266C32.7969 28.4453 32 29.4922 30.6719 29.4922C29.3438 29.4922 28.5156 28.4297 28.5156 26.7266C28.5156 25.0234 29.3516 23.9609 30.6719 23.9609Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,5 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" fill="#7587DB" stroke="none"/>
<path d="M21.1172 31.2812C23.7188 31.2812 25.625 29.7266 25.8906 27.4219H23.9062C23.6328 28.6875 22.5469 29.4844 21.1172 29.4844C19.1953 29.4844 18 27.9062 18 25.3594C18 22.8203 19.1953 21.2422 21.1094 21.2422C22.5312 21.2422 23.6172 22.1172 23.8984 23.4688H25.8828C25.6484 21.1172 23.6719 19.4453 21.1094 19.4453C17.9141 19.4453 15.9375 21.7031 15.9375 25.3672C15.9375 29.0156 17.9219 31.2812 21.1172 31.2812ZM35.3125 25.3281C35.1094 23.5312 33.7812 22.2812 31.5859 22.2812C29.0156 22.2812 27.5078 23.9297 27.5078 26.7031C27.5078 29.5156 29.0234 31.1719 31.5938 31.1719C33.7578 31.1719 35.1016 29.9688 35.3125 28.1797H33.4688C33.2656 29.0703 32.5938 29.5469 31.5859 29.5469C30.2656 29.5469 29.4688 28.5 29.4688 26.7031C29.4688 24.9297 30.2578 23.9062 31.5859 23.9062C32.6484 23.9062 33.2891 24.5 33.4688 25.3281H35.3125Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,5 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" fill="#00A870" stroke="none"/>
<path d="M15.875 19.7266V31H20.1016C23.4766 31 25.4062 28.9297 25.4062 25.3125C25.4062 21.7734 23.4531 19.7266 20.1016 19.7266H15.875ZM17.8906 21.4688H19.8359C22.0469 21.4688 23.3516 22.8828 23.3516 25.3438C23.3516 27.8594 22.0781 29.2578 19.8359 29.2578H17.8906V21.4688ZM30.5938 31.1406C31.7812 31.1406 32.7656 30.5859 33.25 29.6484H33.3828V31H35.2578V19.1484H33.3203V23.8125H33.1875C32.7344 22.875 31.7656 22.3125 30.5938 22.3125C28.4375 22.3125 27.0781 24.0156 27.0781 26.7188C27.0781 29.4375 28.4297 31.1406 30.5938 31.1406ZM31.1953 23.9609C32.5234 23.9609 33.3438 25.0234 33.3438 26.7266C33.3438 28.4453 32.5312 29.4922 31.1953 29.4922C29.8672 29.4922 29.0625 28.4531 29.0625 26.7266C29.0625 25.0078 29.875 23.9609 31.1953 23.9609Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 958 B

@@ -1,33 +0,0 @@
<svg width="200" height="140" viewBox="0 0 200 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g mask="url(#mask0_22_990)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M144.569 105.61L96.5692 133.322L48.5693 105.61V83.7121L96.569 55.9995L144.569 83.7122V105.61Z"
fill="#97A3B7" />
<g filter="url(#filter0_f_22_990)">
<rect x="-3" y="33.9995" width="80" height="60" fill="#E3E6EB" />
</g>
<g filter="url(#filter1_f_22_990)">
<rect x="97" y="97.9995" width="80" height="60" fill="#E3E6EB" />
</g>
</g>
<mask id="mask1_22_990" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="53" y="16" width="86" height="69">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M113.357 42.715L129.829 33.2059C128.859 32.4995 127.789 31.823 126.643 31.1615C121.268 28.0584 114.723 26.0153 107.758 25.023C103.549 19.4606 97.5775 16.1248 90.4344 16.1945C83.6788 16.2821 74.9482 21.9412 68.9271 30.5602C68.096 30.975 67.2847 31.4099 66.4949 31.8659C52.1168 40.1664 49.5542 52.6155 59.0218 61.9309C57.9871 56.1259 58.712 51.0657 62.1231 45.7161C62.0653 46.3482 61.9127 50.143 61.8906 50.7834C61.2209 69.6969 76.9107 84.8409 88.03 84.7113C96.4806 84.6119 103.595 79.6976 108.349 72.0797C114.563 70.8487 120.438 68.786 125.443 65.8968C138.919 58.1167 142.01 46.7146 134.547 37.629L117.948 47.2113C119.71 50.8655 117.997 55.034 112.87 57.9936C107.744 60.9532 100.523 61.9424 94.1928 60.9252C91.3499 60.4563 88.6706 59.5834 86.4524 58.3029L86.4042 58.275L113.357 42.715ZM78.6546 53.7727C72.5285 49.7965 72.9717 43.5469 79.8498 39.5762C86.7276 35.6056 97.5532 35.3496 104.441 38.8864L78.6546 53.7727ZM93.5561 18.1703C98.1657 18.1302 102.284 20.5752 105.496 24.7401C97.0486 23.8219 88.1122 24.4143 79.9732 26.5054C83.672 21.3809 88.444 18.2301 93.5561 18.1703ZM91.3238 81.6169C85.471 81.685 80.3525 77.691 76.9473 71.2848C85.7921 73.6267 95.8719 74.0599 105.374 72.6022C101.618 78.1222 96.6601 81.5531 91.3238 81.6169Z"
fill="currentcolor" />
</mask>
<g mask="url(#mask1_22_990)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M113.357 42.715L129.829 33.2059C128.859 32.4995 127.789 31.823 126.643 31.1615C121.268 28.0584 114.723 26.0153 107.758 25.023C103.549 19.4606 97.5775 16.1248 90.4344 16.1945C83.6788 16.2821 74.9482 21.9412 68.9271 30.5602C68.096 30.975 67.2847 31.4099 66.4949 31.8659C52.1168 40.1664 49.5542 52.6155 59.0218 61.9309C57.9871 56.1259 58.712 51.0657 62.1231 45.7161C62.0653 46.3482 61.9127 50.143 61.8906 50.7834C61.2209 69.6969 76.9107 84.8409 88.03 84.7113C96.4806 84.6119 103.595 79.6976 108.349 72.0797C114.563 70.8487 120.438 68.786 125.443 65.8968C138.919 58.1167 142.01 46.7146 134.547 37.629L117.948 47.2113C119.71 50.8655 117.997 55.034 112.87 57.9936C107.744 60.9532 100.523 61.9424 94.1928 60.9252C91.3499 60.4563 88.6706 59.5834 86.4524 58.3029L86.4042 58.275L113.357 42.715ZM78.6546 53.7727C72.5285 49.7965 72.9717 43.5469 79.8498 39.5762C86.7276 35.6056 97.5532 35.3496 104.441 38.8864L78.6546 53.7727ZM93.5561 18.1703C98.1657 18.1302 102.284 20.5752 105.496 24.7401C97.0486 23.8219 88.1122 24.4143 79.9732 26.5054C83.672 21.3809 88.444 18.2301 93.5561 18.1703ZM91.3238 81.6169C85.471 81.685 80.3525 77.691 76.9473 71.2848C85.7921 73.6267 95.8719 74.0599 105.374 72.6022C101.618 78.1222 96.6601 81.5531 91.3238 81.6169Z"
fill="currentcolor" />
<g opacity="0.3" filter="url(#filter2_f_22_990)">
<path d="M96.569 55.9995L144.569 83.7122V139.138L96.569 166.85L48.5692 139.138V83.7122L96.569 55.9995Z"
fill="#97A3B7" />
</g>
</g>
<ellipse cx="155" cy="78" rx="22" ry="22" transform="rotate(180 155 78)" fill="white" stroke="black" />
<path d="M155 83L155 65" stroke="black" />
<rect x="155" y="87" width="0.00390625" height="0.00390625" fill="#C4C4C4" stroke="black" stroke-width="2"
stroke-linejoin="round" />
<path d="M96.5693 112L96.5693 87.9995" stroke="white" stroke-width="2" />
<path d="M86.5693 97.9995L96.5693 87.9995L106.569 97.9995" stroke="white" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,49 +0,0 @@
<svg width="200" height="140" viewBox="0 0 200 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_216_313" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="80" y="9" width="78" height="88">
<path d="M119 9L157.105 31V75L119 97L80.8949 75V31L119 9Z" fill="currentColor"/>
</mask>
<g mask="url(#mask0_216_313)">
<path d="M119 9L157.105 31V75L119 97L80.8949 75V31L119 9Z" fill="currentColor"/>
<path opacity="0.9" d="M119 -35L157.105 -13V31L119 53L80.8949 31V-13L119 -35Z" fill="currentColor" />
<g opacity="0.3" filter="url(#filter0_f_216_313)">
<path d="M68 48L106.105 70V114L68 136L29.8949 114V70L68 48Z" fill="#97A3B7"/>
</g>
</g>
<mask id="mask1_216_313" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="29" y="48" width="78" height="88">
<path d="M68 48L106.105 70V114L68 136L29.8949 114V70L68 48Z" fill="#97A3B7"/>
</mask>
<g mask="url(#mask1_216_313)">
<path d="M68 48L106.105 70V114L68 136L29.8949 114V70L68 48Z" fill="#97A3B7"/>
<g filter="url(#filter1_f_216_313)">
<rect x="46.3906" y="92" width="80" height="60" fill="#E3E6EB"/>
</g>
<g filter="url(#filter2_f_216_313)">
<rect y="23" width="80" height="60" fill="#E3E6EB"/>
</g>
</g>
<path d="M41.8984 86.2866L44.7267 87.9197L44.727 94.4515L41.8987 92.8185L41.8984 86.2866Z" fill="white"/>
<path d="M53.9189 93.2273L56.7472 94.8603L56.7474 101.392L53.9191 99.7591L53.9189 93.2273Z" fill="white"/>
<path d="M44.7276 107.515L41.8994 105.882L41.8995 109.148L44.7278 110.781L44.7276 107.515L53.9195 112.823L53.9196 116.088L56.7479 117.721L56.7478 114.455L53.9195 112.823L53.9195 109.557L44.7275 104.249L44.7276 107.515Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.348 23.4792C106.188 25.9904 106.535 29.3829 109.395 31.5604C112.66 34.0461 117.963 34.007 121.24 31.4731C124.516 28.9392 124.526 24.8699 121.261 22.3841C118.401 20.2066 113.977 19.9666 110.721 21.6439L115.923 25.6047L113.55 27.4399L108.348 23.4792Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.865 33.0087L129.83 39.8346L132.203 37.9994L123.238 31.1735C122.917 31.521 122.554 31.8531 122.149 32.1657C121.745 32.4784 121.315 32.7594 120.865 33.0087ZM119.662 32.0932C120.123 31.8557 120.561 31.5798 120.967 31.2655C121.373 30.9513 121.73 30.6134 122.035 30.2579L122.035 30.2579C121.73 30.6134 121.373 30.9513 120.967 31.2655C120.561 31.5798 120.123 31.8557 119.662 32.0931L119.662 32.0932Z" fill="white"/>
<path d="M144 70L168.249 112H119.751L144 70Z" fill="white" stroke="#181818"/>
<path d="M144 100L144 82" stroke="#181818"/>
<path d="M144 105H144.004V105.004H144V105Z" stroke="#181818" stroke-width="2" stroke-linejoin="round"/>
<defs>
<filter id="filter0_f_216_313" x="23.8949" y="42" width="88.2102" height="100" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3" result="effect1_foregroundBlur_216_313"/>
</filter>
<filter id="filter1_f_216_313" x="-3.60938" y="42" width="180" height="160" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_216_313"/>
</filter>
<filter id="filter2_f_216_313" x="-50" y="-27" width="180" height="160" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_216_313"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

@@ -1,23 +0,0 @@
<svg width="200" height="140" viewBox="0 0 200 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g mask="url(#mask0_21_716)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M33 46.843L96.3214 119L159.643 46.843C142.742 31.9998 120.583 23 96.3214 23C72.0601 23 49.9009 31.9998 33 46.843Z"
fill="#97A3B7" />
<g filter="url(#filter0_f_21_716)">
<rect x="95" y="21" width="80" height="60" fill="#E3E6EB" />
</g>
<g filter="url(#filter1_f_21_716)">
<rect x="-7" y="43" width="80" height="60" fill="#E3E6EB" />
</g>
</g>
<path
d="M72.8122 63.6882L69.6548 66.8455L75.9009 73.0916C71.2469 75.1648 66.9663 77.925 63.188 81.2433L96.3213 119L108.234 105.425L114.647 111.837L117.804 108.68L80.4504 71.3261C80.4505 71.3261 80.4503 71.3261 80.4504 71.3261L72.8122 63.6882Z"
fill="currentcolor" />
<path
d="M129.455 81.2433L114.137 98.6982L85.3974 69.9585C88.9142 69.1786 92.5697 68.7674 96.3213 68.7674C109.016 68.7674 120.611 73.4766 129.455 81.2433Z"
fill="currentcolor" />
<path
d="M152 21.822L156.867 38.875L157 39.3405L157.469 39.2228L174.671 34.911L162.336 47.6522L161.999 48L162.336 48.3478L174.671 61.089L157.469 56.7772L157 56.6595L156.867 57.125L152 74.178L147.133 57.125L147 56.6595L146.531 56.7772L129.329 61.089L141.664 48.3478L142.001 48L141.664 47.6522L129.329 34.911L146.531 39.2228L147 39.3405L147.133 38.875L152 21.822Z"
fill="white" stroke="black" />
<path d="M101 31L90 42L101 53L93 61" stroke="white" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+12 -2
View File
@@ -47,6 +47,8 @@
.boardItemIcon {
opacity: 0.6;
width: 24px;
height: 24px;
}
.boardItemDesc {
@@ -59,12 +61,20 @@
.trendIcon {
border-radius: 50%;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
margin-right: 8px;
}
.trendIcon > svg {
width: 20px;
height: 20px;
}
.trendIconUp {
background: var(--td-error-color-2);
}
+3 -13
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { ChevronRightIcon } from 'tdesign-icons-react';
import { Card } from 'tdesign-react';
import { ChevronRightIcon, TrendDownIcon, TrendUpIcon } from 'components/LineIcons';
import { Card } from '@miragenflow/ui';
import classnames from 'classnames';
import Style from './index.module.less';
@@ -33,17 +33,7 @@ export const TrendIcon = ({ trend, trendNum }: { trend?: ETrend; trendNum?: stri
[Style.trendIconDown]: trend === ETrend.down,
})}
>
{trend === ETrend.up ? (
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M4.5 8L8 4.5L11.5 8' stroke='currentColor' strokeWidth='1.5' />
<path d='M8 5V12' stroke='currentColor' strokeWidth='1.5' />
</svg>
) : (
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M11.5 8L8 11.5L4.5 8' stroke='currentColor' strokeWidth='1.5' />
<path d='M8 11L8 4' stroke='currentColor' strokeWidth='1.5' />
</svg>
)}
{trend === ETrend.up ? <TrendUpIcon /> : <TrendDownIcon />}
</div>
{trendNum}
</div>
+17 -8
View File
@@ -1,19 +1,28 @@
import React from 'react';
import { DateRangePicker, DateRangeValue } from 'tdesign-react';
import { DateRangePicker, type DateRangeValue } from '@miragenflow/ui/date-picker';
import dayjs from 'dayjs';
const RECENT_7_DAYS: DateRangeValue = [
dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
];
type DatePickerProps = {
value?: DateRangeValue;
onChange?: (value: DateRangeValue) => void;
className?: string;
};
const LastWeekDatePicker = (onChange: (value: DateRangeValue) => void) => (
const LastWeekDatePicker = ({ value, onChange, className }: DatePickerProps) => (
<DateRangePicker
className={className}
mode='date'
placeholder={['开始时间', '结束时间']}
value={RECENT_7_DAYS}
value={value ?? undefined}
format='YYYY-MM-DD'
onChange={(value) => onChange(value)}
allowInput={false}
clearable
presets={{
'今天': [dayjs().toDate(), dayjs().toDate()],
'近 7 天': [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
'近 30 天': [dayjs().subtract(29, 'day').toDate(), dayjs().toDate()],
}}
onChange={(nextValue) => onChange?.(nextValue)}
/>
);
+42 -5
View File
@@ -1,5 +1,10 @@
import React, { memo } from 'react';
import { Button } from 'tdesign-react';
import { Space } from '@miragenflow/ui/space';
import { useLocation, useNavigate } from 'react-router-dom';
import { MiragenFlowButton } from '@miragenflow/ui';
import { useAppDispatch, useAppSelector } from 'modules/store';
import { logout } from 'modules/user';
import { adminLogout } from 'services/platform';
import Light403Icon from 'assets/svg/assets-result-403.svg?component';
import Light404Icon from 'assets/svg/assets-result-404.svg?component';
@@ -16,34 +21,66 @@ interface IErrorPageProps {
code: ECode;
title?: string;
desc?: string;
requiredScope?: string;
}
const errorInfo = {
[ECode.forbidden]: {
title: '403 Forbidden',
title: '403 无权限',
desc: '抱歉,您无权限访问此页面',
icon: <Light403Icon />,
},
[ECode.notFount]: {
title: '404 Not Found',
title: '404 页面不存在',
desc: '抱歉,您访问的页面不存在。',
icon: <Light404Icon />,
},
[ECode.error]: {
title: '500 Internal Server Error',
title: '500 服务异常',
desc: '抱歉,服务器出错啦!',
icon: <Light500Icon />,
},
};
const permissionLabels: Record<string, string> = {
super_admin: '超级管理员',
'admin:read': '查看后台',
'admin:write': '管理后台',
'admin:finance': '财务管理',
'admin:security': '安全管理',
'admin:audit': '审计查看',
};
const ErrorPage: React.FC<IErrorPageProps> = (props) => {
const info = errorInfo[props.code];
const navigate = useNavigate();
const dispatch = useAppDispatch();
const location = useLocation();
const admin = useAppSelector((state) => state.user.userInfo as { roles?: string[]; scopes?: string[] });
const state = (location.state || {}) as { requiredScope?: string; roles?: string[]; requestId?: string };
const requiredScope = props.requiredScope || state.requiredScope;
const roles = state.roles?.length ? state.roles : admin.roles;
const readableRoles = roles?.map((role) => permissionLabels[role] || '其他角色');
const readableScope = requiredScope ? permissionLabels[requiredScope] || '相应权限' : '未标注';
const logoutAndLogin = () => { adminLogout().finally(() => { dispatch(logout()); navigate('/login', { replace: true }); }); };
const switchAdmin = () => { adminLogout().finally(() => { dispatch(logout()); navigate('/login', { replace: true, state: { switchAccount: true } }); }); };
let requestInfo: React.ReactNode = null;
if (props.code === ECode.forbidden) {
requestInfo = <div className={style.description}>当前角色:{readableRoles?.join('、') || '未知'};所需权限:{readableScope}{state.requestId ? `;请求 ID:${state.requestId}` : ''}</div>;
} else if (state.requestId) {
requestInfo = <div className={style.description}>请求 ID:{state.requestId}</div>;
}
return (
<div className={style.errorBox}>
{info?.icon}
<div className={style.title}>{info?.title}</div>
<div className={style.description}>{info?.desc}</div>
<Button theme='primary'>返回首页</Button>
{requestInfo}
<Space>
<MiragenFlowButton type='primary' onClick={() => navigate('/dashboard/base', { replace: true })}>返回系统概览</MiragenFlowButton>
<MiragenFlowButton type='outline' onClick={logoutAndLogin}>重新登录</MiragenFlowButton>
{props.code === ECode.forbidden ? <MiragenFlowButton type='outline' onClick={switchAdmin}>切换管理员</MiragenFlowButton> : null}
</Space>
</div>
);
};
@@ -0,0 +1,22 @@
Lineicons Free 0.0.1
Copyright (c) Lineicons
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+113
View File
@@ -0,0 +1,113 @@
/*
* Localized subset of Lineicons Free 0.0.1 (MIT; see LineIcons.LICENSE.txt).
* Generated from @lineiconshq/free-icons so the admin does not load the full icon catalog.
*/
import React from 'react';
const C = React.createElement;
const o = (type, props) => React.createElement(type, { ...props, children: React.Children.toArray(props.children) });
var NC = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 25", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M22.0679 11.9341C22.2258 12.0716 22.3257 12.2741 22.3257 12.5C22.3257 12.7249 22.2267 12.9267 22.0699 13.0641L18.1064 17.0302C17.8136 17.3231 17.3387 17.3233 17.0457 17.0305C16.7528 16.7377 16.7526 16.2628 17.0454 15.9698L19.7635 13.25L4.88801 13.25L7.60597 15.9699C7.89876 16.2629 7.89859 16.7377 7.6056 17.0305C7.3126 17.3233 6.83773 17.3231 6.54494 17.0301L2.54761 13.03C2.25496 12.7372 2.25496 12.2626 2.54763 11.9697L6.54496 7.96984C6.83776 7.67686 7.31263 7.6767 7.60562 7.9695C7.8986 8.2623 7.89875 8.73718 7.60595 9.03016L4.88785 11.75L19.7633 11.75L17.0454 9.03014C16.7526 8.73715 16.7528 8.26227 17.0458 7.96948C17.3388 7.67669 17.8136 7.67686 18.1064 7.96986L22.0679 11.9341Z", fill: l }) });
var KC = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 25", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M11.5781 5.0638L11.5781 19.9403L8.85829 17.2222C8.5653 16.9294 8.09043 16.9295 7.79763 17.2225C7.50483 17.5155 7.50498 17.9904 7.79797 18.2832L11.7978 22.2805C12.0907 22.5732 12.5653 22.5732 12.8581 22.2805L16.8583 18.2832C17.1513 17.9904 17.1514 17.5155 16.8586 17.2225C16.5659 16.9295 16.091 16.9294 15.798 17.2222L13.0781 19.9401L13.0781 5.06363L15.798 7.78174C16.0909 8.07454 16.5658 8.07439 16.8586 7.7814C17.1514 7.48841 17.1513 7.01354 16.8583 6.72074L12.8886 2.75357C12.7512 2.59919 12.551 2.50195 12.3281 2.50195C12.1042 2.50195 11.9031 2.60013 11.7657 2.7558L7.79798 6.72072C7.50498 7.01351 7.50482 7.48838 7.79761 7.78138C8.0904 8.07438 8.56527 8.07454 8.85827 7.78175L11.5781 5.0638Z", fill: l }) });
var ao = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 25", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M21.0791 12.519C21.0744 12.7044 21.0013 12.8884 20.8599 13.0299L14.8639 19.0301C14.5711 19.3231 14.0962 19.3233 13.8032 19.0305C13.5103 18.7377 13.5101 18.2629 13.8029 17.9699L18.5233 13.2461L4.32813 13.2461C3.91391 13.2461 3.57813 12.9103 3.57812 12.4961C3.57812 12.0819 3.91391 11.7461 4.32812 11.7461L18.5158 11.7461L13.8029 7.03016C13.5101 6.73718 13.5102 6.2623 13.8032 5.9695C14.0962 5.6767 14.5711 5.67685 14.8639 5.96984L20.813 11.9228C20.976 12.0603 21.0795 12.2661 21.0795 12.4961C21.0795 12.5038 21.0794 12.5114 21.0791 12.519Z", fill: l }) });
var Cl = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M3.5 5.25C3.5 4.83579 3.16421 4.5 2.75 4.5C2.33579 4.5 2 4.83579 2 5.25V17.25C2 18.4926 3.00736 19.5 4.25 19.5H21.25C21.6642 19.5 22 19.1642 22 18.75C22 18.3358 21.6642 18 21.25 18H4.25C3.83579 18 3.5 17.6642 3.5 17.25V5.25Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M7 10.2773C5.89543 10.2773 5 11.1728 5 12.2773V15.7501C5 16.1643 5.33579 16.5001 5.75 16.5001H8.25C8.66421 16.5001 9 16.1643 9 15.7501V12.2773C9 11.1728 8.10457 10.2773 7 10.2773ZM6.5 12.2773C6.5 12.0012 6.72386 11.7773 7 11.7773C7.27614 11.7773 7.5 12.0012 7.5 12.2773V15.0001H6.5V12.2773Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M10.5 6.5C10.5 5.39543 11.3954 4.5 12.5 4.5C13.6046 4.5 14.5 5.39543 14.5 6.5V15.7501C14.5 16.1643 14.1642 16.5001 13.75 16.5001H11.25C10.8358 16.5001 10.5 16.1643 10.5 15.7501V6.5ZM12.5 6C12.2239 6 12 6.22386 12 6.5V15.0001H13V6.5C13 6.22386 12.7761 6 12.5 6Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M18 8.05859C16.8954 8.05859 16 8.95402 16 10.0586V15.7501C16 16.1643 16.3358 16.5001 16.75 16.5001H19.25C19.6642 16.5001 20 16.1643 20 15.7501V10.0586C20 8.95402 19.1046 8.05859 18 8.05859ZM17.5 10.0586C17.5 9.78245 17.7239 9.55859 18 9.55859C18.2761 9.55859 18.5 9.78245 18.5 10.0586V15.0001H17.5V10.0586Z", fill: e })] });
var il = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M17.8076 2C18.2218 2 18.5576 2.33579 18.5576 2.75V2.91898C19.3061 3.10815 19.8599 3.78601 19.8599 4.59323C19.8599 5.00744 19.5242 5.34323 19.1099 5.34323C18.6957 5.34323 18.3599 5.00744 18.3599 4.59323C18.3599 4.46796 18.2584 4.36641 18.1331 4.36641H17.7381C17.4714 4.36641 17.2553 4.58257 17.2553 4.84921C17.2553 5.05046 17.3801 5.2306 17.5686 5.30127L18.5734 5.67811C19.3473 5.96833 19.8599 6.70814 19.8599 7.53464C19.8599 8.39065 19.3175 9.11996 18.5576 9.39759V9.63372C18.5576 10.0479 18.2218 10.3837 17.8076 10.3837C17.3934 10.3837 17.0576 10.0479 17.0576 9.63372V9.46488C16.3092 9.2757 15.7553 8.59785 15.7553 7.79062C15.7553 7.37641 16.0911 7.04062 16.5053 7.04062C16.9195 7.04062 17.2553 7.37641 17.2553 7.79062C17.2553 7.91589 17.3568 8.01744 17.4821 8.01744H17.8771C18.1438 8.01744 18.3599 7.80129 18.3599 7.53464C18.3599 7.3334 18.2351 7.15326 18.0467 7.08259L17.0418 6.70575C16.268 6.41552 15.7553 5.67571 15.7553 4.84921C15.7553 3.99321 16.2977 3.2639 17.0576 2.98626V2.75C17.0576 2.33579 17.3934 2 17.8076 2Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.5 3.25045C4.25736 3.25045 3.25 4.25781 3.25 5.50045V18.5005C3.25 19.7431 4.25736 20.7505 5.5 20.7505H5.83333C7.07597 20.7505 8.08333 19.7431 8.08333 18.5005V5.50045C8.08333 4.25781 7.07598 3.25045 5.83333 3.25045H5.5ZM4.75 5.50045C4.75 5.08624 5.08579 4.75045 5.5 4.75045H5.83333C6.24755 4.75045 6.58333 5.08624 6.58333 5.50045V18.5005C6.58333 18.9147 6.24755 19.2505 5.83333 19.2505H5.5C5.08579 19.2505 4.75 18.9147 4.75 18.5005V5.50045Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.833 9.63424C10.5904 9.63424 9.58301 10.6416 9.58301 11.8842V18.5005C9.58301 19.7432 10.5904 20.7505 11.833 20.7505H12.1663C13.409 20.7505 14.4163 19.7432 14.4163 18.5005V11.8842C14.4163 10.6416 13.409 9.63424 12.1663 9.63424H11.833ZM11.083 11.8842C11.083 11.47 11.4188 11.1342 11.833 11.1342H12.1663C12.5806 11.1342 12.9163 11.47 12.9163 11.8842V18.5005C12.9163 18.9147 12.5806 19.2505 12.1663 19.2505H11.833C11.4188 19.2505 11.083 18.9147 11.083 18.5005V11.8842Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M15.916 14.9223C15.916 13.6797 16.9234 12.6723 18.166 12.6723H18.4993C19.742 12.6723 20.7493 13.6797 20.7493 14.9223V18.5005C20.7493 19.7432 19.742 20.7505 18.4993 20.7505H18.166C16.9234 20.7505 15.916 19.7432 15.916 18.5005V14.9223ZM18.166 14.1723C17.7518 14.1723 17.416 14.5081 17.416 14.9223V18.5005C17.416 18.9147 17.7518 19.2505 18.166 19.2505H18.4993C18.9136 19.2505 19.2493 18.9147 19.2493 18.5005V14.9223C19.2493 14.5081 18.9136 14.1723 18.4993 14.1723H18.166Z", fill: e })] });
var vl = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.0016 2.00098C12.4158 2.00098 12.7516 2.33676 12.7516 2.75098V3.53801C16.5416 3.9143 19.5016 7.11197 19.5016 11.001V14.115L20.1938 15.9609C20.7454 17.4319 19.6581 19.001 18.0871 19.001H15.0628C15.0287 20.6631 13.6701 21.9995 11.9998 21.9995C10.3295 21.9995 8.97089 20.6631 8.93682 19.001H5.9161C4.34514 19.001 3.25776 17.4319 3.80936 15.9609L4.5016 14.115V11.001C4.5016 7.11197 7.46161 3.9143 11.2516 3.53801V2.75098C11.2516 2.33676 11.5874 2.00098 12.0016 2.00098ZM10.4375 19.001C10.471 19.8339 11.1573 20.4995 11.9998 20.4995C12.8423 20.4995 13.5286 19.8339 13.5622 19.001H10.4375ZM6.0016 11.001C6.0016 7.68727 8.68789 5.00098 12.0016 5.00098C15.3153 5.00098 18.0016 7.68727 18.0016 11.001V14.1168C18.0016 14.2955 18.0337 14.4727 18.0965 14.64L18.7893 16.4876C18.9732 16.9779 18.6108 17.501 18.0871 17.501H5.9161C5.39244 17.501 5.02998 16.9779 5.21385 16.4876L5.90673 14.64C5.96946 14.4727 6.0016 14.2955 6.0016 14.1168V11.001Z", fill: l }) });
var Ue = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8.40585 5.78711C8.23261 5.4368 8.13525 5.04227 8.13525 4.625C8.13525 3.17525 9.31051 2 10.7603 2C11.4948 2 12.1588 2.30168 12.6353 2.78788C13.1117 2.30168 13.7757 2 14.5103 2C15.96 2 17.1353 3.17525 17.1353 4.625C17.1353 5.04227 17.0379 5.4368 16.8647 5.78711H19.1433C20.386 5.78711 21.3933 6.79447 21.3933 8.03711V8.50001C21.3933 9.31501 20.96 10.0288 20.3112 10.4236V19C20.3112 20.2426 19.3038 21.25 18.0612 21.25H7.2096C5.96696 21.25 4.9596 20.2426 4.9596 19V10.4236C4.31077 10.0288 3.87744 9.31501 3.87744 8.50001V8.03711C3.87744 6.79447 4.8848 5.78711 6.12744 5.78711H8.40585ZM14.5103 3.5C13.8889 3.5 13.3853 4.00368 13.3853 4.625V5.75H14.5103C15.1316 5.75 15.6353 5.24632 15.6353 4.625C15.6353 4.00368 15.1316 3.5 14.5103 3.5ZM11.8853 5.75V4.625C11.8853 4.00368 11.3816 3.5 10.7603 3.5C10.1389 3.5 9.63525 4.00368 9.63525 4.625C9.63525 5.24632 10.1389 5.75 10.7603 5.75H11.8853ZM18.8112 10.75H6.4596V19C6.4596 19.4142 6.79538 19.75 7.2096 19.75H18.0612C18.4754 19.75 18.8112 19.4142 18.8112 19V10.75ZM19.1433 9.25001C19.5576 9.25001 19.8933 8.91422 19.8933 8.50001V8.03711C19.8933 7.6229 19.5576 7.28711 19.1433 7.28711H6.12744C5.71323 7.28711 5.37744 7.6229 5.37744 8.03711V8.50001C5.37744 8.91422 5.71323 9.25001 6.12744 9.25001H19.1433Z", fill: l }) });
var Bi = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M7.5 5.75C7.5 5.33579 7.83579 5 8.25 5H15.75C16.1642 5 16.5 5.33579 16.5 5.75V7.25C16.5 7.66421 16.1642 8 15.75 8H8.25C7.83579 8 7.5 7.66421 7.5 7.25V5.75Z", fill: e }), C("path", { d: "M8.5 9.85C8.00294 9.85 7.6 10.2529 7.6 10.75C7.6 11.2471 8.00294 11.65 8.5 11.65C8.99706 11.65 9.4001 11.2471 9.4001 10.75C9.4001 10.2529 8.99706 9.85 8.5 9.85Z", fill: e }), C("path", { d: "M11.1 10.75C11.1 10.2529 11.5029 9.85 12 9.85C12.4971 9.85 12.9001 10.2529 12.9001 10.75C12.9001 11.2471 12.4972 11.65 12.0001 11.65C11.503 11.65 11.1 11.2471 11.1 10.75Z", fill: e }), C("path", { d: "M15.5 9.85C15.0029 9.85 14.6 10.2529 14.6 10.75C14.6 11.2471 15.0029 11.65 15.5 11.65C15.9971 11.65 16.4001 11.2471 16.4001 10.75C16.4001 10.2529 15.9971 9.85 15.5 9.85Z", fill: e }), C("path", { d: "M7.6 14.25C7.6 13.7529 8.00294 13.35 8.5 13.35C8.99706 13.35 9.4001 13.7529 9.4001 14.25C9.4001 14.7471 8.99716 15.15 8.5001 15.15C8.00304 15.15 7.6 14.7471 7.6 14.25Z", fill: e }), C("path", { d: "M12 13.35C11.5029 13.35 11.1 13.7529 11.1 14.25C11.1 14.7471 11.5029 15.15 12 15.15C12.4971 15.15 12.9001 14.7471 12.9001 14.25C12.9001 13.7529 12.4971 13.35 12 13.35Z", fill: e }), C("path", { d: "M14.6 14.25C14.6 13.7529 15.0029 13.35 15.5 13.35C15.9971 13.35 16.4001 13.7529 16.4001 14.25C16.4001 14.7471 15.9972 15.15 15.5001 15.15C15.003 15.15 14.6 14.7471 14.6 14.25Z", fill: e }), C("path", { d: "M8.5 16.85C8.00294 16.85 7.6 17.2529 7.6 17.75C7.6 18.2471 8.00294 18.65 8.5 18.65C8.99706 18.65 9.4001 18.2471 9.4001 17.75C9.4001 17.2529 8.99706 16.85 8.5 16.85Z", fill: e }), C("path", { d: "M11.1 17.75C11.1 17.2529 11.5029 16.85 12 16.85C12.4971 16.85 12.9001 17.2529 12.9001 17.75C12.9001 18.2471 12.4972 18.65 12.0001 18.65C11.503 18.65 11.1 18.2471 11.1 17.75Z", fill: e }), C("path", { d: "M15.5 16.85C15.0029 16.85 14.6 17.2529 14.6 17.75C14.6 18.2471 15.0029 18.65 15.5 18.65C15.9971 18.65 16.4001 18.2471 16.4001 17.75C16.4001 17.2529 15.9971 16.85 15.5 16.85Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M4.5 4.25C4.5 3.00736 5.50736 2 6.75 2H17.25C18.4926 2 19.5 3.00736 19.5 4.25V19.75C19.5 20.9926 18.4926 22 17.25 22H6.75C5.50736 22 4.5 20.9926 4.5 19.75V4.25ZM6.75 3.5C6.33579 3.5 6 3.83579 6 4.25V19.75C6 20.1642 6.33579 20.5 6.75 20.5H17.25C17.6642 20.5 18 20.1642 18 19.75V4.25C18 3.83579 17.6642 3.5 17.25 3.5H6.75Z", fill: e })] });
var Jr = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M15.5071 10.5245C15.8 10.2316 15.8 9.75674 15.5071 9.46384C15.2142 9.17095 14.7393 9.17095 14.4464 9.46384L10.9649 12.9454L9.55359 11.5341C9.2607 11.2412 8.78582 11.2412 8.49293 11.5341C8.20004 11.827 8.20004 12.3019 8.49294 12.5947L10.4346 14.5364C10.7275 14.8293 11.2023 14.8292 11.4952 14.5364L15.5071 10.5245Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12Z", fill: e })] });
var td = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M20.75 18.5V8.67794L19.25 10.1779V18.5C19.25 18.9142 18.9142 19.25 18.5 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V5.5C4.75 5.08579 5.08579 4.75 5.5 4.75H18.314L19.2936 3.77034C19.3842 3.67974 19.4806 3.59848 19.5816 3.52657C19.2607 3.35027 18.8921 3.25 18.5 3.25H5.5C4.25736 3.25 3.25 4.25736 3.25 5.5V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H18.5C19.7426 20.75 20.75 19.7426 20.75 18.5Z", fill: e }), C("path", { d: "M20.4838 6.51868C20.7767 6.22578 20.7767 5.75091 20.4838 5.45802C20.1909 5.16512 19.7161 5.16512 19.4232 5.45802L11.9298 12.9514L8.57686 9.59849C8.28396 9.3056 7.80909 9.3056 7.5162 9.5985C7.22331 9.89139 7.22331 10.3663 7.5162 10.6592L11.3995 14.5424C11.6924 14.8353 12.1672 14.8353 12.4601 14.5424L20.4838 6.51868Z", fill: e })] });
var Md = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M5.54779 9.09467C5.84069 8.80178 6.31556 8.80178 6.60846 9.09467L12.3281 14.8143L18.0478 9.09467C18.3407 8.80178 18.8156 8.80178 19.1085 9.09467C19.4013 9.38756 19.4013 9.86244 19.1085 10.1553L12.8585 16.4053C12.5656 16.6982 12.0907 16.6982 11.7978 16.4053L5.54779 10.1553C5.2549 9.86244 5.2549 9.38756 5.54779 9.09467Z", fill: l }) });
var Id = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M16.1864 3.75C15.9591 2.74801 15.063 2 13.9922 2H10.0547C8.98389 2 8.08781 2.74801 7.86044 3.75H6.77344C5.5308 3.75 4.52344 4.75736 4.52344 6V19.75C4.52344 20.9926 5.5308 22 6.77344 22H17.2734C18.5161 22 19.5234 20.9926 19.5234 19.75L19.5234 6C19.5234 4.75736 18.5161 3.75 17.2734 3.75H16.1864ZM9.30469 4.25C9.30469 3.83579 9.64047 3.5 10.0547 3.5H13.9922C14.4064 3.5 14.7422 3.83578 14.7422 4.25L14.7422 4.71875C14.7422 5.13296 14.4064 5.46875 13.9922 5.46875H10.0547C9.64047 5.46875 9.30469 5.13296 9.30469 4.71875V4.25ZM7.86777 5.25H6.77344C6.35922 5.25 6.02344 5.58579 6.02344 6V19.75C6.02344 20.1642 6.35922 20.5 6.77344 20.5H17.2734C17.6877 20.5 18.0234 20.1642 18.0234 19.75L18.0234 6C18.0234 5.58579 17.6876 5.25 17.2734 5.25H16.1791C15.9404 6.23626 15.0518 6.96875 13.9922 6.96875H10.0547C8.99506 6.96875 8.10651 6.23626 7.86777 5.25Z", fill: l }) });
var Yd = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M6.38687 9.98359C6.45685 6.94319 8.94344 4.5 12.0007 4.5C14.4558 4.5 16.5415 6.07501 17.3051 8.26898C19.9715 8.72575 22.0001 11.0476 22.0001 13.8439C22.0001 16.9678 19.4676 19.5003 16.3437 19.5003H6.76578C4.13371 19.5003 2 17.3666 2 14.7345C2 12.23 3.93195 10.1767 6.38687 9.98359ZM12.0007 6C9.72784 6 7.88536 7.84248 7.88536 10.1153V10.7188C7.88536 11.133 7.54957 11.4688 7.13536 11.4688H6.76578C4.96214 11.4688 3.5 12.9309 3.5 14.7345C3.5 16.5382 4.96214 18.0003 6.76578 18.0003H16.3437C18.6392 18.0003 20.5001 16.1394 20.5001 13.8439C20.5001 11.6577 18.8118 9.86517 16.6684 9.69996C16.3454 9.67506 16.0748 9.44553 15.9977 9.13086C15.5566 7.33307 13.9334 6 12.0007 6Z", fill: l }) });
var yh = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M6.38688 7.48329C6.45702 4.44302 8.94354 2 12.0007 2C15.0578 2 17.5444 4.44308 17.6144 7.48338C20.0688 7.67706 22.0001 9.7301 22.0001 12.2342C22.0001 12.2685 21.9997 12.3027 21.999 12.3368C21.4783 11.685 20.8414 11.13 20.1197 10.7032C19.571 9.67117 18.4847 8.96844 17.2343 8.96844H16.866C16.4518 8.96844 16.116 8.63266 16.116 8.21844V7.6153C16.116 5.34248 14.2735 3.5 12.0007 3.5C9.72784 3.5 7.88536 5.34248 7.88536 7.6153V8.21844C7.88536 8.63266 7.54957 8.96844 7.13536 8.96844H6.76578C4.96214 8.96844 3.5 10.4306 3.5 12.2342C3.5 14.0379 4.96214 15.5 6.76577 15.5H9.84159C9.78134 15.8661 9.75 16.2419 9.75 16.625C9.75 16.7508 9.75338 16.8758 9.76005 17H6.76577C4.13371 17 2 14.8663 2 12.2342C2 9.72969 3.93195 7.67638 6.38688 7.48329Z", fill: e }), C("path", { d: "M12.0223 15.7449C11.6331 15.603 11.4327 15.1725 11.5746 14.7834C11.8569 14.009 12.3151 13.306 12.9178 12.732C13.7399 11.9491 14.7887 11.4466 15.9139 11.2964C17.0392 11.1463 18.1831 11.3562 19.1817 11.8961C20.1659 12.4282 20.9592 13.254 21.4515 14.2579C21.5044 14.3525 21.5375 14.4597 21.5449 14.5739C21.5656 14.8717 21.4062 15.1656 21.1206 15.3024C20.9956 15.3624 20.8622 15.3845 20.7334 15.3735H19.0637C18.6495 15.3735 18.3137 15.0378 18.3137 14.6235C18.3137 14.2093 18.6495 13.8735 19.0637 13.8735H19.3547C19.0937 13.6146 18.7956 13.3926 18.4683 13.2156C17.7483 12.8263 16.9236 12.675 16.1123 12.7832C15.3011 12.8915 14.5449 13.2538 13.9522 13.8183C13.5177 14.2321 13.1874 14.7389 12.9838 15.2972C12.8419 15.6864 12.4114 15.8868 12.0223 15.7449Z", fill: e }), C("path", { d: "M14.781 20.0344C14.452 19.8564 14.1525 19.633 13.8905 19.3723H14.1851C14.5993 19.3723 14.9351 19.0365 14.9351 18.6223C14.9351 18.2081 14.5993 17.8723 14.1851 17.8723H12.4528C12.055 17.8723 11.7295 18.182 11.7043 18.5735C11.6958 18.6983 11.7185 18.827 11.7764 18.9479C12.267 19.9717 13.069 20.8139 14.0676 21.3538C15.0662 21.8938 16.2101 22.1037 17.3354 21.9535C18.4606 21.8034 19.5095 21.3008 20.3316 20.5179C20.9349 19.9433 21.3934 19.2394 21.6756 18.4641C21.8173 18.0749 21.6167 17.6445 21.2275 17.5028C20.8382 17.3611 20.4078 17.5617 20.2661 17.9509C20.0626 18.5099 19.7321 19.0174 19.2971 19.4317C18.7044 19.9961 17.9482 20.3584 17.137 20.4667C16.3257 20.575 15.501 20.4236 14.781 20.0344Z", fill: e })] });
var IL = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M3.9104 6.27879C2.71011 6.60041 1.99779 7.83416 2.31941 9.03446L4.75231 18.1016V18.4998C4.75231 19.7424 5.75967 20.7498 7.00231 20.7498H20.0023C21.2449 20.7498 22.2523 19.7424 22.2523 18.4998V9.82408C22.2523 8.59588 21.2682 7.59753 20.0455 7.57448L19.2231 4.50513C18.9015 3.30483 17.6677 2.59252 16.4674 2.91414L3.9104 6.27879ZM20.7523 10.8172V9.82408C20.7523 9.40986 20.4165 9.07408 20.0023 9.07408H7.00231C6.58809 9.07408 6.25231 9.40986 6.25231 9.82408V10.8172H20.7523ZM4.75231 12.3186V9.82408C4.75231 8.58143 5.75967 7.57408 7.00231 7.57408H18.4925L17.7742 4.89336C17.667 4.49326 17.2558 4.25582 16.8557 4.36303L4.29863 7.72768C3.89853 7.83488 3.6611 8.24613 3.7683 8.64623L4.75231 12.3186ZM6.25231 13.6145H20.7523V18.4998C20.7523 18.914 20.4165 19.2498 20.0023 19.2498H7.00231C6.5881 19.2498 6.25231 18.914 6.25231 18.4998V13.6145Z", fill: l }) });
var Ln = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.60352 3.25C4.36088 3.25 3.35352 4.25736 3.35352 5.5V8.99998C3.35352 10.2426 4.36087 11.25 5.60352 11.25H9.10352C10.3462 11.25 11.3535 10.2426 11.3535 8.99998V5.5C11.3535 4.25736 10.3462 3.25 9.10352 3.25H5.60352ZM4.85352 5.5C4.85352 5.08579 5.1893 4.75 5.60352 4.75H9.10352C9.51773 4.75 9.85352 5.08579 9.85352 5.5V8.99998C9.85352 9.41419 9.51773 9.74998 9.10352 9.74998H5.60352C5.1893 9.74998 4.85352 9.41419 4.85352 8.99998V5.5Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.60352 12.75C4.36088 12.75 3.35352 13.7574 3.35352 15V18.5C3.35352 19.7426 4.36087 20.75 5.60352 20.75H9.10352C10.3462 20.75 11.3535 19.7426 11.3535 18.5V15C11.3535 13.7574 10.3462 12.75 9.10352 12.75H5.60352ZM4.85352 15C4.85352 14.5858 5.1893 14.25 5.60352 14.25H9.10352C9.51773 14.25 9.85352 14.5858 9.85352 15V18.5C9.85352 18.9142 9.51773 19.25 9.10352 19.25H5.60352C5.1893 19.25 4.85352 18.9142 4.85352 18.5V15Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.8535 5.5C12.8535 4.25736 13.8609 3.25 15.1035 3.25H18.6035C19.8462 3.25 20.8535 4.25736 20.8535 5.5V8.99998C20.8535 10.2426 19.8462 11.25 18.6035 11.25H15.1035C13.8609 11.25 12.8535 10.2426 12.8535 8.99998V5.5ZM15.1035 4.75C14.6893 4.75 14.3535 5.08579 14.3535 5.5V8.99998C14.3535 9.41419 14.6893 9.74998 15.1035 9.74998H18.6035C19.0177 9.74998 19.3535 9.41419 19.3535 8.99998V5.5C19.3535 5.08579 19.0177 4.75 18.6035 4.75H15.1035Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M15.1035 12.75C13.8609 12.75 12.8535 13.7574 12.8535 15V18.5C12.8535 19.7426 13.8609 20.75 15.1035 20.75H18.6035C19.8462 20.75 20.8535 19.7426 20.8535 18.5V15C20.8535 13.7574 19.8462 12.75 18.6035 12.75H15.1035ZM14.3535 15C14.3535 14.5858 14.6893 14.25 15.1035 14.25H18.6035C19.0177 14.25 19.3535 14.5858 19.3535 15V18.5C19.3535 18.9142 19.0177 19.25 18.6035 19.25H15.1035C14.6893 19.25 14.3535 18.9142 14.3535 18.5V15Z", fill: e })] });
var gn = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.28636 3.71264C4.853 4.09212 4.5 4.60958 4.5 5.25V18.75C4.5 19.3904 4.853 19.9079 5.28636 20.2874C5.7212 20.6681 6.30718 20.9769 6.96654 21.2211C8.29107 21.7116 10.0708 22 12 22C13.9292 22 15.7089 21.7116 17.0335 21.2211C17.6928 20.9769 18.2788 20.6681 18.7136 20.2874C19.147 19.9079 19.5 19.3904 19.5 18.75V5.25C19.5 4.60958 19.147 4.09212 18.7136 3.71264C18.2788 3.33187 17.6928 3.02313 17.0335 2.77892C15.7089 2.28836 13.9292 2 12 2C10.0708 2 8.29107 2.28836 6.96654 2.77892C6.30718 3.02313 5.7212 3.33187 5.28636 3.71264ZM6.27454 4.84114C6.02476 5.05985 6 5.20007 6 5.25C6 5.29993 6.02476 5.44015 6.27454 5.65886C6.52284 5.87629 6.92537 6.10625 7.48751 6.31446C8.60601 6.72871 10.2013 7 12 7C13.7987 7 15.394 6.72871 16.5125 6.31446C17.0746 6.10625 17.4772 5.87629 17.7255 5.65886C17.9752 5.44015 18 5.29993 18 5.25C18 5.20007 17.9752 5.05985 17.7255 4.84114C17.4772 4.62371 17.0746 4.39375 16.5125 4.18554C15.394 3.77129 13.7987 3.5 12 3.5C10.2013 3.5 8.60601 3.77129 7.48751 4.18554C6.92537 4.39375 6.52284 4.62371 6.27454 4.84114ZM18 9.75V7.28202C17.7055 7.44688 17.3796 7.59287 17.0335 7.72108C15.7089 8.21164 13.9292 8.5 12 8.5C10.0708 8.5 8.29107 8.21164 6.96654 7.72108C6.62039 7.59287 6.29445 7.44688 6 7.28202V9.75C6 9.79993 6.02476 9.94015 6.27454 10.1589C6.52284 10.3763 6.92537 10.6063 7.48751 10.8145C8.60601 11.2287 10.2013 11.5 12 11.5C13.7987 11.5 15.394 11.2287 16.5125 10.8145C17.0746 10.6063 17.4772 10.3763 17.7255 10.1589C17.9752 9.94015 18 9.79993 18 9.75ZM6 11.782C6.29445 11.9469 6.62039 12.0929 6.96654 12.2211C8.29107 12.7116 10.0708 13 12 13C13.9292 13 15.7089 12.7116 17.0335 12.2211C17.3796 12.0929 17.7055 11.9469 18 11.782V14.25C18 14.2999 17.9752 14.4402 17.7255 14.6589C17.4772 14.8763 17.0746 15.1063 16.5125 15.3145C15.394 15.7287 13.7987 16 12 16C10.2013 16 8.60601 15.7287 7.48751 15.3145C6.92537 15.1063 6.52284 14.8763 6.27454 14.6589C6.02476 14.4402 6 14.2999 6 14.25V11.782ZM6 18.75V16.282C6.29445 16.4469 6.62039 16.5929 6.96654 16.7211C8.29107 17.2116 10.0708 17.5 12 17.5C13.9292 17.5 15.7089 17.2116 17.0335 16.7211C17.3796 16.5929 17.7055 16.4469 18 16.282V18.75C18 18.7999 17.9752 18.9401 17.7255 19.1589C17.4772 19.3763 17.0746 19.6063 16.5125 19.8145C15.394 20.2287 13.7987 20.5 12 20.5C10.2013 20.5 8.60601 20.2287 7.48751 19.8145C6.92537 19.6063 6.52284 19.3763 6.27454 19.1589C6.02476 18.9401 6 18.7999 6 18.75Z", fill: l }) });
var Kn = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M12.25 6.625C12.6642 6.625 13 6.96079 13 7.375V7.81278C13.9908 7.96341 14.75 8.81897 14.75 9.85184C14.75 10.2661 14.4142 10.6018 14 10.6018C13.5858 10.6018 13.25 10.2661 13.25 9.85184C13.25 9.54113 12.9981 9.28924 12.6874 9.28924H12C11.5858 9.28924 11.25 9.62503 11.25 10.0392V10.3043C11.25 10.6169 11.4439 10.8968 11.7366 11.0066L13.2901 11.5891C14.1682 11.9185 14.75 12.758 14.75 13.6959V13.9609C14.75 15.0317 14.002 15.9278 13 16.1552V16.625C13 17.0392 12.6642 17.375 12.25 17.375C11.8358 17.375 11.5 17.0392 11.5 16.625V16.1874C10.5092 16.0368 9.75 15.1812 9.75 14.1483C9.75 13.7341 10.0858 13.3983 10.5 13.3983C10.9142 13.3983 11.25 13.7341 11.25 14.1483C11.25 14.4591 11.5019 14.7109 11.8126 14.7109H12.5C12.9142 14.7109 13.25 14.3752 13.25 13.9609V13.6959C13.25 13.3832 13.0561 13.1034 12.7634 12.9936L11.2099 12.411C10.3318 12.0817 9.75 11.2422 9.75 10.3043V10.0392C9.75 8.96845 10.498 8.07236 11.5 7.845V7.375C11.5 6.96079 11.8358 6.625 12.25 6.625Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.25 2C6.72715 2 2.25 6.47715 2.25 12C2.25 17.5228 6.72715 22 12.25 22C17.7728 22 22.25 17.5228 22.25 12C22.25 6.47715 17.7728 2 12.25 2ZM3.75 12C3.75 7.30558 7.55558 3.5 12.25 3.5C16.9444 3.5 20.75 7.30558 20.75 12C20.75 16.6944 16.9444 20.5 12.25 20.5C7.55558 20.5 3.75 16.6944 3.75 12Z", fill: e })] });
var ks = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M22 6.25649V17.25C22 18.4926 20.9926 19.5 19.75 19.5H4.25C3.00736 19.5 2 18.4926 2 17.25V6.23398C2 6.22372 2.00021 6.2135 2.00061 6.20334C2.01781 5.25972 2.78812 4.5 3.73592 4.5H20.2644C21.2229 4.5 22 5.27697 22.0001 6.23549C22.0001 6.24249 22.0001 6.24949 22 6.25649ZM3.5 8.187V17.25C3.5 17.6642 3.83579 18 4.25 18H19.75C20.1642 18 20.5 17.6642 20.5 17.25V8.18747L13.2873 13.2171C12.5141 13.7563 11.4866 13.7563 10.7134 13.2171L3.5 8.187ZM20.5 6.2286L20.5 6.23398V6.24336C20.4976 6.31753 20.4604 6.38643 20.3992 6.42905L12.4293 11.9867C12.1716 12.1664 11.8291 12.1664 11.5713 11.9867L3.60116 6.42885C3.538 6.38481 3.50035 6.31268 3.50032 6.23568C3.50028 6.10553 3.60577 6 3.73592 6H20.2644C20.3922 6 20.4963 6.10171 20.5 6.2286Z", fill: l }) });
var Us = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 25", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M11.5781 2.5C10.3355 2.5 9.32812 3.50736 9.32812 4.75V6.6285C9.44877 6.70925 9.56333 6.80292 9.66985 6.90952L10.8281 8.06853V4.75C10.8281 4.33579 11.1639 4 11.5781 4H17.5781C17.9923 4 18.3281 4.33579 18.3281 4.75V20.25C18.3281 20.6642 17.9923 21 17.5781 21H11.5781C11.1639 21 10.8281 20.6642 10.8281 20.25V16.9314L9.6699 18.0904C9.56336 18.197 9.44879 18.2907 9.32812 18.3715V20.25C9.32812 21.4926 10.3355 22.5 11.5781 22.5H17.5781C18.8208 22.5 19.8281 21.4926 19.8281 20.25V4.75C19.8281 3.50736 18.8208 2.5 17.5781 2.5H11.5781Z", fill: e }), C("path", { d: "M3.57812 12.5C3.57812 12.7259 3.67796 12.9284 3.83591 13.0659L7.79738 17.0301C8.09017 17.3231 8.56504 17.3233 8.85804 17.0305C9.15104 16.7377 9.1512 16.2629 8.85841 15.9699L6.14046 13.25L12.0781 13.25C12.4923 13.25 12.8281 12.9142 12.8281 12.5C12.8281 12.0858 12.4923 11.75 12.0781 11.75L6.14028 11.75L8.85839 9.03016C9.15119 8.73718 9.15104 8.2623 8.85806 7.9695C8.56507 7.6767 8.0902 7.67685 7.7974 7.96984L3.83388 11.9359C3.67711 12.0733 3.57812 12.2751 3.57812 12.5Z", fill: e })] });
var DH = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M10.9453 20.5C11.3327 21.0667 11.8027 21.5725 12.338 22H6.75C5.50736 22 4.5 20.9926 4.5 19.75V9.62105C4.5 9.02455 4.73686 8.45247 5.15851 8.03055L10.5262 2.65951C10.9482 2.23725 11.5207 2 12.1177 2H17.25C18.4926 2 19.5 3.00736 19.5 4.25V10.3782C19.0266 10.1599 18.5241 9.99391 18 9.88753V4.25C18 3.83579 17.6642 3.5 17.25 3.5H12.248L12.2509 7.4984C12.2518 8.74166 11.2442 9.75 10.0009 9.75H6V19.75C6 20.1642 6.33579 20.5 6.75 20.5H10.9453ZM10.7488 4.55876L7.05986 8.25H10.0009C10.4153 8.25 10.7512 7.91389 10.7509 7.49947L10.7488 4.55876Z", fill: e }), C("path", { d: "M15.8751 14.9999C15.8751 14.5857 16.2109 14.2499 16.6251 14.2499C17.0393 14.2499 17.3751 14.5857 17.3751 14.9999V15.8749H18.2502C18.6644 15.8749 19.0002 16.2107 19.0002 16.6249C19.0002 17.0391 18.6644 17.3749 18.2502 17.3749H17.3751V18.25C17.3751 18.6643 17.0393 19 16.6251 19C16.2109 19 15.8751 18.6643 15.8751 18.25V17.3749H15C14.5858 17.3749 14.25 17.0391 14.25 16.6249C14.25 16.2107 14.5858 15.8749 15 15.8749H15.8751V14.9999Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.25 16.625C11.25 13.6565 13.6565 11.25 16.625 11.25C19.5935 11.25 22 13.6565 22 16.625C22 19.5935 19.5935 22 16.625 22C13.6565 22 11.25 19.5935 11.25 16.625ZM16.625 12.75C14.4849 12.75 12.75 14.4849 12.75 16.625C12.75 18.7651 14.4849 20.5 16.625 20.5C18.7651 20.5 20.5 18.7651 20.5 16.625C20.5 14.4849 18.7651 12.75 16.625 12.75Z", fill: e })] });
var kg = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M2 6C2 4.75736 3.00736 3.75 4.25 3.75H8.5C9.2082 3.75 9.87508 4.08344 10.3 4.65L11.65 6.45C11.7916 6.63885 12.0139 6.75 12.25 6.75H19.75C20.9926 6.75 22 7.75736 22 9V18C22 19.2426 20.9926 20.25 19.75 20.25H4.25C3.00736 20.25 2 19.2426 2 18V6ZM4.25 5.25C3.83579 5.25 3.5 5.58579 3.5 6V18C3.5 18.4142 3.83579 18.75 4.25 18.75H19.75C20.1642 18.75 20.5 18.4142 20.5 18V9C20.5 8.58579 20.1642 8.25 19.75 8.25H12.25C11.5418 8.25 10.8749 7.91656 10.45 7.35L9.1 5.55C8.95836 5.36115 8.73607 5.25 8.5 5.25H4.25Z", fill: l }) });
var _g = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 25", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.9992 8.7743C9.88118 8.7743 8.16419 10.4913 8.16419 12.6093C8.16419 14.7273 9.88118 16.4443 11.9992 16.4443C14.1172 16.4443 15.8342 14.7273 15.8342 12.6093C15.8342 10.4913 14.1172 8.7743 11.9992 8.7743ZM9.66419 12.6093C9.66419 11.3197 10.7096 10.2743 11.9992 10.2743C13.2888 10.2743 14.3342 11.3197 14.3342 12.6093C14.3342 13.8989 13.2888 14.9443 11.9992 14.9443C10.7096 14.9443 9.66419 13.8989 9.66419 12.6093Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M2.5809 8.9224C1.96404 9.99083 2.33012 11.357 3.39854 11.9739C3.88777 12.2563 3.88776 12.9625 3.39856 13.2449C2.33013 13.8618 1.96407 15.2279 2.58092 16.2964L4.09692 18.9222C4.71391 19.9908 6.08044 20.3568 7.14896 19.7399C7.63844 19.4573 8.25011 19.8106 8.25011 20.3754C8.25011 21.6092 9.2503 22.6094 10.4841 22.6094H13.5165C14.7502 22.6094 15.7501 21.6091 15.7501 20.3756C15.7501 19.8108 16.3615 19.458 16.8503 19.7402C17.9185 20.357 19.2845 19.991 19.9012 18.9227L21.4176 16.2963C22.0344 15.2279 21.6684 13.8617 20.6 13.2449C20.1108 12.9624 20.1108 12.2563 20.6 11.9739C21.6684 11.3571 22.0345 9.99089 21.4176 8.92247L19.9012 6.29604C19.2845 5.2278 17.9185 4.86179 16.8503 5.47854C16.3615 5.76076 15.7501 5.40794 15.7501 4.84314C15.7501 3.60961 14.7502 2.60938 13.5165 2.60938H10.4841C9.2503 2.60938 8.25011 3.60957 8.25011 4.84337C8.25011 5.40822 7.63842 5.76152 7.14894 5.47892C6.08042 4.86201 4.71388 5.22797 4.09689 6.29663L2.5809 8.9224ZM4.14854 10.6748C3.79755 10.4722 3.6773 10.0234 3.87994 9.6724L5.39593 7.04663C5.59863 6.69554 6.04772 6.57518 6.39894 6.77796C7.88811 7.63773 9.75011 6.56327 9.75011 4.84337C9.75011 4.43799 10.0787 4.10937 10.4841 4.10937L13.5165 4.10937C13.9216 4.10937 14.2501 4.43788 14.2501 4.84314C14.2501 6.56227 16.1112 7.63733 17.6003 6.77758C17.9511 6.57504 18.3997 6.69524 18.6022 7.04604L20.1186 9.67247C20.3212 10.0234 20.201 10.4722 19.85 10.6749C18.3608 11.5346 18.3608 13.6841 19.85 14.5439C20.2009 14.7465 20.3212 15.1953 20.1186 15.5463L18.6022 18.1727C18.3996 18.5235 17.9511 18.6437 17.6003 18.4412C16.1112 17.5815 14.2501 18.6565 14.2501 20.3756C14.2501 20.7809 13.9216 21.1094 13.5165 21.1094H10.4841C10.0787 21.1094 9.75011 20.7808 9.75011 20.3754C9.75011 18.6555 7.88812 17.5811 6.39896 18.4408C6.04774 18.6436 5.59866 18.5232 5.39596 18.1722L3.87996 15.5464C3.67732 15.1954 3.79757 14.7466 4.14856 14.5439C5.63778 13.6841 5.63775 11.5346 4.14854 10.6748Z", fill: e })] });
var tM = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 25", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M4.25315 5.39728C5.24418 5.9627 6.47801 5.25034 6.48385 4.10938L7.98373 4.10938C7.98956 5.25038 9.22343 5.96275 10.2145 5.3973L10.9635 6.6946C11.3812 6.46162 11.8798 6.30786 12.3876 6.21697C12.3568 6.12702 12.3169 6.03871 12.2675 5.95326L11.5095 4.64028C11.0998 3.93076 10.1954 3.68503 9.48364 4.0873C9.47615 3.26978 8.81111 2.60938 7.99183 2.60938H6.47573C5.65647 2.60938 4.99145 3.26976 4.98393 4.08724C4.27222 3.68501 3.3678 3.93074 2.95817 4.64024L2.20012 5.95323C1.79048 6.66275 2.0299 7.56889 2.73413 7.98413C2.0299 8.39938 1.7905 9.30551 2.20013 10.015L2.95819 11.328C3.36782 12.0375 4.27222 12.2832 4.98394 11.881C4.98962 12.4994 5.37154 13.0279 5.91185 13.2486C5.909 12.6779 6.05286 12.0988 6.35883 11.5689L6.4266 11.4515C6.17233 10.5593 5.11989 10.0765 4.25316 10.571L3.50323 9.27204C4.48844 8.69649 4.48842 7.27175 3.50321 6.69621L4.25315 5.39728Z", fill: e }), C("path", { d: "M7.23401 9.48438C8.06244 9.48438 8.73401 8.8128 8.73401 7.98438C8.73401 7.15595 8.06244 6.48438 7.23401 6.48438C6.40558 6.48438 5.73401 7.15595 5.73401 7.98438C5.73401 8.8128 6.40558 9.48438 7.23401 9.48438Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.6406 15.1107C11.6406 13.418 13.0129 12.0457 14.7056 12.0457C16.3984 12.0457 17.7706 13.418 17.7706 15.1107C17.7706 16.8035 16.3984 18.1757 14.7056 18.1757C13.0129 18.1757 11.6406 16.8035 11.6406 15.1107ZM14.7056 13.5457C13.8413 13.5457 13.1406 14.2464 13.1406 15.1107C13.1406 15.975 13.8413 16.6757 14.7056 16.6757C15.57 16.6757 16.2706 15.975 16.2706 15.1107C16.2706 14.2464 15.57 13.5457 14.7056 13.5457Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M7.65797 17.9026C7.15164 17.0256 7.45257 15.9045 8.32916 15.3984C8.55144 15.27 8.55094 14.9496 8.32953 14.8218C7.45293 14.3157 7.15258 13.1948 7.65869 12.3182L8.76495 10.4021C9.27102 9.52553 10.3919 9.22535 11.2683 9.73135C11.49 9.85937 11.767 9.69928 11.767 9.44355C11.767 8.43154 12.5874 7.61115 13.5994 7.61115H15.8121C16.8245 7.61115 17.6448 8.43199 17.6448 9.44397C17.6448 9.7002 17.9221 9.85987 18.1434 9.73212C19.0198 9.22613 20.1404 9.52641 20.6464 10.4028L21.7531 12.3196C22.259 13.1959 21.9588 14.3164 21.0825 14.8223C20.8615 14.9499 20.861 15.2697 21.0829 15.3978C21.9591 15.9037 22.26 17.0245 21.7538 17.9012L20.6475 19.8173C20.1414 20.6939 19.0205 20.9942 18.1439 20.4881C17.9222 20.3601 17.6448 20.5201 17.6448 20.7764C17.6448 21.7886 16.8243 22.6094 15.8119 22.6094H13.5996C12.5875 22.6094 11.767 21.7889 11.767 20.7768C11.767 20.5211 11.4899 20.3606 11.2678 20.4888C10.3913 20.9949 9.27006 20.6948 8.76384 19.818L7.65797 17.9026ZM9.07916 16.6974C8.91962 16.7895 8.86512 16.9935 8.957 17.1526L10.0629 19.068C10.1547 19.2271 10.3584 19.2818 10.5178 19.1898C11.7392 18.4846 13.267 19.3656 13.267 20.7768C13.267 20.9605 13.4159 21.1094 13.5996 21.1094H15.8119C15.9957 21.1094 16.1448 20.9603 16.1448 20.7764C16.1448 19.3657 17.672 18.4836 18.8939 19.1891C19.0531 19.281 19.2566 19.2264 19.3485 19.0673L20.4548 17.1512C20.5465 16.9923 20.4921 16.7888 20.3329 16.6968C19.1119 15.9919 19.1106 14.2287 20.3325 13.5233C20.4913 13.4316 20.5458 13.2285 20.4541 13.0696L19.3474 11.1528C19.2556 10.9938 19.0523 10.9394 18.8934 11.0312C17.6715 11.7366 16.1448 10.8542 16.1448 9.44397C16.1448 9.2601 15.9957 9.11114 15.8121 9.11114L13.5994 9.11115C13.4158 9.11115 13.267 9.25997 13.267 9.44355C13.267 10.8543 11.7397 11.7356 10.5183 11.0304C10.3592 10.9385 10.1558 10.9931 10.064 11.1521L8.95773 13.0682C8.86583 13.2273 8.92037 13.4308 9.07953 13.5227C10.3018 14.2284 10.3006 15.9922 9.07916 16.6974Z", fill: e })] });
var hp = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M9.59859 3.49501C9.90762 2.65448 10.6463 2 11.616 2C12.5858 2 13.3245 2.65449 13.6335 3.49502C13.8851 3.38177 14.1652 3.31802 14.4678 3.31802C15.7613 3.31802 16.6437 4.48247 16.6437 5.69968V6.13703C16.8528 6.06365 17.0791 6.02338 17.3196 6.02338C18.6131 6.02338 19.4956 7.18783 19.4956 8.40504V14.1096C19.5135 14.8186 19.5172 16.7392 18.6678 18.5107C18.2348 19.4137 17.573 20.2935 16.5699 20.9459C15.565 21.5996 14.2658 21.9935 12.6103 22L12.6074 22C10.7934 22 9.18343 21.1137 8.00738 20.2156C6.81983 19.3087 5.98549 18.3213 5.70771 17.9747C5.64971 17.9023 5.59912 17.8417 5.54755 17.7856L3.20116 15.2304L3.18883 15.2167C2.38497 14.2968 2.38497 12.842 3.18883 11.9221C4.03934 10.9489 5.46177 10.943 6.31921 11.9044L6.58834 12.1788V5.69968C6.58834 4.48247 7.47077 3.31802 8.76424 3.31802C9.06687 3.31802 9.347 3.38177 9.59859 3.49501ZM9.44013 5.69273V11.0417C9.44013 11.4559 9.77592 11.7917 10.1901 11.7917C10.6043 11.7917 10.9401 11.4559 10.9401 11.0417L10.9402 5.69968C10.9402 5.69646 10.9401 5.69323 10.9401 5.69V4.38166C10.9401 3.79659 11.3345 3.5 11.616 3.5C11.8976 3.5 12.2919 3.79659 12.2919 4.38166V11.2299C12.2919 11.6441 12.6277 11.9799 13.0419 11.9799C13.4562 11.9799 13.7919 11.6441 13.7919 11.2299V5.69968C13.7919 5.11461 14.1863 4.81802 14.4678 4.81802C14.7494 4.81802 15.1437 5.11461 15.1437 5.69968V11.2299C15.1437 11.6441 15.4795 11.9799 15.8937 11.9799C16.308 11.9799 16.6437 11.6441 16.6437 11.2299V8.40504C16.6437 7.81997 17.0381 7.52338 17.3196 7.52338C17.6012 7.52338 17.9956 7.81997 17.9956 8.40504L17.9956 14.1192L17.9958 14.1386C18.013 14.7999 18.0053 16.4231 17.3152 17.8622C16.9765 18.5686 16.4808 19.2145 15.7521 19.6885C15.0254 20.1612 14.0188 20.4942 12.6059 20.5C11.2702 20.4995 9.98186 19.8361 8.91777 19.0235C7.86479 18.2194 7.11701 17.3346 6.87825 17.0367C6.81079 16.9525 6.73649 16.8626 6.65237 16.771L4.31323 14.2238C4.00849 13.8684 4.01018 13.2618 4.31832 12.9092C4.57632 12.6139 4.94736 12.6139 5.20537 12.9092C5.21482 12.92 5.22459 12.9305 5.23465 12.9408L6.80288 14.5398C7.01621 14.7573 7.33994 14.8242 7.62198 14.7089C7.90403 14.5937 8.08834 14.3193 8.08834 14.0146V5.69968C8.08834 5.11461 8.48271 4.81802 8.76424 4.81802C9.04466 4.81802 9.43702 5.11226 9.44013 5.69273Z", fill: l }) });
var Ap = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.8227 4.77124L12 4.94862L12.1773 4.77135C14.4244 2.52427 18.0676 2.52427 20.3147 4.77134C22.5618 7.01842 22.5618 10.6616 20.3147 12.9087L13.591 19.6324C12.7123 20.5111 11.2877 20.5111 10.409 19.6324L3.6853 12.9086C1.43823 10.6615 1.43823 7.01831 3.6853 4.77124C5.93237 2.52417 9.5756 2.52417 11.8227 4.77124ZM10.762 5.8319C9.10073 4.17062 6.40725 4.17062 4.74596 5.8319C3.08468 7.49319 3.08468 10.1867 4.74596 11.848L11.4697 18.5718C11.7625 18.8647 12.2374 18.8647 12.5303 18.5718L19.254 11.8481C20.9153 10.1868 20.9153 7.49329 19.254 5.83201C17.5927 4.17072 14.8993 4.17072 13.238 5.83201L12.5304 6.53961C12.3897 6.68026 12.199 6.75928 12 6.75928C11.8011 6.75928 11.6104 6.68026 11.4697 6.53961L10.762 5.8319Z", fill: l }) });
var Mc = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.45 4.90342C12.1833 4.70342 11.8167 4.70342 11.55 4.90342L5.05 9.77842C4.86115 9.92006 4.75 10.1423 4.75 10.3784V18.4998C4.75 18.9141 5.08579 19.2498 5.5 19.2498H9V16.9998C9 15.343 10.3431 13.9998 12 13.9998C13.6569 13.9998 15 15.343 15 16.9998V19.2498H18.5C18.9142 19.2498 19.25 18.9141 19.25 18.4998V10.3784C19.25 10.1423 19.1389 9.92006 18.95 9.77842L12.45 4.90342ZM10.65 3.70342C11.45 3.10342 12.55 3.10342 13.35 3.70342L19.85 8.57842C20.4166 9.00334 20.75 9.67021 20.75 10.3784V18.4998C20.75 19.7425 19.7426 20.7498 18.5 20.7498H14.25C13.8358 20.7498 13.5 20.4141 13.5 19.9998V16.9998C13.5 16.1714 12.8284 15.4998 12 15.4998C11.1716 15.4998 10.5 16.1714 10.5 16.9998V19.9998C10.5 20.4141 10.1642 20.7498 9.75 20.7498H5.5C4.25736 20.7498 3.25 19.7425 3.25 18.4998V10.3784C3.25 9.67021 3.58344 9.00334 4.15 8.57842L10.65 3.70342Z", fill: l }) });
var au = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M12 8.24961C12.4142 8.24961 12.75 8.5854 12.75 8.99961V9.43739C13.7408 9.58802 14.5 10.4436 14.5 11.4765C14.5 11.8907 14.1642 12.2265 13.75 12.2265C13.3358 12.2265 13 11.8907 13 11.4765C13 11.1657 12.7481 10.9139 12.4374 10.9139H11.75C11.3358 10.9139 11 11.2496 11 11.6639V11.9289C11 12.2416 11.1939 12.5214 11.4866 12.6312L13.0401 13.2138C13.9182 13.5431 14.5 14.3826 14.5 15.3205V15.5855C14.5 16.6563 13.752 17.5524 12.75 17.7798V18.2496C12.75 18.6638 12.4142 18.9996 12 18.9996C11.5858 18.9996 11.25 18.6638 11.25 18.2496V17.812C10.2592 17.6614 9.5 16.8058 9.5 15.7729C9.5 15.3587 9.83579 15.0229 10.25 15.0229C10.6642 15.0229 11 15.3587 11 15.7729C11 16.0837 11.2519 16.3355 11.5626 16.3355H12.25C12.6642 16.3355 13 15.9998 13 15.5855V15.3205C13 15.0078 12.8061 14.728 12.5134 14.6182L10.9599 14.0356C10.0818 13.7063 9.5 12.8668 9.5 11.9289V11.6639C9.5 10.5931 10.248 9.69697 11.25 9.46961V8.99961C11.25 8.5854 11.5858 8.24961 12 8.24961Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M13.0927 3.01432C13.0296 2.97929 12.9653 2.94752 12.9 2.91899V1.99961C12.9 1.50255 12.4971 1.09961 12 1.09961C11.5029 1.09961 11.1 1.50255 11.1 1.99961V2.91899C11.0347 2.94752 10.9704 2.97929 10.9073 3.01432L5.6573 5.93099C4.94301 6.32782 4.5 7.08071 4.5 7.89784V19.7496C4.5 20.9923 5.50736 21.9996 6.75 21.9996H17.25C18.4926 21.9996 19.5 20.9923 19.5 19.7496V7.89784C19.5 7.08071 19.057 6.32782 18.3427 5.93099L13.0927 3.01432ZM12 6.64961C12.4971 6.64961 12.9 6.24667 12.9 5.74961V4.6232L17.6142 7.24222C17.8523 7.3745 18 7.62546 18 7.89784V19.7496C18 20.1638 17.6642 20.4996 17.25 20.4996H6.75C6.33579 20.4996 6 20.1638 6 19.7496V7.89784C6 7.62546 6.14767 7.3745 6.38577 7.24222L11.1 4.6232V5.74961C11.1 6.24667 11.5029 6.64961 12 6.64961Z", fill: e })] });
var Tu = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12.8609 4.96887C12.3192 4.7747 11.727 4.7747 11.1853 4.96887L2.91085 7.935C1.73804 8.35542 1.73806 10.014 2.91085 10.4344L11.1853 13.4006C11.727 13.5948 12.3192 13.5948 12.8609 13.4006L21.1354 10.4344C22.3082 10.014 22.3082 8.35542 21.1354 7.93501L12.8609 4.96887ZM11.6915 6.38089C11.9059 6.30403 12.1403 6.30403 12.3547 6.38089L20.1764 9.18473L12.3547 11.9886C12.1403 12.0654 11.9059 12.0654 11.6915 11.9886L3.86977 9.18473L11.6915 6.38089Z", fill: e }), C("path", { d: "M2.91085 13.5646L5.05398 12.7964L7.27658 13.5931L3.86977 14.8144L11.6915 17.6182C11.9059 17.6951 12.1403 17.6951 12.3547 17.6182L20.1764 14.8144L16.7695 13.5931L18.9921 12.7964L21.1354 13.5646C22.3082 13.9851 22.3082 15.6437 21.1354 16.0641L12.8609 19.0302C12.3192 19.2244 11.727 19.2244 11.1853 19.0302L2.91085 16.0641C1.73806 15.6437 1.73804 13.9851 2.91085 13.5646Z", fill: e })] });
var Xu = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.52344 3.25C4.2808 3.25 3.27344 4.25736 3.27344 5.5V18.5C3.27344 19.7426 4.2808 20.75 5.52344 20.75H18.5234C19.7661 20.75 20.7734 19.7426 20.7734 18.5V5.5C20.7734 4.25736 19.7661 3.25 18.5234 3.25H5.52344ZM4.77344 5.5C4.77344 5.08579 5.10922 4.75 5.52344 4.75H18.5234C18.9377 4.75 19.2734 5.08579 19.2734 5.5V8.58301L4.77344 8.58301V5.5ZM4.77344 10.083V18.5C4.77344 18.9142 5.10922 19.25 5.52344 19.25H8.60645L8.60645 10.083H4.77344ZM10.1064 10.083V13.916H19.2734V10.083L10.1064 10.083ZM10.1064 15.416H19.2734V18.5C19.2734 18.9142 18.9377 19.25 18.5234 19.25H10.1064V15.416Z", fill: l }) });
var XV = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M10.5 16C10.5 15.1716 11.1716 14.5 12 14.5C12.8284 14.5 13.5 15.1716 13.5 16V17.5C13.5 18.3284 12.8284 19 12 19C11.1716 19 10.5 18.3284 10.5 17.5V16Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M6.75 7.25C6.75 4.3505 9.10051 2 12 2C14.8995 2 17.25 4.35051 17.25 7.25V9.125H18.5C19.7426 9.125 20.75 10.1324 20.75 11.375V17.2495C20.75 19.8729 18.6234 21.9995 16 21.9995H8C5.37665 21.9995 3.25 19.8729 3.25 17.2495V11.375C3.25 10.1324 4.25736 9.125 5.5 9.125H6.75V7.25ZM8.25 9.125H15.75V7.25C15.75 5.17893 14.0711 3.5 12 3.5C9.92893 3.5 8.25 5.17893 8.25 7.25V9.125ZM5.5 10.625C5.08579 10.625 4.75 10.9608 4.75 11.375V17.2495C4.75 19.0444 6.20507 20.4995 8 20.4995H16C17.7949 20.4995 19.25 19.0444 19.25 17.2495V11.375C19.25 10.9608 18.9142 10.625 18.5 10.625H5.5Z", fill: e })] });
var bZ = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M3.5625 6C3.5625 5.58579 3.89829 5.25 4.3125 5.25H20.3125C20.7267 5.25 21.0625 5.58579 21.0625 6C21.0625 6.41421 20.7267 6.75 20.3125 6.75L4.3125 6.75C3.89829 6.75 3.5625 6.41422 3.5625 6Z", fill: e }), C("path", { d: "M3.5625 18C3.5625 17.5858 3.89829 17.25 4.3125 17.25L20.3125 17.25C20.7267 17.25 21.0625 17.5858 21.0625 18C21.0625 18.4142 20.7267 18.75 20.3125 18.75L4.3125 18.75C3.89829 18.75 3.5625 18.4142 3.5625 18Z", fill: e }), C("path", { d: "M4.3125 11.25C3.89829 11.25 3.5625 11.5858 3.5625 12C3.5625 12.4142 3.89829 12.75 4.3125 12.75L20.3125 12.75C20.7267 12.75 21.0625 12.4142 21.0625 12C21.0625 11.5858 20.7267 11.25 20.3125 11.25L4.3125 11.25Z", fill: e })] });
var zv = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M8.55814 11.25C8.14393 11.25 7.80814 11.5858 7.80814 12C7.80814 12.4142 8.14393 12.75 8.55814 12.75H15.4419C15.8561 12.75 16.1919 12.4142 16.1919 12C16.1919 11.5858 15.8561 11.25 15.4419 11.25H8.55814Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12Z", fill: e })] });
var $a = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 25", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M20.5565 10.6338C21.0009 9.27575 20.8528 7.76958 20.1367 6.53501C19.0503 4.63378 16.8528 3.67081 14.7046 4.11526C13.7663 3.05353 12.3836 2.46094 10.9515 2.46094C8.75399 2.46094 6.82807 3.86835 6.13671 5.94242C4.7293 6.23872 3.51943 7.10291 2.80338 8.36217C1.71696 10.2634 1.96387 12.6338 3.42066 14.2634C2.97622 15.6461 3.14906 17.1276 3.8651 18.3622C4.95152 20.2634 7.14906 21.2511 9.2972 20.7819C10.2602 21.8437 11.6182 22.4609 13.0503 22.4609C15.2478 22.4609 17.1737 21.0535 17.8651 18.9795C19.2725 18.6832 20.4824 17.819 21.1984 16.5597C22.2849 14.6585 22.0379 12.2634 20.5565 10.6338ZM13.0503 21.1523C12.1614 21.1523 11.3219 20.856 10.6552 20.2881C10.6799 20.2634 10.754 20.2387 10.7787 20.214L14.754 17.9177C14.9515 17.7943 15.075 17.5967 15.075 17.3498V11.7449L16.754 12.7079C16.7787 12.7079 16.7787 12.7325 16.7787 12.7572V17.3992C16.8034 19.4733 15.1244 21.1523 13.0503 21.1523ZM5.00091 17.7202C4.55646 16.9548 4.40831 16.0659 4.55646 15.2017C4.58115 15.2264 4.63054 15.2511 4.67992 15.2758L8.65523 17.572C8.85276 17.6955 9.09967 17.6955 9.2972 17.572L14.1614 14.7572V16.7079C14.1614 16.7325 14.1614 16.7572 14.1367 16.7572L10.112 19.0782C8.33424 20.1153 6.03794 19.498 5.00091 17.7202ZM3.96387 9.02884C4.40831 8.26341 5.09967 7.69551 5.91449 7.37452V12.1153C5.91449 12.3375 6.03794 12.5597 6.23548 12.6832L11.0997 15.498L9.42066 16.4609C9.39597 16.4609 9.37128 16.4856 9.37128 16.4609L5.34659 14.1399C3.51943 13.1029 2.92683 10.8066 3.96387 9.02884ZM17.791 12.2387L12.9268 9.4239L14.6058 8.46094C14.6305 8.46094 14.6552 8.43625 14.6552 8.46094L18.6799 10.7819C20.4824 11.819 21.075 14.1153 20.0379 15.893C19.5935 16.6585 18.9021 17.2264 18.0873 17.5227V12.8066C18.112 12.5844 17.9886 12.3622 17.791 12.2387ZM19.4454 9.7202C19.4207 9.69551 19.3713 9.67081 19.3219 9.64612L15.3466 7.34983C15.1491 7.22637 14.9021 7.22637 14.7046 7.34983L9.84041 10.1646V8.21402C9.84041 8.18933 9.84041 8.16464 9.86511 8.16464L13.8898 5.84365C15.6923 4.80662 17.9639 5.4239 19.0009 7.22637C19.4454 7.96711 19.5935 8.856 19.4454 9.7202ZM8.92683 13.177L7.24782 12.214C7.22313 12.214 7.22313 12.1893 7.22313 12.1646V7.52267C7.22313 5.44859 8.90214 3.76958 10.9762 3.76958C11.8651 3.76958 12.7046 4.06588 13.3713 4.63378C13.3466 4.65847 13.2972 4.68316 13.2478 4.70785L9.27251 7.00415C9.07498 7.1276 8.95152 7.32514 8.95152 7.57205V13.177H8.92683ZM9.84041 11.2017L12.0133 9.94242L14.1861 11.2017V13.6955L12.0133 14.9548L9.84041 13.6955V11.2017Z", fill: l }) });
var tx = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M19.3028 3.7801C18.4241 2.90142 16.9995 2.90142 16.1208 3.7801L14.3498 5.5511C14.3442 5.55633 14.3387 5.56166 14.3333 5.5671C14.3279 5.57253 14.3225 5.57803 14.3173 5.58359L5.83373 14.0672C5.57259 14.3283 5.37974 14.6497 5.27221 15.003L4.05205 19.0121C3.9714 19.2771 4.04336 19.565 4.23922 19.7608C4.43508 19.9567 4.72294 20.0287 4.98792 19.948L8.99703 18.7279C9.35035 18.6203 9.67176 18.4275 9.93291 18.1663L20.22 7.87928C21.0986 7.0006 21.0986 5.57598 20.22 4.6973L19.3028 3.7801ZM14.8639 7.15833L6.89439 15.1278C6.80735 15.2149 6.74306 15.322 6.70722 15.4398L5.8965 18.1036L8.56029 17.2928C8.67806 17.257 8.7852 17.1927 8.87225 17.1057L16.8417 9.13619L14.8639 7.15833ZM17.9024 8.07553L19.1593 6.81862C19.4522 6.52572 19.4522 6.05085 19.1593 5.75796L18.2421 4.84076C17.9492 4.54787 17.4743 4.54787 17.1814 4.84076L15.9245 6.09767L17.9024 8.07553Z", fill: l }) });
var Nx = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M19.4357 13.9174C20.8659 13.0392 20.8659 10.9608 19.4357 10.0826L9.55234 4.01389C8.05317 3.09335 6.125 4.17205 6.125 5.93128L6.125 18.0688C6.125 19.828 8.05317 20.9067 9.55234 19.9861L19.4357 13.9174ZM18.6508 11.3609C19.1276 11.6536 19.1276 12.3464 18.6508 12.6391L8.76745 18.7079C8.26772 19.0147 7.625 18.6552 7.625 18.0688L7.625 5.93128C7.625 5.34487 8.26772 4.9853 8.76745 5.29215L18.6508 11.3609Z", fill: l }) });
var lk = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M11.2502 6C11.2502 5.58579 11.586 5.25 12.0002 5.25C12.4145 5.25 12.7502 5.58579 12.7502 6V11.2502H18.0007C18.4149 11.2502 18.7507 11.586 18.7507 12.0002C18.7507 12.4145 18.4149 12.7502 18.0007 12.7502H12.7502V18.0007C12.7502 18.4149 12.4145 18.7507 12.0002 18.7507C11.586 18.7507 11.2502 18.4149 11.2502 18.0007V12.7502H6C5.58579 12.7502 5.25 12.4145 5.25 12.0002C5.25 11.586 5.58579 11.2502 6 11.2502H11.2502V6Z", fill: l }) });
var Mk = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M11.25 13.42C11.25 13.8342 11.5858 14.17 12 14.17C12.4142 14.17 12.75 13.8342 12.75 13.42V2.75C12.75 2.33579 12.4142 2 12 2C11.5858 2 11.25 2.33579 11.25 2.75V13.42Z", fill: e }), C("path", { d: "M19.6248 12.8747C19.6248 9.44687 17.3628 6.54733 14.25 5.58733V4.02942C18.2014 5.0315 21.1248 8.61171 21.1248 12.8747C21.1248 17.9142 17.0394 21.9995 11.9999 21.9995C6.96043 21.9995 2.875 17.9142 2.875 12.8747C2.875 8.61165 5.79852 5.0314 9.75 4.02938V5.58728C6.63713 6.54722 4.375 9.44681 4.375 12.8747C4.375 17.0858 7.78883 20.4995 11.9999 20.4995C16.211 20.4995 19.6248 17.0858 19.6248 12.8747Z", fill: e })] });
var Kk = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M3.13644 9.54175C3.02923 9.94185 3.26667 10.3531 3.66676 10.4603C4.06687 10.5675 4.47812 10.3301 4.58533 9.92998C5.04109 8.22904 6.04538 6.72602 7.44243 5.65403C8.83948 4.58203 10.5512 4.00098 12.3122 4.00098C14.0731 4.00098 15.7848 4.58203 17.1819 5.65403C18.3999 6.58866 19.3194 7.85095 19.8371 9.28639L18.162 8.34314C17.801 8.1399 17.3437 8.26774 17.1405 8.62867C16.9372 8.98959 17.0651 9.44694 17.426 9.65017L20.5067 11.3849C20.68 11.4825 20.885 11.5072 21.0766 11.4537C21.2682 11.4001 21.4306 11.2727 21.5282 11.0993L23.2629 8.01828C23.4661 7.65734 23.3382 7.2 22.9773 6.99679C22.6163 6.79358 22.159 6.92145 21.9558 7.28239L21.195 8.63372C20.5715 6.98861 19.5007 5.54258 18.095 4.464C16.436 3.19099 14.4033 2.50098 12.3122 2.50098C10.221 2.50098 8.1883 3.19099 6.52928 4.464C4.87027 5.737 3.67766 7.52186 3.13644 9.54175Z", fill: e }), C("path", { d: "M21.4906 14.4582C21.5978 14.0581 21.3604 13.6469 20.9603 13.5397C20.5602 13.4325 20.1489 13.6699 20.0417 14.07C19.5859 15.7709 18.5816 17.274 17.1846 18.346C15.7875 19.418 14.0758 19.999 12.3149 19.999C10.5539 19.999 8.84219 19.418 7.44514 18.346C6.2292 17.4129 5.31079 16.1534 4.79261 14.721L6.45529 15.6573C6.81622 15.8605 7.27356 15.7327 7.47679 15.3718C7.68003 15.0108 7.55219 14.5535 7.19127 14.3502L4.11056 12.6155C3.93723 12.5179 3.73222 12.4932 3.54065 12.5467C3.34907 12.6003 3.18662 12.7278 3.08903 12.9011L1.3544 15.9821C1.15119 16.3431 1.27906 16.8004 1.64 17.0036C2.00094 17.2068 2.45828 17.079 2.66149 16.718L3.42822 15.3562C4.05115 17.0054 5.12348 18.4552 6.532 19.536C8.19102 20.809 10.2237 21.499 12.3149 21.499C14.406 21.499 16.4387 20.809 18.0977 19.536C19.7568 18.263 20.9494 16.4781 21.4906 14.4582Z", fill: e })] });
var Vm = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 25", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M11.25 2.75C6.14154 2.75 2 6.89029 2 11.998C2 17.1056 6.14154 21.2459 11.25 21.2459C13.5335 21.2459 15.6238 20.4187 17.2373 19.0475L20.7182 22.5287C21.011 22.8216 21.4859 22.8217 21.7788 22.5288C22.0717 22.2359 22.0718 21.761 21.7789 21.4681L18.2983 17.9872C19.6714 16.3736 20.5 14.2826 20.5 11.998C20.5 6.89029 16.3585 2.75 11.25 2.75ZM3.5 11.998C3.5 7.71905 6.96962 4.25 11.25 4.25C15.5304 4.25 19 7.71905 19 11.998C19 16.2769 15.5304 19.7459 11.25 19.7459C6.96962 19.7459 3.5 16.2769 3.5 11.998Z", fill: l }) });
var Am = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 25", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M15.277 13.2502C15.277 13.6645 14.9412 14.0002 14.527 14.0002L7.96792 14.0002C7.5537 14.0002 7.21792 13.6645 7.21792 13.2502C7.21792 12.836 7.5537 12.5002 7.96792 12.5002L14.527 12.5002C14.9412 12.5002 15.277 12.836 15.277 13.2502Z", fill: e }), C("path", { d: "M12.8468 9.75025C12.8468 10.1645 12.511 10.5002 12.0968 10.5002H7.96719C7.55297 10.5002 7.21719 10.1645 7.21719 9.75025C7.21719 9.33603 7.55297 9.00025 7.96719 9.00025L12.0968 9.00025C12.511 9.00025 12.8468 9.33603 12.8468 9.75025Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M2 11.4989C2 6.39126 6.14154 2.25098 11.25 2.25098C16.3585 2.25098 20.5 6.39126 20.5 11.4989C20.5 13.7836 19.6714 15.8747 18.2983 17.4883L21.7791 20.9695C22.072 21.2624 22.072 21.7372 21.7791 22.0301C21.4862 22.323 21.0113 22.323 20.7184 22.0301L17.2372 18.5486C15.6237 19.9197 13.5334 20.7469 11.25 20.7469C6.14154 20.7469 2 16.6066 2 11.4989ZM11.25 3.75098C6.96962 3.75098 3.5 7.22003 3.5 11.4989C3.5 15.7779 6.96962 19.2469 11.25 19.2469C15.5304 19.2469 19 15.7779 19 11.4989C19 7.22003 15.5304 3.75098 11.25 3.75098Z", fill: e })] });
var YS = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M10.787 15.6126C11.1768 15.7025 11.5829 15.75 12 15.75C14.9685 15.75 17.375 13.3435 17.375 10.375C17.375 7.40647 14.9685 5 12 5C9.03147 5 6.625 7.40647 6.625 10.375C6.625 12.0921 7.43016 13.6211 8.68354 14.6052L7.33598 16.4521C6.83707 17.1359 7.04126 18.1013 7.77431 18.5245C8.50736 18.9477 9.44555 18.6419 9.78827 17.8679L10.787 15.6126ZM12 6.5C9.8599 6.5 8.125 8.2349 8.125 10.375C8.125 11.5943 8.68811 12.682 9.56845 13.3923L11.0434 11.3708C11.2694 11.0611 11.6924 10.9717 12.0243 11.1633C12.3562 11.3549 12.4903 11.766 12.3351 12.1165L11.4101 14.2054C11.6024 14.2348 11.7994 14.25 12 14.25C14.1401 14.25 15.875 12.5151 15.875 10.375C15.875 8.2349 14.1401 6.5 12 6.5Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M18 2C19.2426 2 20.25 3.00736 20.25 4.25V19.75C20.25 20.9926 19.2426 22 18 22H6C4.75736 22 3.75 20.9926 3.75 19.75V4.25C3.75 3.00736 4.75736 2 6 2H18ZM18.75 4.25C18.75 3.83579 18.4142 3.5 18 3.5L6 3.5C5.58579 3.5 5.25 3.83579 5.25 4.25L5.25 19.75C5.25 20.1642 5.58579 20.5 6 20.5H18C18.4142 20.5 18.75 20.1642 18.75 19.75L18.75 4.25Z", fill: e })] });
var Ny = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M14.7223 12.7585C14.7426 12.3448 14.4237 11.9929 14.01 11.9726C13.5963 11.9522 13.2444 12.2711 13.2241 12.6848L12.9999 17.2415C12.9796 17.6552 13.2985 18.0071 13.7122 18.0274C14.1259 18.0478 14.4778 17.7289 14.4981 17.3152L14.7223 12.7585Z", fill: e }), C("path", { d: "M9.98802 11.9726C9.5743 11.9929 9.25542 12.3448 9.27577 12.7585L9.49993 17.3152C9.52028 17.7289 9.87216 18.0478 10.2859 18.0274C10.6996 18.0071 11.0185 17.6552 10.9981 17.2415L10.774 12.6848C10.7536 12.2711 10.4017 11.9522 9.98802 11.9726Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M10.249 2C9.00638 2 7.99902 3.00736 7.99902 4.25V5H5.5C4.25736 5 3.25 6.00736 3.25 7.25C3.25 8.28958 3.95503 9.16449 4.91303 9.42267L5.54076 19.8848C5.61205 21.0729 6.59642 22 7.78672 22H16.2113C17.4016 22 18.386 21.0729 18.4573 19.8848L19.085 9.42267C20.043 9.16449 20.748 8.28958 20.748 7.25C20.748 6.00736 19.7407 5 18.498 5H15.999V4.25C15.999 3.00736 14.9917 2 13.749 2H10.249ZM14.499 5V4.25C14.499 3.83579 14.1632 3.5 13.749 3.5H10.249C9.83481 3.5 9.49902 3.83579 9.49902 4.25V5H14.499ZM5.5 6.5C5.08579 6.5 4.75 6.83579 4.75 7.25C4.75 7.66421 5.08579 8 5.5 8H18.498C18.9123 8 19.248 7.66421 19.248 7.25C19.248 6.83579 18.9123 6.5 18.498 6.5H5.5ZM6.42037 9.5H17.5777L16.96 19.7949C16.9362 20.191 16.6081 20.5 16.2113 20.5H7.78672C7.38995 20.5 7.06183 20.191 7.03807 19.7949L6.42037 9.5Z", fill: e })] });
var rO = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M4.75 4C4.75 3.58579 4.41421 3.25 4 3.25C3.58579 3.25 3.25 3.58579 3.25 4V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H20.0005C20.4147 20.75 20.7505 20.4142 20.7505 20C20.7505 19.5858 20.4147 19.25 20.0005 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V10.3913L8.66929 14.3108C8.80994 14.4514 9.00071 14.5304 9.19963 14.5304C9.39854 14.5304 9.58931 14.4514 9.72996 14.3108L13.1523 10.8884L16.9328 14.6689H15.2115C14.7973 14.6689 14.4615 15.0047 14.4615 15.4189C14.4615 15.8332 14.7973 16.1689 15.2115 16.1689H18.7473C18.9462 16.1689 19.137 16.0899 19.2777 15.9493C19.4183 15.8086 19.4973 15.6178 19.4973 15.4189L19.4971 11.8834C19.497 11.4691 19.1612 11.1334 18.747 11.1334C18.3328 11.1334 17.997 11.4692 17.9971 11.8835L17.9972 13.612L13.6826 9.29743C13.542 9.15678 13.3512 9.07776 13.1523 9.07776C12.9534 9.07776 12.7626 9.15678 12.622 9.29743L9.19964 12.7198L4.75 8.26994V4Z", fill: l }) });
var wO = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { d: "M4.75 4C4.75 3.58579 4.41421 3.25 4 3.25C3.58579 3.25 3.25 3.58579 3.25 4V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H20.0005C20.4147 20.75 20.7505 20.4142 20.7505 20C20.7505 19.5858 20.4147 19.25 20.0005 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V15.7301L9.19964 11.2802L12.622 14.7026C12.7626 14.8432 12.9534 14.9222 13.1523 14.9222C13.3512 14.9222 13.542 14.8432 13.6826 14.7026L17.9972 10.388L17.9971 12.1165C17.997 12.5308 18.3328 12.8666 18.747 12.8666C19.1612 12.8666 19.497 12.5309 19.4971 12.1166L19.4973 8.58111C19.4973 8.38218 19.4183 8.19141 19.2777 8.05074C19.137 7.91008 18.9462 7.83105 18.7473 7.83105H15.2115C14.7973 7.83105 14.4615 8.16684 14.4615 8.58105C14.4615 8.99527 14.7973 9.33105 15.2115 9.33105H16.9328L13.1523 13.1116L9.72996 9.68923C9.58931 9.54857 9.39854 9.46956 9.19963 9.46956C9.00071 9.46956 8.80994 9.54858 8.66929 9.68924L4.75 13.6087V4Z", fill: l }) });
var KO = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M10.5 16C10.5 15.1716 11.1716 14.5 12 14.5C12.8284 14.5 13.5 15.1716 13.5 16V17.5C13.5 18.3284 12.8284 19 12 19C11.1716 19 10.5 18.3284 10.5 17.5V16Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C9.10051 2 6.75 4.35051 6.75 7.25V9.125H5.5C4.25736 9.125 3.25 10.1324 3.25 11.375V17.2495C3.25 19.8729 5.37665 21.9995 8 21.9995H16C18.6234 21.9995 20.75 19.8729 20.75 17.2495V11.375C20.75 10.1324 19.7426 9.125 18.5 9.125H8.25V7.25C8.25 5.17893 9.92893 3.5 12 3.5C13.4184 3.5 14.6541 4.28724 15.2919 5.45221C15.4909 5.81553 15.9466 5.9488 16.31 5.74987C16.6733 5.55095 16.8066 5.09516 16.6076 4.73184C15.7172 3.10553 13.9882 2 12 2ZM7.47268 10.625C7.48175 10.6253 7.49085 10.6255 7.5 10.6255C7.50915 10.6255 7.51825 10.6253 7.52732 10.625H18.5C18.9142 10.625 19.25 10.9608 19.25 11.375V17.2495C19.25 19.0444 17.7949 20.4995 16 20.4995H8C6.20507 20.4995 4.75 19.0444 4.75 17.2495V11.375C4.75 10.9608 5.08579 10.625 5.5 10.625H7.47268Z", fill: e })] });
var wD = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M16.4337 6.35C16.4337 8.74 14.4937 10.69 12.0937 10.69L12.0837 10.68C9.69365 10.68 7.74365 8.73 7.74365 6.34C7.74365 3.95 9.70365 2 12.0937 2C14.4837 2 16.4337 3.96 16.4337 6.35ZM14.9337 6.34C14.9337 4.78 13.6637 3.5 12.0937 3.5C10.5337 3.5 9.25365 4.78 9.25365 6.34C9.25365 7.9 10.5337 9.18 12.0937 9.18C13.6537 9.18 14.9337 7.9 14.9337 6.34Z", fill: e }), C("path", { d: "M12.0235 12.1895C14.6935 12.1895 16.7835 12.9395 18.2335 14.4195V14.4095C20.2801 16.4956 20.2739 19.2563 20.2735 19.4344L20.2735 19.4395C20.2635 19.8495 19.9335 20.1795 19.5235 20.1795H19.5135C19.0935 20.1695 18.7735 19.8295 18.7735 19.4195C18.7735 19.3695 18.7735 17.0895 17.1535 15.4495C15.9935 14.2795 14.2635 13.6795 12.0235 13.6795C9.78346 13.6795 8.05346 14.2795 6.89346 15.4495C5.27346 17.0995 5.27346 19.3995 5.27346 19.4195C5.27346 19.8295 4.94346 20.1795 4.53346 20.1795C4.17346 20.1995 3.77346 19.8595 3.77346 19.4495L3.77345 19.4448C3.77305 19.2771 3.76646 16.506 5.81346 14.4195C7.26346 12.9395 9.35346 12.1895 12.0235 12.1895Z", fill: e })] });
var pD = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M15.3289 11.4955C14.4941 11.4955 13.724 11.2188 13.1051 10.7522C13.3972 10.3301 13.6284 9.86262 13.786 9.36254C14.1827 9.7539 14.7276 9.99545 15.3289 9.99545C16.5422 9.99545 17.5258 9.01185 17.5258 7.79851C17.5258 6.58517 16.5422 5.60156 15.3289 5.60156C14.7276 5.60156 14.1827 5.84312 13.786 6.23449C13.6284 5.73441 13.3972 5.26698 13.1051 4.84488C13.7239 4.37824 14.4941 4.10156 15.3289 4.10156C17.3706 4.10156 19.0258 5.75674 19.0258 7.79851C19.0258 9.84027 17.3706 11.4955 15.3289 11.4955Z", fill: e }), C("path", { d: "M14.7723 13.1891C15.0227 13.437 15.2464 13.6945 15.4463 13.9566C16.7954 13.9826 17.7641 14.3143 18.4675 14.7651C19.2032 15.2366 19.6941 15.8677 20.0242 16.5168C20.3563 17.1698 20.5204 17.8318 20.6002 18.337C20.6398 18.5878 20.6579 18.795 20.6661 18.9365C20.6702 19.0071 20.6717 19.061 20.6724 19.0952L20.6726 19.1161L20.6727 19.1313L20.6727 19.1363L21.4197 19.1486C20.6793 19.1358 20.6728 19.136 20.6727 19.1363L20.6727 19.1376C20.6666 19.5509 20.9961 19.8914 21.4096 19.8985C21.8237 19.9057 22.1653 19.5758 22.1725 19.1617L21.4284 19.1488C22.1725 19.1617 22.1725 19.1621 22.1725 19.1617L22.1725 19.1599L22.1726 19.1575L22.1726 19.1511L22.1727 19.1319C22.1727 19.1163 22.1726 19.0951 22.1721 19.0686C22.1712 19.0158 22.1689 18.9419 22.1636 18.85C22.153 18.6665 22.1303 18.4094 22.0819 18.1029C21.9856 17.4936 21.7848 16.6697 21.3612 15.8368C20.9357 15 20.2801 14.1451 19.2768 13.5022C18.2708 12.8574 16.9604 12.4549 15.274 12.4549C14.8284 12.4549 14.4092 12.483 14.0148 12.5362C14.2852 12.7384 14.5376 12.9566 14.7723 13.1891Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.13173 7.79855C5.13173 5.75678 6.7869 4.1016 8.82867 4.1016C10.8704 4.1016 12.5256 5.75678 12.5256 7.79855C12.5256 9.84031 10.8704 11.4955 8.82867 11.4955C6.7869 11.4955 5.13173 9.84031 5.13173 7.79855ZM8.82867 5.6016C7.61533 5.6016 6.63173 6.58521 6.63173 7.79855C6.63173 9.01189 7.61533 9.99549 8.82867 9.99549C10.042 9.99549 11.0256 9.01189 11.0256 7.79855C11.0256 6.58521 10.042 5.6016 8.82867 5.6016Z", fill: e }), C("path", { d: "M3.37502 19.1374C3.38126 19.5507 3.0517 19.8914 2.63812 19.8986C2.22397 19.9058 1.88241 19.5759 1.87522 19.1617L2.62511 19.1487C1.87522 19.1617 1.87523 19.1621 1.87522 19.1617L1.87519 19.1599L1.87516 19.1575L1.87509 19.1511L1.875 19.1319C1.87499 19.1163 1.87512 19.0951 1.87559 19.0687C1.87653 19.0158 1.87882 18.942 1.88413 18.85C1.89474 18.6665 1.91745 18.4094 1.96585 18.103C2.0621 17.4936 2.26292 16.6697 2.68648 15.8368C3.11206 15 3.76758 14.1452 4.77087 13.5022C5.77688 12.8575 7.08727 12.455 8.77376 12.455C10.4602 12.455 11.7706 12.8575 12.7767 13.5022C13.7799 14.1452 14.4355 15 14.861 15.8368C15.2846 16.6697 15.4854 17.4936 15.5817 18.103C15.6301 18.4094 15.6528 18.6665 15.6634 18.85C15.6687 18.942 15.671 19.0158 15.6719 19.0687C15.6724 19.0951 15.6725 19.1163 15.6725 19.1319L15.6724 19.1511L15.6724 19.1575L15.6723 19.1599C15.6723 19.1603 15.6723 19.1617 14.9282 19.1488L15.6723 19.1617C15.6651 19.5759 15.3235 19.9058 14.9094 19.8986C14.4959 19.8914 14.1664 19.5509 14.1725 19.1376L14.1725 19.1364C14.1726 19.1361 14.1791 19.1358 14.9199 19.1487L14.1725 19.1364L14.1725 19.1314L14.1724 19.1161L14.1722 19.0952C14.1716 19.061 14.17 19.0072 14.1659 18.9366C14.1577 18.7951 14.1396 18.5878 14.1 18.337C14.0202 17.8319 13.8561 17.1699 13.524 16.5168C13.1939 15.8677 12.703 15.2366 11.9673 14.7651C11.2343 14.2954 10.2132 13.955 8.77376 13.955C7.33434 13.955 6.31319 14.2954 5.58022 14.7651C4.84453 15.2366 4.35363 15.8677 4.02351 16.5168C3.6914 17.1699 3.52727 17.8319 3.44749 18.337C3.40787 18.5878 3.38981 18.7951 3.38163 18.9366C3.37756 19.0072 3.37596 19.061 3.37536 19.0952C3.37505 19.1123 3.375 19.1245 3.375 19.1314L3.37502 19.1374Z", fill: e })] });
var wj = ({ size: o2 = 20, color: l = "currentColor", ...e }) => C("svg", { ...e, width: o2, height: o2, viewBox: "0 0 25 24", xmlns: "http://www.w3.org/2000/svg", children: C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M5.75 3.25C4.50736 3.25 3.5 4.25736 3.5 5.5V18.5C3.5 19.7426 4.50736 20.75 5.75 20.75H18.75C19.9926 20.75 21 19.7426 21 18.5V9.5C21 8.25736 19.9926 7.25 18.75 7.25H18.5V5.5C18.5 4.25736 17.4926 3.25 16.25 3.25H5.75ZM17 7.25V5.5C17 5.08579 16.6642 4.75 16.25 4.75H5.75C5.33579 4.75 5 5.08579 5 5.5V7.25H17ZM5 8.75V18.5C5 18.9142 5.33579 19.25 5.75 19.25H18.75C19.1642 19.25 19.5 18.9142 19.5 18.5V9.5C19.5 9.08579 19.1642 8.75 18.75 8.75H5Z", fill: l }) });
var hb = ({ size: l = 20, color: e = "currentColor", ...t }) => o("svg", { ...t, width: l, height: l, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [C("path", { d: "M8.78362 8.78412C8.49073 9.07702 8.49073 9.55189 8.78362 9.84478L10.9388 12L8.78362 14.1552C8.49073 14.4481 8.49073 14.923 8.78362 15.2159C9.07652 15.5088 9.55139 15.5088 9.84428 15.2159L11.9995 13.0607L14.1546 15.2158C14.4475 15.5087 14.9224 15.5087 15.2153 15.2158C15.5082 14.9229 15.5082 14.448 15.2153 14.1551L13.0602 12L15.2153 9.84485C15.5082 9.55196 15.5082 9.07708 15.2153 8.78419C14.9224 8.4913 14.4475 8.4913 14.1546 8.78419L11.9995 10.9393L9.84428 8.78412C9.55139 8.49123 9.07652 8.49123 8.78362 8.78412Z", fill: e }), C("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12Z", fill: e })] });
export {
Bi as AddAndSubtractIcon,
lk as AddIcon,
$a as AiIcon,
il as BillIcon,
Cl as ChartIcon,
wO as ChartLineIcon,
Jr as CheckCircleIcon,
Md as ChevronDownIcon,
ao as ChevronRightIcon,
hb as CloseCircleIcon,
Yd as CloudIcon,
IL as CreditcardIcon,
Mc as Dashboard1Icon,
Ln as DashboardIcon,
Ny as DeleteIcon,
au as DiscountIcon,
tx as EditIcon,
Am as FileSearchIcon,
kg as FolderIcon,
YS as FolderOpenIcon,
Ue as GiftIcon,
Ap as HeartIcon,
Tu as LayersIcon,
KO as LockCheckedIcon,
XV as LockOnIcon,
Us as LogoutIcon,
ks as MailIcon,
zv as MinusCircleIcon,
Kn as MoneyIcon,
vl as NotificationIcon,
DH as OrderIcon,
Nx as PlayCircleIcon,
Mk as PoweroffIcon,
Kk as RefreshIcon,
yh as RotateIcon,
Vm as SearchIcon,
gn as ServerIcon,
_g as SettingIcon,
hp as StopCircleIcon,
NC as SwapIcon,
_g as SystemSettingIcon,
td as TaskCheckedIcon,
Id as TaskIcon,
tM as TaskSettingIcon,
Xu as TemplateIcon,
rO as TrendDownIcon,
wO as TrendUpIcon,
KC as UndertakeTransactionIcon,
wD as UserCircleIcon,
wD as UserIcon,
pD as UserListIcon,
bZ as ViewListIcon,
wj as WalletIcon
};
+2
View File
@@ -10,11 +10,13 @@ export const CHART_COLORS = {
placeholderColor: 'rgba(0, 0, 0, 0.35)',
borderColor: '#dcdcdc',
containerColor: '#fff',
secondaryContainerColor: '#f3f4f6',
},
[ETheme.dark]: {
textColor: 'rgba(255, 255, 255, 0.9)',
placeholderColor: 'rgba(255, 255, 255, 0.35)',
borderColor: '#5e5e5e',
containerColor: '#242424',
secondaryContainerColor: '#343434',
},
};
-22
View File
@@ -1,22 +0,0 @@
export default {
mock: {
// 本地mock数据
API: '',
},
development: {
// 开发环境接口请求
API: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
},
test: {
// 测试环境接口地址
API: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
},
release: {
// 正式环境接口地址
API: 'https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com',
},
site: {
// TDesign部署特殊需要 与release功能一致
API: 'https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com',
},
};
+2 -1
View File
@@ -48,6 +48,7 @@ declare module 'tvision-color';
declare interface ImportMeta {
env: {
MODE: 'development' | 'test' | 'release' | 'mock' | 'site';
MODE: 'development' | 'test' | 'release' | 'site';
BASE_URL: string;
};
}
+44 -3
View File
@@ -7,6 +7,48 @@ import lodashSet from 'lodash/set';
import lodashMap from 'lodash/map';
import { ETheme } from '../types';
const resolveChartTokens = (value: any, colors: Record<string, any>): any => {
if (typeof value === 'string') {
return value
.replaceAll('var(--td-component-border)', colors.borderColor)
.replaceAll('var(--td-text-color-secondary)', colors.placeholderColor)
.replaceAll('var(--td-text-color-primary)', colors.textColor)
.replaceAll('var(--td-bg-color-secondarycontainer)', colors.secondaryContainerColor)
.replaceAll('var(--td-bg-color-container)', colors.containerColor)
.replaceAll('var(--td-brand-color)', colors.colorList?.[0] || colors.textColor)
.replaceAll('var(--td-success-color)', colors.colorList?.[2] || colors.textColor)
.replaceAll('var(--td-warning-color)', colors.colorList?.[3] || colors.textColor)
.replaceAll('var(--td-error-color)', colors.colorList?.[5] || colors.textColor);
}
if (Array.isArray(value)) return value.map((item) => resolveChartTokens(item, colors));
if (!value || typeof value !== 'object') return value;
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveChartTokens(item, colors)]));
};
const applyChartTextColor = (value: any, color: string, key = ''): any => {
if (Array.isArray(value)) return value.map((item) => applyChartTextColor(item, color, key));
if (!value || typeof value !== 'object') return value;
const result = Object.fromEntries(Object.entries(value).map(([childKey, item]) => [childKey, applyChartTextColor(item, color, childKey)]));
if (['textStyle', 'axisLabel', 'nameTextStyle', 'label'].includes(key) && result.color === undefined) result.color = color;
// ECharts 4+ expects text properties directly on title/legend/tooltip.
// Keeping the values flat avoids the deprecated `textStyle` hierarchy warning.
if (['title', 'legend', 'tooltip'].includes(key)) {
const nestedTextStyle = result.textStyle;
if (nestedTextStyle && typeof nestedTextStyle === 'object') {
Object.entries(nestedTextStyle).forEach(([property, propertyValue]) => {
if (result[property] === undefined) result[property] = propertyValue;
});
}
if (result.color === undefined) result.color = nestedTextStyle?.color || color;
delete result.textStyle;
}
if (['xAxis', 'yAxis'].includes(key)) {
result.axisLabel = { ...(result.axisLabel || {}), color: result.axisLabel?.color || color };
result.nameTextStyle = { ...(result.nameTextStyle || {}), color: result.nameTextStyle?.color || color };
}
return result;
};
export type TChartColorKey = keyof (typeof CHART_COLORS)[ETheme.light];
/**
* 根据当前主题色返回动态的图表颜色列表
@@ -21,9 +63,8 @@ export default function useDynamicChart(
const { theme, color } = useAppSelector(selectGlobal);
return useMemo(() => {
const dynamicColor = getChartColor(theme, color);
const newOptions = {
...options,
};
const newOptions = applyChartTextColor(resolveChartTokens(options, dynamicColor), dynamicColor.textColor);
newOptions.textStyle = { ...(newOptions.textStyle || {}), color: newOptions.textStyle?.color || dynamicColor.textColor };
// 设置动态的图表颜色
lodashSet(newOptions, 'color', dynamicColor.colorList);
if (configs) {
@@ -1,4 +1,5 @@
.sidePanel {
position: relative;
height: 100%;
display: flex;
flex-direction: row!important;
@@ -27,3 +28,17 @@
.mixContent {
overflow: auto;
}
@media (max-width: 999px) {
.sideContainer,
.topPanel,
.mixMain,
.mixContent {
min-width: 0;
width: auto;
}
.sideContainer {
width: 100%;
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Layout } from 'tdesign-react';
import { Layout } from '@miragenflow/ui/layout';
import { ELayout } from 'modules/global';
import Header from './Header';
import Footer from './Footer';
@@ -41,7 +41,7 @@ const MixLayout = React.memo(() => (
</Layout>
));
const FullPageLayout = React.memo(() => <Content />);
const FullPageLayout = React.memo(() => <Content fullPage />);
export default {
[ELayout.side]: SideLayout,
+53 -10
View File
@@ -1,14 +1,23 @@
import React, { Suspense, memo } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Layout, Loading } from 'tdesign-react';
import React, { Suspense, memo, useEffect, useState } from 'react';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { Loading } from '@miragenflow/ui';
import { Layout } from '@miragenflow/ui/layout';
import routers, { IRouter } from 'router';
import { resolve } from 'utils/path';
import Page from './Page';
import Style from './AppRouter.module.less';
import { useAppDispatch, useAppSelector } from 'modules/store';
import { getUserInfo, setToken } from 'modules/user';
import { refreshAdminSession } from 'services/platform';
const { Content } = Layout;
type TRenderRoutes = (routes: IRouter[], parentPath?: string, breadcrumbs?: string[]) => React.ReactNode[];
const PermissionGate = ({ scope, children }: { scope?: string; children: React.ReactNode }) => {
const user = useAppSelector((state) => state.user.userInfo as { roles?: string[]; scopes?: string[] });
if (scope && !user.roles?.includes('super_admin') && !user.scopes?.includes(scope)) return <Navigate to='/403' replace state={{ requiredScope: scope, roles: user.roles || [] }} />;
return <>{children}</>;
};
/**
* 渲染应用路由
* @param routes
@@ -18,7 +27,7 @@ type TRenderRoutes = (routes: IRouter[], parentPath?: string, breadcrumbs?: stri
const renderRoutes: TRenderRoutes = (routes, parentPath = '', breadcrumb = []) =>
routes.map((route, index: number) => {
const { Component, children, redirect, meta } = route;
const currentPath = resolve(parentPath, route.path);
const currentPath = route.absolute ? route.path : resolve(parentPath, route.path);
let currentBreadcrumb = breadcrumb;
if (meta?.title) {
@@ -37,9 +46,11 @@ const renderRoutes: TRenderRoutes = (routes, parentPath = '', breadcrumb = []) =
key={index}
path={currentPath}
element={
<PermissionGate scope={meta?.requiredScope}>
<Page isFullPage={route.isFullPage} breadcrumbs={currentBreadcrumb}>
<Component />
</Page>
</PermissionGate>
}
/>
);
@@ -48,9 +59,38 @@ const renderRoutes: TRenderRoutes = (routes, parentPath = '', breadcrumb = []) =
return children ? renderRoutes(children, currentPath, currentBreadcrumb) : null;
});
const AppRouter = () => (
<Content>
<Suspense
const AppRouter = ({ fullPage = false }: { fullPage?: boolean }) => {
const location = useLocation();
const token = useAppSelector((state) => state.user.token);
const dispatch = useAppDispatch();
const [bootstrapping, setBootstrapping] = useState(!location.pathname.startsWith('/login'));
const userInfo = useAppSelector((state) => state.user.userInfo as { roles?: string[]; scopes?: string[] });
useEffect(() => {
let active = true;
if (location.pathname.startsWith('/login')) {
setBootstrapping(false);
return () => { active = false; };
}
let loadUser: Promise<unknown>;
if (token && (userInfo.roles?.length || userInfo.scopes?.length)) {
loadUser = Promise.resolve();
} else if (token) {
loadUser = dispatch(getUserInfo()).unwrap();
} else {
loadUser = refreshAdminSession().then((next) => {
if (!next) return undefined;
dispatch(setToken(next));
return dispatch(getUserInfo()).unwrap();
});
}
loadUser.catch(() => undefined).finally(() => {
if (active) setBootstrapping(false);
});
return () => { active = false; };
}, [dispatch, location.pathname, token, userInfo.roles?.length, userInfo.scopes?.length]);
if (bootstrapping) return <div className={Style.loading}><Loading /></div>;
if (!token && !location.pathname.startsWith('/login')) return <Navigate to='/login' replace />;
const routesContent = <Suspense
fallback={
<div className={Style.loading}>
<Loading />
@@ -58,8 +98,11 @@ const AppRouter = () => (
}
>
<Routes>{renderRoutes(routers)}</Routes>
</Suspense>
</Content>
);
</Suspense>;
return fullPage ? routesContent : <Content>
{routesContent}
</Content>;
};
export default memo(AppRouter);
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { Layout, Row } from 'tdesign-react';
import { Row } from '@miragenflow/ui/grid';
import { Layout } from '@miragenflow/ui/layout';
import { useAppSelector } from 'modules/store';
import { selectGlobal } from 'modules/global';
@@ -13,7 +14,7 @@ const Footer = () => {
return (
<TFooter>
<Row justify='center'>Copyright © 2022-{new Date().getFullYear()} Tencent. All Rights Reserved</Row>
<Row justify='center'>元境幻生管理台 · MiragenFlow</Row>
</TFooter>
);
};
@@ -12,23 +12,39 @@
}
}
.menuIcon {
width: 30px !important;
height: 30px !important;
width: 40px !important;
height: 40px !important;
}
.menuIcon > svg {
width: 24px !important;
height: 24px !important;
}
.dropItem {
display: flex;
align-items: center;
min-height: 28px;
svg {
width: 24px;
height: 24px;
flex: 0 0 auto;
}
span {
margin-left: 8px;
}
}
.dropdown {
min-width: 40px;
height: 40px !important;
padding-left: 0;
padding-right: 0;
justify-content: flex-start;
text-align: left;
.icon {
font-size: 20px;
width: 24px;
height: 24px;
display: inline-block;
margin: 0 5px;
flex: 0 0 auto;
margin: 0 4px;
}
:global {
.t-button--variant-text {
@@ -38,17 +54,27 @@
.t-button__text {
display: flex;
align-items: center;
justify-content: flex-start;
text-align: left;
}
.t-dropdown__item {
max-width: none !important;
width: 117px;
width: 136px;
&-text {
display: flex;
align-items: center;
}
.t-icon {
margin-right: 8px;
}
}
}
@media (max-width: 600px) {
.text {
display: none;
}
.dropdown .icon {
margin-left: 2px;
margin-right: 2px;
}
}
@@ -1,18 +1,14 @@
import React, { memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Popup, Badge, Dropdown, Space } from 'tdesign-react';
import {
Icon,
LogoGithubIcon,
MailIcon,
HelpCircleIcon,
SettingIcon,
PoweroffIcon,
UserCircleIcon,
} from 'tdesign-icons-react';
import { Dropdown } from '@miragenflow/ui/dropdown';
import { Popup } from '@miragenflow/ui/popup';
import { Space } from '@miragenflow/ui/space';
import { MiragenFlowButton } from '@miragenflow/ui';
import { ChevronDownIcon, DashboardIcon, SettingIcon, PoweroffIcon, UserCircleIcon } from 'components/LineIcons';
import { useAppDispatch } from 'modules/store';
import { toggleSetting } from 'modules/global';
import { logout } from 'modules/user';
import { adminLogout } from 'services/platform';
import Style from './HeaderIcon.module.less';
const { DropdownMenu, DropdownItem } = Dropdown;
@@ -21,63 +17,37 @@ export default memo(() => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const gotoWiki = () => {
window.open('https://tdesign.tencent.com/react/overview');
};
const gotoGitHub = () => {
window.open('https://github.com/Tencent/tdesign-react-starter');
};
const clickHandler = (data: any) => {
if (data.value === 1) {
navigate('/user/index');
}
const handleOverview = () => {
navigate('/dashboard/base');
};
const handleLogout = async () => {
try {
await adminLogout();
} finally {
try {
await dispatch(logout());
navigate('/login/index');
} finally {
navigate('/login');
}
}
};
return (
<Space align='center'>
<Badge className={Style.badge} count={6} dot={false} maxCount={99} shape='circle' showZero={false} size='medium'>
<Button className={Style.menuIcon} shape='square' size='large' variant='text' icon={<MailIcon />} />
</Badge>
<Popup content='代码仓库' placement='bottom' showArrow destroyOnClose>
<Button
className={Style.menuIcon}
shape='square'
size='large'
variant='text'
onClick={gotoGitHub}
icon={<LogoGithubIcon />}
/>
</Popup>
<Popup content='帮助文档' placement='bottom' showArrow destroyOnClose>
<Button
className={Style.menuIcon}
shape='square'
size='large'
variant='text'
onClick={gotoWiki}
icon={<HelpCircleIcon />}
/>
</Popup>
<Dropdown trigger={'click'} onClick={clickHandler}>
<Button variant='text' className={Style.dropdown}>
<Icon name='user-circle' className={Style.icon} />
<span className={Style.text}>Tencent</span>
<Icon name='chevron-down' className={Style.icon} />
</Button>
<Dropdown trigger={'click'}>
<MiragenFlowButton aria-label='管理员菜单' type='text' className={Style.dropdown}>
<UserCircleIcon className={Style.icon} />
<span className={Style.text}>元境幻生管理员</span>
<ChevronDownIcon className={Style.icon} />
</MiragenFlowButton>
<DropdownMenu>
<DropdownItem value={1}>
<DropdownItem value='overview' onClick={handleOverview}>
<div className={Style.dropItem}>
<UserCircleIcon />
<span>个人中心</span>
<DashboardIcon />
<span>系统概览</span>
</div>
</DropdownItem>
<DropdownItem value={1} onClick={handleLogout}>
<DropdownItem value='logout' onClick={handleLogout}>
<div className={Style.dropItem}>
<PoweroffIcon />
<span>退出登录</span>
@@ -86,11 +56,12 @@ export default memo(() => {
</DropdownMenu>
</Dropdown>
<Popup content='页面设置' placement='bottom' showArrow destroyOnClose>
<Button
<MiragenFlowButton
aria-label='页面设置'
className={Style.menuIcon}
shape='square'
size='large'
variant='text'
type='text'
onClick={() => dispatch(toggleSetting())}
icon={<SettingIcon />}
/>
@@ -1,14 +0,0 @@
.panel {
:global {
.t-input {
border: none;
}
.t-input:hover {
background: var(--td-bg-color-secondarycontainer);
transition: background var(--td-anim-duration-base) linear
}
.t-input--focused {
box-shadow: none;
}
}
}
@@ -1,8 +0,0 @@
import React from 'react';
import { Input } from 'tdesign-react';
import { SearchIcon } from 'tdesign-icons-react';
import Style from './Search.module.less';
const Search = () => <Input className={Style.panel} prefixIcon={<SearchIcon />} placeholder='请输入搜索内容' />;
export default React.memo(Search);
@@ -9,3 +9,27 @@
justify-content: space-between;
border-bottom: 1px solid var(--td-component-stroke);
}
.menuButton {
width: 40px;
height: 40px;
}
.menuButton > svg {
width: 24px !important;
height: 24px !important;
}
@media (max-width: 600px) {
.panel {
padding-left: 8px;
padding-right: 8px;
:global {
.t-space {
min-width: 0;
gap: 4px;
}
}
}
}
@@ -1,11 +1,12 @@
import React, { memo } from 'react';
import { Layout, Button, Space } from 'tdesign-react';
import { ViewListIcon } from 'tdesign-icons-react';
import { Layout } from '@miragenflow/ui/layout';
import { Space } from '@miragenflow/ui/space';
import { MiragenFlowButton } from '@miragenflow/ui';
import { ViewListIcon } from 'components/LineIcons';
import { useAppDispatch, useAppSelector } from 'modules/store';
import { selectGlobal, toggleMenu } from 'modules/global';
import HeaderIcon from './HeaderIcon';
import { HeaderMenu } from '../Menu';
import Search from './Search';
import Style from './index.module.less';
const { Header } = Layout;
@@ -28,14 +29,15 @@ export default memo((props: { showMenu?: boolean }) => {
} else {
HeaderLeft = (
<Space align='center'>
<Button
<MiragenFlowButton
aria-label={globalState.collapsed ? '展开菜单' : '收起菜单'}
className={Style.menuButton}
shape='square'
size='large'
variant='text'
type='text'
onClick={() => dispatch(toggleMenu(null))}
icon={<ViewListIcon />}
/>
<Search />
</Space>
);
}
+79 -2
View File
@@ -9,17 +9,41 @@
opacity: 0.4;
height: 38px;
line-height: 38px;
text-align: center;
padding: 0 20px;
text-align: left;
}
.menuLogo {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
justify-content: flex-start;
align-items: center;
padding: 0 20px;
box-sizing: border-box;
cursor: pointer;
color: var(--td-text-color-primary);
text-align: left;
}
.brandLogo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
line-height: 1.1;
color: var(--td-text-color-primary);
}
.brandLogo strong {
font-size: 18px;
letter-spacing: 0;
}
.brandLogo small {
color: var(--td-text-color-secondary);
font-size: 11px;
font-weight: 400;
}
.menuMiniLogo {
@@ -32,3 +56,56 @@
width: 184px;
height: 32px;
}
.menuPanel2 :global(.t-menu__item > svg:not(.t-fake-arrow)),
.menuPanel2 :global(.t-submenu__title > svg:not(.t-fake-arrow)),
.headerMenu :global(.t-menu__item > svg:not(.t-fake-arrow)),
.headerMenu :global(.t-submenu__title > svg:not(.t-fake-arrow)) {
width: 24px;
height: 24px;
flex: 0 0 auto;
margin-right: var(--td-comp-margin-s);
}
.menuPanel2 :global(.t-menu__item > .t-fake-arrow),
.menuPanel2 :global(.t-submenu__title > .t-fake-arrow),
.headerMenu :global(.t-menu__item > .t-fake-arrow),
.headerMenu :global(.t-submenu__title > .t-fake-arrow) {
width: 16px;
height: 16px;
margin-right: 0;
}
// Keep the navigation label block anchored to the icon/text start even when
// TDesign's internal flex defaults change between menu themes.
.menuPanel2 :global(.t-menu__item),
.menuPanel2 :global(.t-submenu__title),
.headerMenu :global(.t-menu__item),
.headerMenu :global(.t-submenu__title) {
justify-content: flex-start;
text-align: left;
}
.mobileMenuBackdrop {
display: none;
}
@media (max-width: 999px) {
.menuPanel2 {
position: fixed !important;
inset: 0 auto 0 0;
z-index: 1001;
width: 232px !important;
box-shadow: var(--td-shadow-3);
}
.mobileMenuBackdrop {
position: fixed;
inset: 0 0 0 232px;
z-index: 1000;
display: block;
padding: 0;
border: 0;
background: rgba(0, 0, 0, 0.32);
}
}
+73 -91
View File
@@ -1,115 +1,97 @@
import React, { memo, useState } from 'react';
import React, { memo, useEffect, useMemo, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Menu, MenuValue } from 'tdesign-react';
import { Menu, type MenuValue } from '@miragenflow/ui/menu';
import { MiragenFlowButton } from '@miragenflow/ui';
import router, { IRouter } from 'router';
import { resolve } from 'utils/path';
import { useAppSelector } from 'modules/store';
import { selectGlobal } from 'modules/global';
import { useAppDispatch, useAppSelector } from 'modules/store';
import { selectGlobal, toggleMenu } from 'modules/global';
import MenuLogo from './MenuLogo';
import Style from './Menu.module.less';
const { SubMenu, MenuItem, HeadMenu } = Menu;
interface IMenuProps { showLogo?: boolean; showOperation?: boolean; }
interface IMenuProps {
showLogo?: boolean;
showOperation?: boolean;
}
const renderMenuItems = (menu: IRouter[], parentPath = '') => {
const navigate = useNavigate();
return menu.map((item) => {
const { children, meta, path } = item;
if (!meta || meta?.hidden === true) {
// 无meta信息 或 hidden == true,路由不显示为菜单
return null;
}
const { Icon, title, single } = meta;
const routerPath = resolve(parentPath, path);
if (!children || children.length === 0) {
return (
<MenuItem
key={routerPath}
value={routerPath}
icon={Icon ? <Icon /> : undefined}
onClick={() => navigate(routerPath)}
>
{title}
</MenuItem>
);
}
if (single && children?.length > 0) {
const firstChild = children[0];
if (firstChild?.meta && !firstChild?.meta?.hidden) {
const { Icon, title } = meta;
const singlePath = resolve(resolve(parentPath, path), firstChild.path);
return (
<MenuItem
key={singlePath}
value={singlePath}
icon={Icon ? <Icon /> : undefined}
onClick={() => navigate(singlePath)}
>
{title}
</MenuItem>
);
}
}
return (
<SubMenu key={routerPath} value={routerPath} title={title} icon={Icon ? <Icon /> : undefined}>
{renderMenuItems(children, routerPath)}
</SubMenu>
);
});
const routePath = (item: IRouter, parentPath = '') => item.absolute ? item.path : `${parentPath}${item.path}` || '/';
const isVisible = (item: IRouter, scopes: string[], roles: string[]) => {
if (!item.meta || item.meta.hidden) return false;
if (item.meta.requiredScope && !roles.includes('super_admin') && !scopes.includes(item.meta.requiredScope)) return false;
return true;
};
const visibleChildren = (item: IRouter, scopes: string[], roles: string[]) => (item.children || []).filter((child) => isVisible(child, scopes, roles));
const hasVisibleChild = (item: IRouter, scopes: string[], roles: string[]) => visibleChildren(item, scopes, roles).length > 0;
const renderMenuItems = (menu: IRouter[], scopes: string[], roles: string[], parentPath = ''): React.ReactNode[] => menu.map((item) => {
const { children, meta } = item;
if (!meta || !isVisible(item, scopes, roles)) return null;
const currentPath = routePath(item, parentPath);
if (children?.length) {
const visible = visibleChildren(item, scopes, roles);
if (!visible.length) return null;
if (visible.length === 1) {
const child = visible[0];
const childPath = routePath(child, currentPath);
const ChildIcon = child.meta?.Icon || meta.Icon;
return <MenuItem key={childPath} value={childPath} icon={ChildIcon ? <ChildIcon /> : undefined}>{child.meta?.title || meta.title}</MenuItem>;
}
return <SubMenu key={currentPath} value={currentPath} title={meta.title} icon={meta.Icon ? <meta.Icon /> : undefined}>
{renderMenuItems(visible, scopes, roles, currentPath)}
</SubMenu>;
}
return <MenuItem key={currentPath} value={currentPath} icon={meta.Icon ? <meta.Icon /> : undefined}>{meta.title}</MenuItem>;
});
const expandedFor = (menu: IRouter[], pathname: string, scopes: string[], roles: string[], parent = ''): string[] => menu.flatMap((item) => {
if (!item.children?.length || !hasVisibleChild(item, scopes, roles)) return [];
const visible = visibleChildren(item, scopes, roles);
if (visible.length === 1) return [];
const current = routePath(item, parent);
const childPaths = visible.map((child) => routePath(child, current));
return childPaths.includes(pathname) ? [current] : expandedFor(visible, pathname, scopes, roles, current);
});
/**
* 顶部菜单
*/
export const HeaderMenu = memo(() => {
const globalState = useAppSelector(selectGlobal);
const location = useLocation();
const [active, setActive] = useState<MenuValue>(location.pathname); // todo
return (
<HeadMenu
expandType='popup'
style={{ marginBottom: 20 }}
value={active}
theme={globalState.theme}
onChange={(v) => setActive(v)}
>
{renderMenuItems(router)}
</HeadMenu>
);
const navigate = useNavigate();
const user = useAppSelector((state) => state.user.userInfo as { scopes?: string[]; roles?: string[] });
const [active, setActive] = useState<MenuValue>(location.pathname);
useEffect(() => setActive(location.pathname), [location.pathname]);
return <HeadMenu className={Style.headerMenu} expandType='popup' style={{ marginBottom: 20 }} value={active} theme={globalState.theme} onChange={(value) => { setActive(value); navigate(String(value)); }}>
{renderMenuItems(router, user.scopes || [], user.roles || [])}
</HeadMenu>;
});
/**
* 左侧菜单
*/
export default memo((props: IMenuProps) => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const globalState = useAppSelector(selectGlobal);
const { version } = globalState;
const bottomText = globalState.collapsed ? version : `元境幻生管理台 ${version}`;
return (
const user = useAppSelector((state) => state.user.userInfo as { scopes?: string[]; roles?: string[] });
const scopes = user.scopes || [];
const roles = user.roles || [];
const requiredExpanded = useMemo(() => expandedFor(router, location.pathname, scopes, roles), [location.pathname, scopes.join(','), roles.join(',')]);
const [expanded, setExpanded] = useState<MenuValue[]>(requiredExpanded);
useEffect(() => {
setExpanded((current) => Array.from(new Set([...current, ...requiredExpanded])));
}, [requiredExpanded.join('|')]);
const onNavigate = (value: MenuValue) => {
navigate(String(value));
if (window.innerWidth < 1000) dispatch(toggleMenu(true));
};
if (globalState.collapsed) return null;
return <>
<MiragenFlowButton htmlType='button' type='text' className={Style.mobileMenuBackdrop} aria-label='关闭菜单' onClick={() => dispatch(toggleMenu(true))} />
<Menu
width='232px'
style={{ flexShrink: 0, height: '100%' }}
className={Style.menuPanel2}
value={location.pathname}
expanded={expanded}
onChange={onNavigate}
onExpand={setExpanded}
theme={globalState.theme}
collapsed={globalState.collapsed}
operations={props.showOperation ? <div className={Style.menuTip}>{bottomText}</div> : undefined}
logo={props.showLogo ? <MenuLogo collapsed={globalState.collapsed} /> : undefined}
>
{renderMenuItems(router)}
</Menu>
);
operations={props.showOperation ? <div className={Style.menuTip}>元境幻生管理台 {globalState.version}</div> : undefined}
logo={props.showLogo ? <MenuLogo collapsed={false} /> : undefined}
>{renderMenuItems(router, scopes, roles)}</Menu>
</>;
});
+10 -4
View File
@@ -1,8 +1,8 @@
import React, { memo } from 'react';
import Style from './Menu.module.less';
import FullLogo from 'assets/svg/assets-logo-full.svg?component';
import MiniLogo from 'assets/svg/assets-t-logo.svg?component';
import { useNavigate } from 'react-router-dom';
import { MiragenFlowButton } from '@miragenflow/ui';
interface IProps {
collapsed?: boolean;
@@ -16,8 +16,14 @@ export default memo((props: IProps) => {
};
return (
<div className={Style.menuLogo} onClick={handleClick}>
{props.collapsed ? <MiniLogo /> : <FullLogo />}
</div>
<MiragenFlowButton
aria-label='返回管理台首页'
className={Style.menuLogo}
htmlType='button'
onClick={handleClick}
type='text'
>
{props.collapsed ? <MiniLogo /> : <span className={Style.brandLogo}><strong>元境幻生</strong><small>管理台</small></span>}
</MiragenFlowButton>
);
});
@@ -7,3 +7,9 @@
.breadcrumb {
margin-bottom: 8px;
}
@media (max-width: 600px) {
.panel {
margin: 8px;
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../modules/store';
import { selectGlobal, switchFullPage } from '../../modules/global';
import { Layout, Breadcrumb } from 'tdesign-react';
import { Layout } from '@miragenflow/ui/layout';
import { Breadcrumb } from '@miragenflow/ui/breadcrumb';
import Style from './Page.module.less';
const { Content } = Layout;
@@ -10,6 +10,11 @@
border-radius: 50%;
}
.box:focus-visible {
outline: 2px solid var(--td-brand-color);
outline-offset: 3px;
}
.item {
width: 24px;
height: 24px;
@@ -1,24 +1,70 @@
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { defaultColor } from 'configs/color';
import { Popup, ColorPickerPanel } from 'tdesign-react';
import { ColorPickerPanel } from '@miragenflow/ui/color-picker';
import { Popup } from '@miragenflow/ui/popup';
import Style from './RadioColor.module.less';
interface IProps {
defaultValue?: number | string;
value?: number | string;
onChange: (color: string) => void;
}
const RadioColor = (props: IProps) => {
const [isColoPickerDisplay, onPopupVisibleChange] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
const selectedIndex = defaultColor.indexOf(props.value as string);
const activeIndex = selectedIndex === -1 ? defaultColor.length : selectedIndex;
const colorOptionCount = defaultColor.length + 1;
const focusOption = (index: number) => {
panelRef.current?.querySelectorAll<HTMLElement>('[role="radio"]')[index]?.focus();
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>, index: number) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
if (index === defaultColor.length) {
onPopupVisibleChange(true);
} else {
props.onChange(defaultColor[index]);
}
return;
}
let nextIndex = index;
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
nextIndex = (index + 1) % colorOptionCount;
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
nextIndex = (index - 1 + colorOptionCount) % colorOptionCount;
} else if (event.key === 'Home') {
nextIndex = 0;
} else if (event.key === 'End') {
nextIndex = colorOptionCount - 1;
} else {
return;
}
event.preventDefault();
focusOption(nextIndex);
if (nextIndex < defaultColor.length) {
props.onChange(defaultColor[nextIndex]);
onPopupVisibleChange(false);
}
};
return (
<div className={Style.panel}>
<div ref={panelRef} className={Style.panel} role='radiogroup' aria-label='主题色'>
{defaultColor.map((color, index) => (
<div
key={index}
role='radio'
aria-label={color}
aria-checked={selectedIndex === index}
tabIndex={activeIndex === index ? 0 : -1}
onClick={() => props?.onChange(color)}
onKeyDown={(event) => handleKeyDown(event, index)}
className={Style.box}
style={{ borderColor: props.defaultValue === color && !isColoPickerDisplay ? color : 'transparent' }}
style={{ borderColor: props.value === color && !isColoPickerDisplay ? color : 'transparent' }}
>
<div className={Style.item} style={{ backgroundColor: color }} />
</div>
@@ -37,17 +83,23 @@ const RadioColor = (props: IProps) => {
colorModes={['monochrome']}
format='HEX'
swatchColors={[]}
defaultValue={props.defaultValue as string}
value={typeof props.value === 'string' ? props.value : undefined}
/>
}
>
<div
key='dynamic'
role='radio'
aria-label='自定义颜色'
aria-checked={selectedIndex === -1}
tabIndex={activeIndex === defaultColor.length ? 0 : -1}
aria-haspopup='dialog'
onKeyDown={(event) => handleKeyDown(event, defaultColor.length)}
className={Style.box}
style={{
borderColor:
isColoPickerDisplay || defaultColor.indexOf(props.defaultValue as string) === -1
? (props.defaultValue as string)
isColoPickerDisplay || defaultColor.indexOf(props.value as string) === -1
? (props.value as string)
: 'transparent',
}}
>
@@ -3,11 +3,20 @@
justify-content: space-between;
}
.rectOption {
cursor: pointer;
border-radius: 4px;
}
.rectOption:focus-visible {
outline: 2px solid var(--td-brand-color);
outline-offset: 3px;
}
.rectItem {
border: 2px solid #e3e6eb;
padding: 8px;
border-radius: 3px;
cursor: pointer;
}
.rectItemSelected {
@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import React, { memo, useEffect, useState } from 'react';
import classname from 'classnames';
import Style from './RadioRect.module.less';
@@ -9,21 +9,58 @@ interface IOption {
}
interface IProps {
value?: number | string;
defaultValue?: number | string;
onChange: (value?: any) => void;
options: IOption[];
ariaLabel?: string;
}
export default memo((props: IProps) => {
const [selectValue, setSelectValue] = useState(props.defaultValue);
const isControlled = props.value !== undefined;
const currentValue = isControlled ? props.value : selectValue;
const handleClick = (option: IOption) => {
useEffect(() => {
if (!isControlled) {
setSelectValue(props.defaultValue);
}
}, [isControlled, props.defaultValue]);
const handleChange = (option: IOption) => {
if (!isControlled) {
setSelectValue(option.value);
props?.onChange(option.value);
}
props.onChange(option.value);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>, index: number) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleChange(props.options[index]);
return;
}
let nextIndex = index;
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
nextIndex = (index + 1) % props.options.length;
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
nextIndex = (index - 1 + props.options.length) % props.options.length;
} else if (event.key === 'Home') {
nextIndex = 0;
} else if (event.key === 'End') {
nextIndex = props.options.length - 1;
} else {
return;
}
event.preventDefault();
handleChange(props.options[nextIndex]);
event.currentTarget.parentElement?.querySelectorAll<HTMLElement>('[role="radio"]')[nextIndex]?.focus();
};
return (
<div className={Style.radioRectPanel}>
<div className={Style.radioRectPanel} role='radiogroup' aria-label={props.ariaLabel}>
{props.options.map((item, index) => {
let ImageItem = item.image;
if (typeof item.image === 'string') {
@@ -31,10 +68,17 @@ export default memo((props: IProps) => {
}
return (
<div key={index}>
<div
className={classname(Style.rectItem, { [Style.rectItemSelected]: selectValue === item.value })}
onClick={() => handleClick(item)}
key={item.value ?? index}
className={Style.rectOption}
role='radio'
aria-checked={currentValue === item.value}
tabIndex={currentValue === item.value || (currentValue === undefined && index === 0) ? 0 : -1}
onClick={() => handleChange(item)}
onKeyDown={(event) => handleKeyDown(event, index)}
>
<div
className={classname(Style.rectItem, { [Style.rectItemSelected]: currentValue === item.value })}
>
{ImageItem}
</div>
@@ -11,3 +11,14 @@
line-height: 21px;
margin-bottom: 24px;
}
.layoutPreview {
width: 88px;
height: 48px;
display: grid;
place-items: center;
border-radius: 3px;
background: var(--td-bg-color-container);
color: var(--td-text-color-secondary);
font-size: 13px;
}
+13 -10
View File
@@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { Row, Col, Switch } from 'tdesign-react';
import { Row, Col } from '@miragenflow/ui/grid';
import { Switch } from '@miragenflow/ui';
import { useAppDispatch, useAppSelector } from 'modules/store';
import {
selectGlobal,
@@ -47,15 +48,15 @@ const themeList = [
const layoutList = [
{
value: ELayout.side,
image: 'https://tdesign.gtimg.com/starter/setting/side.png',
image: <div className={Style.layoutPreview}>侧</div>,
},
{
value: ELayout.top,
image: 'https://tdesign.gtimg.com/starter/setting/top.png',
image: <div className={Style.layoutPreview}>顶</div>,
},
{
value: ELayout.mix,
image: 'https://tdesign.gtimg.com/starter/setting/mix.png',
image: <div className={Style.layoutPreview}>混</div>,
},
];
@@ -76,25 +77,27 @@ export default memo(() => {
<div>
<div className={Style.settingTitle}>主题模式</div>
<RadioRect
defaultValue={globalState.systemTheme ? ESettingTheme.system : globalState.theme}
value={globalState.systemTheme ? ESettingTheme.system : globalState.theme}
onChange={handleThemeSwitch}
options={themeList}
ariaLabel='主题模式'
/>
<div className={Style.settingTitle}>主题色</div>
<RadioColor defaultValue={globalState.color} onChange={(value) => dispatch(switchColor(value))} />
<RadioColor value={globalState.color} onChange={(value) => dispatch(switchColor(value))} />
<div className={Style.settingTitle}>导航布局</div>
<RadioRect
defaultValue={globalState.layout}
value={globalState.layout}
onChange={(value) => dispatch(switchLayout(value))}
options={layoutList}
ariaLabel='导航布局'
/>
<div className={Style.settingTitle}>元素开关</div>
<Row justify='space-between'>
<Col>
<div className={Style.settingSubTitle}>显示 Header</div>
<div className={Style.settingSubTitle}>显示顶部栏</div>
</Col>
<Col>
<Switch size='large' value={globalState.showHeader} onChange={() => dispatch(toggleShowHeader())} />
@@ -103,7 +106,7 @@ export default memo(() => {
<Row justify='space-between'>
<Col>
<div className={Style.settingSubTitle}>显示 Breadcrumbs</div>
<div className={Style.settingSubTitle}>显示面包屑导航</div>
</Col>
<Col>
<Switch size='large' value={globalState.showBreadcrumbs} onChange={() => dispatch(toggleShowBreadcrumbs())} />
@@ -112,7 +115,7 @@ export default memo(() => {
<Row justify='space-between'>
<Col>
<div className={Style.settingSubTitle}>显示 Footer</div>
<div className={Style.settingSubTitle}>显示页脚</div>
</Col>
<Col>
<Switch size='large' value={globalState.showFooter} onChange={() => dispatch(toggleShowFooter())} />
+43 -16
View File
@@ -1,24 +1,49 @@
import React, { memo, useEffect } from 'react';
import { Drawer, Layout } from 'tdesign-react';
import { Layout } from '@miragenflow/ui/layout';
import { Drawer } from '@miragenflow/ui';
import { useLocation } from 'react-router-dom';
import throttle from 'lodash/throttle';
import { useAppSelector, useAppDispatch } from 'modules/store';
import { selectGlobal, toggleSetting, toggleMenu, ELayout, switchTheme } from 'modules/global';
import { selectGlobal, setSetting, toggleMenu, switchTheme } from 'modules/global';
import Setting from './components/Setting';
import AppLayout from './components/AppLayout';
import AppRouter from './components/AppRouter';
import Style from './index.module.less';
export default memo(() => {
const globalState = useAppSelector(selectGlobal);
const dispatch = useAppDispatch();
const AppContainer = AppLayout[globalState.isFullPage ? ELayout.fullPage : globalState.layout];
const location = useLocation();
const isLoginRoute = location.pathname.startsWith('/login');
const closeSettings = () => {
dispatch(setSetting(false));
window.requestAnimationFrame(() => {
document.querySelector<HTMLElement>('[aria-label="页面设置"]')?.focus();
});
};
const settingsDrawer = (
<Drawer
destroyOnClose
visible={globalState.setting}
size='min(458px, calc(100vw - 24px))'
footer={false}
header='页面配置'
closeBtn
closeOnOverlayClick
closeOnEscKeydown
onClose={closeSettings}
onOverlayClick={closeSettings}
>
<Setting />
</Drawer>
);
useEffect(() => {
dispatch(switchTheme(globalState.theme));
const handleResize = throttle(() => {
if (window.innerWidth < 900) {
if (window.innerWidth < 1000) {
dispatch(toggleMenu(true));
} else if (window.innerWidth > 1000) {
} else {
dispatch(toggleMenu(false));
}
}, 100);
@@ -28,19 +53,21 @@ export default memo(() => {
};
}, []);
// 登录页是独立的全屏入口,不需要被后台的 Layout/Content 再包一层。
// 这样移动端会直接复用桌面左侧主体的定位,不会出现额外容器边界。
if (isLoginRoute) {
return <>
<AppRouter fullPage />
{settingsDrawer}
</>;
}
const AppContainer = AppLayout[globalState.layout];
return (
<Layout className={Style.panel}>
<AppContainer />
<Drawer
destroyOnClose
visible={globalState.setting}
size='458px'
footer={false}
header='页面配置'
onClose={() => dispatch(toggleSetting())}
>
<Setting />
</Drawer>
{settingsDrawer}
</Layout>
);
});
+27 -3
View File
@@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { TDesignProvider } from '@miragenflow/ui';
import store from 'modules/store';
import App from 'layouts/index';
@@ -9,8 +10,26 @@ import 'tdesign-react/es/style/index.css';
import './styles/index.less';
const env = import.meta.env.MODE || 'development';
const baseRouterName = env === 'site' ? '/starter/react/' : import.meta.env.BASE_URL.replace(/\/$/, '');
const baseRouterName = (() => {
const configuredBaseUrl = import.meta.env.BASE_URL || '/';
// Vite allows an absolute `base` URL for the static site build. React
// Router, however, expects `basename` to contain only the current path.
// Normalize both `/admin/` and `https://host/starter/react/` to their
// pathname so deep links continue to render instead of matching no route.
try {
const { pathname } = new URL(configuredBaseUrl, window.location.origin);
const normalized = pathname === '/' ? '' : pathname.replace(/\/$/, '');
const current = window.location.pathname.replace(/\/$/, '') || '/';
if (!normalized || current === normalized || current.startsWith(`${normalized}/`)) return normalized;
// When the admin bundle is mounted through the web app's `/admin` proxy,
// prefer the runtime mount path if a stale/absolute build base is present.
if (current === '/admin' || current.startsWith('/admin/')) return '/admin';
return '';
} catch {
return configuredBaseUrl.replace(/\/$/, '');
}
})();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = document.getElementById('app')!;
@@ -18,8 +37,13 @@ const root = document.getElementById('app')!;
const renderApp = () => {
ReactDOM.createRoot(root).render(
<Provider store={store}>
<BrowserRouter basename={baseRouterName}>
<BrowserRouter
basename={baseRouterName}
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
>
<TDesignProvider locale='zh-CN'>
<App />
</TDesignProvider>
</BrowserRouter>
</Provider>,
);
+4 -2
View File
@@ -73,6 +73,9 @@ const globalSlice = createSlice({
toggleSetting: (state) => {
state.setting = !state.setting;
},
setSetting: (state, action: PayloadAction<boolean>) => {
state.setting = Boolean(action.payload);
},
toggleShowHeader: (state) => {
state.showHeader = !state.showHeader;
},
@@ -94,14 +97,12 @@ const globalSlice = createSlice({
},
openSystemTheme: (state) => {
const media = window.matchMedia('(prefers-color-scheme:dark)');
if (media.matches) {
const finalTheme = media.matches ? ETheme.dark : ETheme.light;
state.chartColors = CHART_COLORS[finalTheme];
// 切换主题颜色
state.theme = finalTheme;
state.systemTheme = true;
document.documentElement.setAttribute('theme-mode', finalTheme);
}
},
switchColor: (state, action) => {
if (action?.payload) {
@@ -138,6 +139,7 @@ export const selectGlobal = (state: RootState) => state.global;
export const {
toggleMenu,
toggleSetting,
setSetting,
toggleShowHeader,
toggleShowBreadcrumbs,
toggleShowFooter,
-64
View File
@@ -1,64 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '../store';
import { getContractList, IContract } from 'services/contract';
const namespace = 'list/base';
interface IInitialState {
loading: boolean;
current: number;
pageSize: number;
total: number;
contractList: IContract[];
}
const initialState: IInitialState = {
loading: true,
current: 1,
pageSize: 10,
total: 0,
contractList: [],
};
export const getList = createAsyncThunk(
`${namespace}/getList`,
async (params: { pageSize: number; current: number }) => {
const result = await getContractList(params);
return {
list: result?.list,
total: result?.total,
pageSize: params.pageSize,
current: params.current,
};
},
);
const listBaseSlice = createSlice({
name: namespace,
initialState,
reducers: {
clearPageState: () => initialState,
},
extraReducers: (builder) => {
builder
.addCase(getList.pending, (state) => {
state.loading = true;
})
.addCase(getList.fulfilled, (state, action) => {
state.loading = false;
state.contractList = action.payload?.list;
state.total = action.payload?.total;
state.pageSize = action.payload?.pageSize;
state.current = action.payload?.current;
})
.addCase(getList.rejected, (state) => {
state.loading = false;
});
},
});
export const { clearPageState } = listBaseSlice.actions;
export const selectListBase = (state: RootState) => state.listBase;
export default listBaseSlice.reducer;
-73
View File
@@ -1,73 +0,0 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../store';
import { getProductList, IProduct } from 'services/product';
const namespace = 'list/card';
interface IInitialState {
pageLoading: boolean;
loading: boolean;
current: number;
pageSize: number;
total: number;
productList: IProduct[];
}
const initialState: IInitialState = {
pageLoading: true,
loading: true,
current: 1,
pageSize: 12,
total: 0,
productList: [],
};
export const getList = createAsyncThunk(
`${namespace}/getList`,
async (params: { pageSize: number; current: number }) => {
const { pageSize, current } = params;
const result = await getProductList({
pageSize,
current,
});
return {
list: result?.list,
total: result?.total,
pageSize: params.pageSize,
current: params.current,
};
},
);
const listCardSlice = createSlice({
name: namespace,
initialState,
reducers: {
clearPageState: () => initialState,
switchPageLoading: (state, action: PayloadAction<boolean>) => {
state.pageLoading = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getList.pending, (state) => {
state.loading = true;
})
.addCase(getList.fulfilled, (state, action) => {
state.loading = false;
state.productList = action.payload?.list;
state.total = action.payload?.total;
state.pageSize = action.payload?.pageSize;
state.current = action.payload?.current;
})
.addCase(getList.rejected, (state) => {
state.loading = false;
});
},
});
export const { clearPageState, switchPageLoading } = listCardSlice.actions;
export const selectListCard = (state: RootState) => state.listCard;
export default listCardSlice.reducer;
-64
View File
@@ -1,64 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '../store';
import { getContractList, IContract } from 'services/contract';
const namespace = 'list/select';
interface IInitialState {
loading: boolean;
current: number;
pageSize: number;
total: number;
contractList: IContract[];
}
const initialState: IInitialState = {
loading: true,
current: 1,
pageSize: 10,
total: 0,
contractList: [],
};
export const getList = createAsyncThunk(
`${namespace}/getSelectList`,
async (params: { pageSize: number; current: number }) => {
const result = await getContractList(params);
return {
list: result?.list,
total: result?.total,
pageSize: params.pageSize,
current: params.current,
};
},
);
const listSelectSlice = createSlice({
name: namespace,
initialState,
reducers: {
clearPageState: () => initialState,
},
extraReducers: (builder) => {
builder
.addCase(getList.pending, (state) => {
state.loading = true;
})
.addCase(getList.fulfilled, (state, action) => {
state.loading = false;
state.contractList = action.payload?.list;
state.total = action.payload?.total;
state.pageSize = action.payload?.pageSize;
state.current = action.payload?.current;
})
.addCase(getList.rejected, (state) => {
state.loading = false;
});
},
});
export const { clearPageState } = listSelectSlice.actions;
export const selectListSelect = (state: RootState) => state.listSelect;
export default listSelectSlice.reducer;
-6
View File
@@ -3,16 +3,10 @@ import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';
import global from './global';
import user from './user';
import listBase from './list/base';
import listSelect from './list/select';
import listCard from './list/card';
const reducer = combineReducers({
global,
user,
listBase,
listSelect,
listCard,
});
export const store = configureStore({
+14 -60
View File
@@ -1,90 +1,46 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '../store';
import { adminRequest, setAdminAccessToken } from 'services/platform';
const namespace = 'user';
const TOKEN_NAME = 'tdesign-starter';
const initialState = {
token: localStorage.getItem(TOKEN_NAME) || 'main_token', // 默认token不走权限
token: '',
userInfo: {},
};
// login
export const login = createAsyncThunk(`${namespace}/login`, async (userInfo: Record<string, unknown>) => {
const mockLogin = async (userInfo: Record<string, unknown>) => {
// 登录请求流程
console.log(userInfo);
// const { account, password } = userInfo;
// if (account !== 'td') {
// return {
// code: 401,
// message: '账号不存在',
// };
// }
// if (['main_', 'dev_'].indexOf(password) === -1) {
// return {
// code: 401,
// message: '密码错误',
// };
// }
// const token = {
// main_: 'main_token',
// dev_: 'dev_token',
// }[password];
return {
code: 200,
message: '登陆成功',
data: 'main_token',
};
};
const res = await mockLogin(userInfo);
if (res.code === 200) {
return res.data;
}
throw res;
const result = await adminRequest<{ accessToken?: string; mfaRequired?: boolean; challengeId?: string }>('/api/v1/admin/auth/login', { method: 'POST', body: JSON.stringify({ email: userInfo.account, password: userInfo.password, mfaCode: userInfo.mfaCode, recoveryCode: userInfo.recoveryCode, challengeId: userInfo.challengeId, deviceId: 'admin-browser', captchaChallengeId: userInfo.captchaChallengeId, captchaAnswer: userInfo.captchaAnswer }) });
if (result.accessToken) setAdminAccessToken(result.accessToken);
return result;
});
// getUserInfo
export const getUserInfo = createAsyncThunk(`${namespace}/getUserInfo`, async (_, { getState }: any) => {
const { token } = getState();
const mockRemoteUserInfo = async (token: string) => {
if (token === 'main_token') {
return {
name: 'td_main',
roles: ['all'],
};
}
return {
name: 'td_dev',
roles: ['userIndex', 'dashboardBase', 'login'],
};
};
const res = await mockRemoteUserInfo(token);
return res;
const result = await adminRequest<{ id?: string; email: string; roles?: string[]; scopes?: string[]; mfaRequired?: boolean; mfaEnabled?: boolean; selfApprovalAllowed?: boolean }>('/api/v1/admin/me');
return { id: result.id, name: result.email, email: result.email, roles: result.roles || [], scopes: result.scopes || [], mfaRequired: result.mfaRequired, mfaEnabled: result.mfaEnabled, selfApprovalAllowed: result.selfApprovalAllowed === true };
});
const userSlice = createSlice({
name: namespace,
initialState,
reducers: {
setToken: (state, action: { payload: string }) => { state.token = action.payload; },
logout: (state) => {
localStorage.removeItem(TOKEN_NAME);
state.token = '';
state.userInfo = {};
},
remove: (state) => {
state.token = '';
},
clearMfaChallenge: (state) => {
state.userInfo = { ...state.userInfo, mfaChallengeId: undefined, mfaRequired: undefined };
},
},
extraReducers: (builder) => {
builder
.addCase(login.fulfilled, (state, action) => {
localStorage.setItem(TOKEN_NAME, action.payload);
state.token = action.payload;
if (action.payload.accessToken) state.token = action.payload.accessToken;
state.userInfo = { ...state.userInfo, mfaChallengeId: action.payload.challengeId, mfaRequired: action.payload.mfaRequired };
})
.addCase(getUserInfo.fulfilled, (state, action) => {
state.userInfo = action.payload;
@@ -92,8 +48,6 @@ const userSlice = createSlice({
},
});
export const selectListBase = (state: RootState) => state.listBase;
export const { logout, remove } = userSlice.actions;
export const { logout, remove, setToken, clearMfaChallenge } = userSlice.actions;
export default userSlice.reducer;
+103
View File
@@ -0,0 +1,103 @@
import React, { useMemo } from 'react';
import { Card, Statistic } from '@miragenflow/ui';
import ReactEcharts from 'echarts-for-react';
import useDynamicChart from 'hooks/useDynamicChart';
import Style from './index.module.less';
type FinanceRow = Record<string, any>;
type FinanceKind = 'billing' | 'recharges' | 'payments';
const numberValue = (value: unknown) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};
const dateValue = (row: FinanceRow) => {
const value = row.createdAt || row.paidAt || row.appliedAt;
return typeof value === 'string' && value.length >= 10 ? value.slice(0, 10) : '未知日期';
};
const baseGrid = { left: 48, right: 24, top: 26, bottom: 34, containLabel: true };
const axisStyle = { axisLine: { lineStyle: { color: 'var(--td-component-border)' } }, splitLine: { lineStyle: { color: 'var(--td-component-border)' } } };
const statusLabels: Record<string, string> = { pending: '待处理', processing: '处理中', queued: '排队中', received: '已接收', paid: '已到账', succeeded: '成功', applied: '已应用', confirmed_success: '已确认成功', rejected: '已拒绝', failed: '失败', confirmed_failure: '已确认失败', canceled: '已取消', refunded: '已退款', unknown: '未知' };
const providerLabels: Record<string, string> = { manual: '人工', mock: '模拟', adapter: '适配器', webhook: '支付回调', unknown: '未知' };
const ChartPanel = ({ title, option }: { title: string; option: Record<string, any> }) => {
const themedOption = useDynamicChart(option);
return <Card title={title} bordered={false} className={Style.chartCard}>
<ReactEcharts option={themedOption} notMerge lazyUpdate style={{ height: 270, width: '100%' }} />
</Card>;
};
const emptyOption = (title: string) => ({ title: { text: title, left: 'center', top: 'middle', fontSize: 14, fontWeight: 400 }, xAxis: { show: false }, yAxis: { show: false }, series: [] });
const StatCards = ({ items }: { items: Array<{ title: string; value: number; suffix?: string; decimalPlaces?: number }> }) => <div className={Style.stats}>{items.map((item) => <Card key={item.title} className={Style.statCard} bordered={false}><Statistic title={item.title} value={item.value} suffix={item.suffix} decimalPlaces={item.decimalPlaces} /></Card>)}</div>;
const statusOptionFor = (rows: FinanceRow[], fallback: string) => {
const counts = rows.reduce<Record<string, number>>((result, row) => {
const status = String(row.status || 'unknown');
result[status] = (result[status] || 0) + 1;
return result;
}, {});
const data = Object.entries(counts).reduce<Array<{ name: string; value: number }>>((result, [status, value]) => {
const name = statusLabels[status] || '其他状态';
const item = result.find((entry) => entry.name === name);
if (item) item.value += value; else result.push({ name, value });
return result;
}, []);
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption(fallback);
};
export const FinanceCharts = ({ kind, rows }: { kind: FinanceKind; rows: FinanceRow[] }) => {
const billingSummary = useMemo(() => {
const credit = rows.reduce((sum, row) => { const amount = numberValue(row.amount); return sum + (amount > 0 ? amount : 0); }, 0);
const debit = rows.reduce((sum, row) => { const amount = numberValue(row.amount); return sum + (amount < 0 ? Math.abs(amount) : 0); }, 0);
return { total: rows.length, credit, debit, net: credit - debit };
}, [rows]);
const billingTrend = useMemo(() => {
const byDate = rows.reduce<Record<string, { credit: number; debit: number }>>((result, row) => {
const date = dateValue(row); const amount = numberValue(row.amount); const current = result[date] || { credit: 0, debit: 0 };
if (amount >= 0) current.credit += amount; else current.debit += Math.abs(amount);
result[date] = current; return result;
}, {});
const dates = Object.keys(byDate).sort().slice(-14);
return dates.length ? { tooltip: { trigger: 'axis' }, grid: baseGrid, legend: { bottom: 0, data: ['入账金币', '扣减金币'] }, xAxis: { type: 'category', data: dates.map((date) => date.slice(5)), ...axisStyle }, yAxis: { type: 'value', min: 0, name: '金币', ...axisStyle }, series: [{ name: '入账金币', type: 'bar', data: dates.map((date) => byDate[date].credit), barMaxWidth: 26 }, { name: '扣减金币', type: 'bar', data: dates.map((date) => byDate[date].debit), barMaxWidth: 26 }] } : emptyOption('暂无账本趋势');
}, [rows]);
const billingTypes = useMemo(() => {
const counts = rows.reduce<Record<string, number>>((result, row) => { const type = String(row.type || 'unknown'); result[type] = (result[type] || 0) + 1; return result; }, {});
const labels: Record<string, string> = { credit: '充值入账', recharge: '充值入账', debit: '任务扣费', settle: '任务扣费', adjustment: '余额调整', balance_adjustment: '余额调整', manual_recharge: '人工充值', refund: '退款', purchase: '套餐购买', reserve: '余额预留', release: '预留释放', charge: '任务消费', unknown: '未知' };
const data = Object.entries(counts).reduce<Array<{ name: string; value: number }>>((result, [type, value]) => {
const name = labels[type] || '其他类型';
const item = result.find((entry) => entry.name === name);
if (item) item.value += value; else result.push({ name, value });
return result;
}, []);
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption('暂无账本类型');
}, [rows]);
const rechargeTrend = useMemo(() => {
const byDate = rows.reduce<Record<string, { fiat: number; coins: number }>>((result, row) => {
const date = dateValue(row); const current = result[date] || { fiat: 0, coins: 0 }; current.fiat += numberValue(row.fiatAmount); current.coins += numberValue(row.balanceAmount); result[date] = current; return result;
}, {});
const dates = Object.keys(byDate).sort().slice(-14);
return dates.length ? { tooltip: { trigger: 'axis' }, grid: baseGrid, legend: { bottom: 0, data: ['支付金额', '到账金币'] }, xAxis: { type: 'category', data: dates.map((date) => date.slice(5)), ...axisStyle }, yAxis: [{ type: 'value', name: '金额', min: 0, ...axisStyle }, { type: 'value', name: '金币', min: 0, ...axisStyle }], series: [{ name: '支付金额', type: 'bar', data: dates.map((date) => byDate[date].fiat), barMaxWidth: 26 }, { name: '到账金币', type: 'line', yAxisIndex: 1, data: dates.map((date) => byDate[date].coins), smooth: true, symbolSize: 7 }] } : emptyOption('暂无充值趋势');
}, [rows]);
const providerOption = useMemo(() => {
const counts = rows.reduce<Record<string, number>>((result, row) => { const provider = providerLabels[String(row.provider || 'unknown')] || '其他支付方式'; result[provider] = (result[provider] || 0) + 1; return result; }, {});
const data = Object.entries(counts).sort(([, a], [, b]) => b - a).slice(0, 12);
return data.length ? { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { ...baseGrid, left: 76 }, xAxis: { type: 'value', min: 0, ...axisStyle }, yAxis: { type: 'category', data: data.map(([name]) => name), ...axisStyle }, series: [{ name: '事件数', type: 'bar', data: data.map(([, value]) => value), barMaxWidth: 24, label: { show: true, position: 'right' } }] } : emptyOption('暂无支付适配器数据');
}, [rows]);
if (kind === 'billing') return <><StatCards items={[{ title: '账本记录', value: billingSummary.total }, { title: '累计入账', value: billingSummary.credit, suffix: '金币', decimalPlaces: 2 }, { title: '累计扣减', value: billingSummary.debit, suffix: '金币', decimalPlaces: 2 }, { title: '净变化', value: billingSummary.net, suffix: '金币', decimalPlaces: 2 }]} /><div className={Style.chartGrid}><ChartPanel title='近 14 日金币流动' option={billingTrend} /><ChartPanel title='账本类型分布' option={billingTypes} /></div></>;
if (kind === 'recharges') {
const pending = rows.filter((row) => ['pending', 'processing', 'queued'].includes(String(row.status))).length;
const paid = rows.filter((row) => ['paid', 'succeeded', 'applied', 'confirmed_success'].includes(String(row.status))).length;
const fiat = rows.reduce((sum, row) => sum + numberValue(row.fiatAmount), 0);
return <><StatCards items={[{ title: '充值订单', value: rows.length }, { title: '已到账订单', value: paid }, { title: '待处理订单', value: pending }, { title: '累计订单金额', value: fiat, suffix: '元', decimalPlaces: 2 }]} /><div className={Style.chartGrid}><ChartPanel title='充值订单状态' option={statusOptionFor(rows, '暂无充值订单')} /><ChartPanel title='近 14 日充值金额与金币' option={rechargeTrend} /></div></>;
}
const applied = rows.filter((row) => ['applied', 'succeeded', 'confirmed_success', 'paid'].includes(String(row.status))).length;
const rejected = rows.filter((row) => ['rejected', 'failed', 'confirmed_failure'].includes(String(row.status))).length;
const pending = rows.filter((row) => ['pending', 'processing', 'queued', 'received', 'unknown'].includes(String(row.status))).length;
return <><StatCards items={[{ title: '支付事件', value: rows.length }, { title: '已应用事件', value: applied }, { title: '异常事件', value: rejected }, { title: '待处理事件', value: pending }]} /><div className={Style.chartGrid}><ChartPanel title='支付事件状态' option={statusOptionFor(rows, '暂无支付事件')} /><ChartPanel title='支付适配器事件量' option={providerOption} /></div></>;
};
@@ -0,0 +1,328 @@
import React, { useMemo } from 'react';
import { Card, Statistic } from '@miragenflow/ui';
import ReactEcharts from 'echarts-for-react';
import useDynamicChart from 'hooks/useDynamicChart';
import Style from '../index.module.less';
type ChartRow = Record<string, any>;
const isChartRow = (value: unknown): value is ChartRow => Boolean(value && typeof value === 'object' && !Array.isArray(value));
const chartRows = (value: unknown) => Array.isArray(value) ? value.filter(isChartRow) : [];
const numberValue = (value: unknown) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};
const percent = (value: unknown) => `${(numberValue(value) * 100).toFixed(1)}%`;
const emptyOption = (title: string) => ({
title: { text: title, left: 'center', top: 'middle', fontSize: 14, fontWeight: 400 },
xAxis: { show: false },
yAxis: { show: false },
series: [],
});
const ChartPanel = ({ title, option }: { title: string; option: Record<string, any> }) => {
const themedOption = useDynamicChart(option);
return <Card title={title} bordered={false} className={Style.chartCard}>
<ReactEcharts option={themedOption} notMerge lazyUpdate style={{ height: 270, width: '100%' }} />
</Card>;
};
const baseGrid = { left: 48, right: 24, top: 26, bottom: 34, containLabel: true };
const axisStyle = { axisLine: { lineStyle: { color: 'var(--td-component-border)' } }, splitLine: { lineStyle: { color: 'var(--td-component-border)' } } };
export const OverviewCharts = ({ dashboard }: { dashboard: ChartRow }) => {
const trend = chartRows(dashboard?.trend);
const modelMetrics = chartRows(dashboard?.modelMetrics);
const channelMetrics = chartRows(dashboard?.channelMetrics);
const messageTrend = chartRows(dashboard?.messageTrend);
const paymentTrend = chartRows(dashboard?.paymentTrend);
const revenueTrend = chartRows(dashboard?.revenueTrend);
const users = numberValue(dashboard?.users);
const activeUsers = Math.min(users, Math.max(0, numberValue(dashboard?.activeUsers)));
const channels = numberValue(dashboard?.channels);
const availableChannels = Math.min(channels, Math.max(0, numberValue(dashboard?.availableChannels)));
const p50 = dashboard?.p50LatencyMs === undefined ? undefined : Math.max(0, numberValue(dashboard.p50LatencyMs));
const p95 = dashboard?.p95LatencyMs === undefined ? undefined : Math.max(0, numberValue(dashboard.p95LatencyMs));
const maxLatency = Math.max(100, p95 || 0, p50 || 0);
const activityOption = useMemo(() => users ? ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (value: number) => `${value} 人` },
grid: { ...baseGrid, left: 72, right: 30 },
xAxis: { type: 'value', max: users, ...axisStyle },
yAxis: { type: 'category', data: ['活跃用户', '其他用户'], ...axisStyle },
series: [{ type: 'bar', barWidth: 22, showBackground: true, backgroundStyle: { color: 'var(--td-bg-color-secondarycontainer)' }, data: [activeUsers, Math.max(0, users - activeUsers)], itemStyle: { borderRadius: [0, 5, 5, 0] }, label: { show: true, position: 'right', formatter: '{c} 人' } }],
}) : emptyOption('暂无用户数据'), [activeUsers, users]);
const channelOption = useMemo(() => channels ? ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (value: number) => `${value} 个` },
grid: { ...baseGrid, left: 72, right: 36, top: 48, bottom: 42 },
xAxis: { type: 'value', min: 0, max: channels, ...axisStyle },
yAxis: { type: 'category', data: ['渠道可用情况'], ...axisStyle },
series: [
{ name: '可用渠道', type: 'bar', stack: 'total', barWidth: 30, data: [availableChannels], itemStyle: { color: 'var(--td-success-color)', borderRadius: [6, 0, 0, 6] }, label: { show: true, position: 'inside', formatter: `${availableChannels} 个` } },
{ name: '不可用渠道', type: 'bar', stack: 'total', barWidth: 30, data: [Math.max(0, channels - availableChannels)], itemStyle: { color: 'var(--td-bg-color-secondarycontainer)', borderRadius: [0, 6, 6, 0] }, label: { show: true, position: 'inside', color: 'var(--td-text-color-secondary)', formatter: (params: ChartRow) => params.value ? `${params.value} 个` : '' } },
],
graphic: [{ type: 'text', left: 'center', top: 8, style: { text: `可用率 ${Math.round((availableChannels / channels) * 100)}%`, fontSize: 16, fontWeight: 600, fill: 'var(--td-text-color-primary)' } }],
}) : emptyOption('暂无渠道数据'), [availableChannels, channels]);
const latencyOption = useMemo(() => {
if (p50 === undefined && p95 === undefined) return emptyOption('暂无延迟样本');
const ring = (value: number | undefined, center: [string, string], color: string) => {
const ratio = value === undefined ? 0 : Math.min(1, Math.max(0, value / maxLatency));
return {
type: 'pie',
center,
radius: ['52%', '68%'],
startAngle: 90,
clockwise: true,
silent: true,
label: { show: false },
labelLine: { show: false },
data: [
{ value: ratio, itemStyle: { color } },
{ value: 1 - ratio, itemStyle: { color: 'var(--td-bg-color-secondarycontainer)' } },
],
};
};
const centerText = (value: number | undefined, label: string, left: string) => ({
type: 'group', left, top: 0, width: '16%', height: '100%',
children: [
{ type: 'text', left: 'center', top: '38%', style: { text: value === undefined ? '暂无样本' : `${Math.round(value)} 毫秒`, textAlign: 'center', fill: 'var(--td-text-color-primary)', fontSize: value === undefined ? 13 : 16, fontWeight: 600 } },
{ type: 'text', left: 'center', top: '61%', style: { text: label, textAlign: 'center', fill: 'var(--td-text-color-secondary)', fontSize: 12 } },
],
});
return {
tooltip: { trigger: 'item', formatter: () => '延迟数据按最近调用样本计算' },
series: [p50 === undefined ? undefined : ring(p50, ['28%', '50%'], 'var(--td-brand-color)'), p95 === undefined ? undefined : ring(p95, ['72%', '50%'], 'var(--td-warning-color)')].filter(Boolean),
graphic: [centerText(p50, 'P50', '20%'), centerText(p95, 'P95', '64%')],
};
}, [maxLatency, p50, p95]);
const trendOption = useMemo(() => trend.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['任务数', '成功率'] },
xAxis: { type: 'category', data: trend.map((item: ChartRow) => String(item.date).slice(5)), ...axisStyle },
yAxis: [{ type: 'value', name: '任务数', ...axisStyle }, { type: 'value', name: '成功率', max: 1, axisLabel: { formatter: (value: number) => `${Math.round(value * 100)}%` }, ...axisStyle }],
series: [
{ name: '任务数', type: 'bar', barMaxWidth: 26, data: trend.map((item: ChartRow) => numberValue(item.tasks)) },
{ name: '成功率', type: 'line', yAxisIndex: 1, smooth: true, data: trend.map((item: ChartRow) => numberValue(item.successRate)), symbolSize: 7 },
],
}) : emptyOption('暂无任务趋势'), [trend]);
const channelSuccessOption = useMemo(() => channelMetrics.length ? ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params: ChartRow[]) => {
const item = params?.[0];
const source = channelMetrics.find((row: ChartRow) => String(row.label) === String(item?.name));
return `${item?.name || '渠道'}:${percent(item?.value)}${source ? `(${numberValue(source.attempts)} 次调用)` : ''}`;
} },
grid: { ...baseGrid, left: 88, right: 28 },
xAxis: { type: 'value', min: 0, max: 1, axisLabel: { formatter: (value: number) => `${Math.round(value * 100)}%` }, ...axisStyle },
yAxis: { type: 'category', data: channelMetrics.map((item: ChartRow, index: number) => String(item.label || `渠道 ${index + 1}`)), ...axisStyle },
series: [{ name: '成功率', type: 'bar', barMaxWidth: 22, data: channelMetrics.map((item: ChartRow) => {
let color = 'var(--td-warning-color)';
if (item.health === 'healthy') color = 'var(--td-success-color)';
else if (['open', 'disabled'].includes(String(item.health))) color = 'var(--td-error-color)';
return { value: Math.min(1, Math.max(0, numberValue(item.successRate))), itemStyle: { color } };
}), label: { show: true, position: 'right', formatter: (params: ChartRow) => percent(params.value) } }],
}) : emptyOption('暂无来源渠道数据'), [channelMetrics]);
const messageTrendOption = useMemo(() => messageTrend.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['发送失败', '待发送', '失败率'] },
xAxis: { type: 'category', data: messageTrend.map((item: ChartRow) => String(item.date || '').slice(5)), ...axisStyle },
yAxis: [{ type: 'value', min: 0, name: '条数', ...axisStyle }, { type: 'value', min: 0, max: 1, name: '失败率', axisLabel: { formatter: (value: number) => `${Math.round(value * 100)}%` }, ...axisStyle }],
series: [{ name: '发送失败', type: 'bar', barMaxWidth: 24, data: messageTrend.map((item: ChartRow) => numberValue(item.failed)) }, { name: '待发送', type: 'bar', barMaxWidth: 24, data: messageTrend.map((item: ChartRow) => numberValue(item.pending)) }, { name: '失败率', type: 'line', yAxisIndex: 1, smooth: true, data: messageTrend.map((item: ChartRow) => numberValue(item.failureRate)), symbolSize: 7 }],
}) : emptyOption('暂无消息趋势'), [messageTrend]);
const paymentTrendOption = useMemo(() => paymentTrend.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['已处理', '支付异常', '待处理'] },
xAxis: { type: 'category', data: paymentTrend.map((item: ChartRow) => String(item.date || '').slice(5)), ...axisStyle },
yAxis: { type: 'value', min: 0, name: '事件数', ...axisStyle },
series: [{ name: '已处理', type: 'bar', barMaxWidth: 24, data: paymentTrend.map((item: ChartRow) => numberValue(item.applied)) }, { name: '支付异常', type: 'bar', barMaxWidth: 24, data: paymentTrend.map((item: ChartRow) => numberValue(item.rejected)) }, { name: '待处理', type: 'line', smooth: true, data: paymentTrend.map((item: ChartRow) => numberValue(item.pending)), symbolSize: 7 }],
}) : emptyOption('暂无支付趋势'), [paymentTrend]);
const revenueOption = useMemo(() => revenueTrend.length ? ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: baseGrid,
legend: { bottom: 0, data: ['收入(元)', '到账金币'] },
xAxis: { type: 'category', data: revenueTrend.map((item: ChartRow) => String(item.date || '').slice(5)), ...axisStyle },
yAxis: [{ type: 'value', min: 0, name: '元', ...axisStyle }, { type: 'value', min: 0, name: '金币', ...axisStyle }],
series: [
{ name: '收入(元)', type: 'bar', barMaxWidth: 24, data: revenueTrend.map((item: ChartRow) => numberValue(item.fiatAmount)) },
{ name: '到账金币', type: 'line', yAxisIndex: 1, smooth: true, symbolSize: 7, data: revenueTrend.map((item: ChartRow) => numberValue(item.balanceAmount)) },
],
}) : emptyOption('暂无收入趋势'), [revenueTrend]);
const modelOption = useMemo(() => modelMetrics.length ? ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (value: number) => percent(value) },
grid: baseGrid,
xAxis: { type: 'value', max: 1, axisLabel: { formatter: (value: number) => `${Math.round(value * 100)}%` }, ...axisStyle },
yAxis: { type: 'category', data: modelMetrics.map((item: ChartRow) => String(item.modelName || item.name || '未命名模型')), ...axisStyle },
series: [{ name: '成功率', type: 'bar', data: modelMetrics.map((item: ChartRow) => numberValue(item.successRate)), barMaxWidth: 24 }],
}) : emptyOption('暂无模型指标'), [modelMetrics]);
const statusOption = useMemo(() => {
const total = numberValue(dashboard?.tasks);
const counts = dashboard?.taskStatusCounts && typeof dashboard.taskStatusCounts === 'object' ? dashboard.taskStatusCounts as Record<string, unknown> : {};
const succeeded = numberValue(counts.succeeded ?? dashboard?.succeededTasks);
const queued = numberValue(counts.queued ?? dashboard?.queuedTasks);
const running = numberValue(counts.running);
const failed = numberValue(counts.failed);
const canceled = numberValue(counts.canceled);
const unknown = numberValue(counts.unknown);
const partial = numberValue(counts.partial);
const accounted = succeeded + queued + running + failed + canceled + unknown + partial;
const other = Math.max(0, total - accounted);
return total ? {
tooltip: { trigger: 'item', formatter: (params: ChartRow) => `${params.name}:${params.value}(${params.percent}%)` },
legend: { bottom: 0 },
series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data: [{ name: '成功', value: succeeded }, { name: '部分成功', value: partial }, { name: '排队中', value: queued }, { name: '运行中', value: running }, { name: '失败', value: failed }, { name: '已取消', value: canceled }, { name: '待对账', value: unknown }, { name: '其他', value: other }].filter((item) => item.value > 0), label: { formatter: '{b}\n{d}%' } }],
} : emptyOption('暂无任务状态');
}, [dashboard]);
return <div className={Style.chartGrid}>
<ChartPanel title='用户活跃度' option={activityOption} />
<ChartPanel title='渠道可用率' option={channelOption} />
<ChartPanel title='任务延迟' option={latencyOption} />
<ChartPanel title='近 7 日任务趋势' option={trendOption} />
<ChartPanel title='模型成功率' option={modelOption} />
<ChartPanel title='任务状态分布' option={statusOption} />
<ChartPanel title='来源渠道成功率' option={channelSuccessOption} />
<ChartPanel title='消息失败趋势' option={messageTrendOption} />
<ChartPanel title='支付异常趋势' option={paymentTrendOption} />
<ChartPanel title='收入与到账金币趋势' option={revenueOption} />
</div>;
};
export const ChannelHealthCharts = ({ rows }: { rows: ChartRow[] }) => {
const safeRows = useMemo(() => rows.filter(isChartRow), [rows]);
const channels = useMemo(() => safeRows.slice(0, 12), [safeRows]);
const performanceOption = useMemo(() => channels.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['成功率', 'P95 延迟'] },
xAxis: { type: 'category', data: channels.map((row, index) => String(row.label || `渠道 ${index + 1}`)), axisLabel: { rotate: channels.length > 7 ? 28 : 0 }, ...axisStyle },
yAxis: [{ type: 'value', max: 1, axisLabel: { formatter: (value: number) => `${Math.round(value * 100)}%` }, ...axisStyle }, { type: 'value', name: '毫秒', ...axisStyle }],
series: [{ name: '成功率', type: 'bar', data: channels.map((row) => numberValue(row.successRate)), barMaxWidth: 24 }, { name: 'P95 延迟', type: 'line', yAxisIndex: 1, data: channels.map((row) => numberValue(row.latencyP95Ms)), smooth: true, symbolSize: 7 }],
}) : emptyOption('暂无渠道健康数据'), [channels]);
const stateOption = useMemo(() => {
const stateCount = safeRows.reduce<Record<string, number>>((result, row) => {
const state = !row.lastTestedAt && row.lastTestHealthy === undefined ? 'untested' : String(row.health || 'unknown');
result[state] = (result[state] || 0) + 1;
return result;
}, {});
const labels: Record<string, string> = { healthy: '健康', degraded: '降级', open: '熔断', disabled: '已停用', untested: '未测试', unknown: '未知' };
const data = Object.entries(stateCount).map(([key, value]) => ({ name: labels[key] || '未知', value }));
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption('暂无渠道状态');
}, [safeRows]);
return <div className={Style.chartGrid}>
<ChartPanel title='渠道成功率与延迟' option={performanceOption} />
<ChartPanel title='渠道状态分布' option={stateOption} />
</div>;
};
export const TaskCharts = ({ rows }: { rows: ChartRow[] }) => {
const safeRows = useMemo(() => rows.filter(isChartRow), [rows]);
const statusLabels: Record<string, string> = { queued: '排队中', running: '运行中', succeeded: '成功', partial: '部分成功', failed: '失败', canceled: '已取消', unknown: '待对账' };
const statusOption = useMemo(() => {
const statusCount = safeRows.reduce<Record<string, number>>((result, row) => {
const key = String(row.status || 'unknown');
result[key] = (result[key] || 0) + 1;
return result;
}, {});
const data = Object.entries(statusCount).map(([key, value]) => ({ name: statusLabels[key] || '未知状态', value }));
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption('暂无任务状态');
}, [safeRows]);
const trendOption = useMemo(() => {
const days = safeRows.reduce<Record<string, { total: number; success: number; failed: number }>>((result, row) => {
const date = typeof row.createdAt === 'string' ? row.createdAt.slice(0, 10) : '未知日期';
const current = result[date] || { total: 0, success: 0, failed: 0 };
current.total += 1;
if (row.status === 'succeeded' || row.status === 'partial') current.success += 1;
if (row.status === 'failed' || row.status === 'canceled') current.failed += 1;
result[date] = current;
return result;
}, {});
const dates = Object.keys(days).sort().slice(-14);
return dates.length ? { tooltip: { trigger: 'axis' }, grid: baseGrid, legend: { bottom: 0, data: ['任务数', '成功', '失败'] }, xAxis: { type: 'category', data: dates.map((date) => date.slice(5)), ...axisStyle }, yAxis: { type: 'value', ...axisStyle }, series: [{ name: '任务数', type: 'bar', data: dates.map((date) => days[date].total), barMaxWidth: 26 }, { name: '成功', type: 'line', data: dates.map((date) => days[date].success), smooth: true }, { name: '失败', type: 'line', data: dates.map((date) => days[date].failed), smooth: true }] } : emptyOption('暂无任务趋势');
}, [safeRows]);
return <div className={Style.chartGrid}>
<ChartPanel title='任务状态分布' option={statusOption} />
<ChartPanel title='任务创建趋势' option={trendOption} />
</div>;
};
export const RealtimeMetricsCharts = ({ rows, history }: { rows: ChartRow[]; history: ChartRow[] }) => {
const safeRows = useMemo(() => rows.filter(isChartRow), [rows]);
const safeHistory = useMemo(() => history.filter(isChartRow), [history]);
const samples = useMemo(() => {
const latest = safeRows[0] || {};
if (safeHistory.length) return safeHistory;
return safeRows.length ? [{ ...latest, recordedAt: new Date().toISOString() }] : [];
}, [safeHistory, safeRows]);
const metricOption = useMemo(() => samples.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['连接数', '订阅数'] },
xAxis: { type: 'category', data: samples.map((item) => new Date(item.recordedAt || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })), ...axisStyle },
yAxis: { type: 'value', ...axisStyle },
series: [{ name: '连接数', type: 'line', data: samples.map((item) => numberValue(item.connections)), smooth: true, areaStyle: {} }, { name: '订阅数', type: 'line', data: samples.map((item) => numberValue(item.subscriptions)), smooth: true }],
}) : emptyOption('等待实时数据'), [samples]);
const queueOption = useMemo(() => samples.length ? ({
tooltip: { trigger: 'axis' },
grid: baseGrid,
legend: { bottom: 0, data: ['任务队列', '事件积压'] },
xAxis: { type: 'category', data: samples.map((item) => new Date(item.recordedAt || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })), ...axisStyle },
yAxis: { type: 'value', ...axisStyle },
series: [{ name: '任务队列', type: 'bar', data: samples.map((item) => numberValue(item.taskQueues)), barMaxWidth: 26 }, { name: '事件积压', type: 'line', data: samples.map((item) => numberValue(item.eventBacklog)), smooth: true, symbolSize: 7 }],
}) : emptyOption('等待队列数据'), [samples]);
return <div className={Style.chartGrid}>
<ChartPanel title='连接与订阅趋势' option={metricOption} />
<ChartPanel title='队列与事件积压' option={queueOption} />
</div>;
};
export const AuditDashboardCharts = ({ rows }: { rows: ChartRow[] }) => {
const safeRows = useMemo(() => rows.filter(isChartRow), [rows]);
const statusFor = (row: ChartRow) => {
const status = Number(row.statusCode);
if (status >= 400) return '异常';
return /失败|拒绝|错误|死信|failed|rejected|error|dead/i.test(`${row.action || ''} ${row.actionLabel || ''}`) ? '异常' : '正常';
};
const categoryOption = useMemo(() => {
const counts = safeRows.reduce<Record<string, number>>((result, row) => { const key = String(row.category || '其他管理'); result[key] = (result[key] || 0) + 1; return result; }, {});
const categoryLabels: Record<string, string> = { admin: '认证与安全', audit: '审计访问', user: '用户管理', channel: '渠道管理', channel_group: '渠道管理', model_product: '模型与价格', pricing: '模型与价格', plan: '套餐与权益', task: '任务处理', payment: '财务与支付', approval: '审批管理', system_settings: '系统设置', message: '消息通知', '认证与安全': '认证与安全', '审计访问': '审计访问', '用户管理': '用户管理', '渠道管理': '渠道管理', '模型与价格': '模型与价格', '套餐与权益': '套餐与权益', '任务处理': '任务处理', '财务与支付': '财务与支付', '审批管理': '审批管理', '系统设置': '系统设置', '消息通知': '消息通知' };
const data = Object.entries(counts).map(([name, value]) => ({ name: categoryLabels[name] || '其他管理', value }));
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption('暂无审计记录');
}, [safeRows]);
const statusOption = useMemo(() => {
const counts = safeRows.reduce<Record<string, number>>((result, row) => { const key = statusFor(row); result[key] = (result[key] || 0) + 1; return result; }, {});
const data = [{ name: '正常(HTTP < 400)', value: counts.正常 || 0 }, { name: '异常(HTTP >= 400 或业务错误)', value: counts.异常 || 0 }].filter((item) => item.value > 0);
return data.length ? { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '72%'], center: ['50%', '45%'], data, label: { formatter: '{b}\n{d}%' } }] } : emptyOption('暂无日志状态');
}, [safeRows]);
const summary = useMemo(() => {
const abnormal = safeRows.filter((row) => statusFor(row) === '异常').length;
return { total: safeRows.length, normal: safeRows.length - abnormal, abnormal, abnormalRate: safeRows.length ? abnormal / safeRows.length : 0 };
}, [safeRows]);
const timelineOption = useMemo(() => {
const days = safeRows.reduce<Record<string, { normal: number; error: number }>>((result, row) => {
const date = typeof row.createdAt === 'string' ? row.createdAt.slice(0, 10) : '未知日期';
const current = result[date] || { normal: 0, error: 0 };
if (statusFor(row) === '正常') current.normal += 1;
else current.error += 1;
result[date] = current;
return result;
}, {});
const dates = Object.keys(days).sort().slice(-14);
return dates.length ? { tooltip: { trigger: 'axis' }, grid: baseGrid, legend: { bottom: 0, data: ['正常 < 400', '异常 >= 400 或业务错误'] }, xAxis: { type: 'category', data: dates.map((date) => date.slice(5)), ...axisStyle }, yAxis: { type: 'value', ...axisStyle }, series: [{ name: '正常 < 400', type: 'bar', stack: 'status', data: dates.map((date) => days[date].normal), barMaxWidth: 28 }, { name: '异常 >= 400 或业务错误', type: 'bar', stack: 'status', data: dates.map((date) => days[date].error), barMaxWidth: 28 }] } : emptyOption('暂无日志趋势');
}, [safeRows]);
return <>
<div className={Style.stats}>
<Card className={Style.statCard} bordered={false}><Statistic title='日志总数' value={summary.total} /></Card>
<Card className={Style.statCard} bordered={false}><Statistic title='正常日志(HTTP < 400)' value={summary.normal} /></Card>
<Card className={Style.statCard} bordered={false}><Statistic title='异常日志(HTTP >= 400 或业务错误)' value={summary.abnormal} /></Card>
<Card className={Style.statCard} bordered={false}><Statistic title='异常占比' value={Number((summary.abnormalRate * 100).toFixed(1))} suffix='%' /></Card>
</div>
<div className={Style.chartGrid}>
<ChartPanel title='日志分类占比' option={categoryOption} />
<ChartPanel title='正常与异常日志' option={statusOption} />
<ChartPanel title='日志数量趋势' option={timelineOption} />
</div>
</>;
};
@@ -0,0 +1 @@
export { AuditDashboardCharts, ChannelHealthCharts, OverviewCharts, RealtimeMetricsCharts, TaskCharts } from './Charts';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-261
View File
@@ -1,261 +0,0 @@
import type { EChartOption } from 'echarts';
import { getChartDataSet, ONE_WEEK_LIST } from 'utils/chart';
export const getLineChartOptions = (dateTime: Array<string> = []): EChartOption => {
const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
return {
tooltip: {
trigger: 'item',
},
grid: {
left: '0',
right: '20px',
top: '5px',
bottom: '36px',
containLabel: true,
},
legend: {
left: 'center',
bottom: '0',
orient: 'horizontal', // legend 横向布局。
data: ['本月', '上月'],
textStyle: {
fontSize: 12,
},
},
xAxis: {
type: 'category',
data: timeArray,
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#E3E6EB',
width: 1,
},
},
},
yAxis: {
type: 'value',
},
series: [
{
name: '本月',
data: outArray,
type: 'line',
smooth: false,
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
borderWidth: 1,
},
areaStyle: {
color: '#0053D92F',
},
},
{
name: '上月',
data: inArray,
type: 'line',
smooth: false,
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
borderWidth: 1,
},
},
],
};
};
export const getPieChartOptions = (radius = 42): EChartOption => ({
tooltip: {
trigger: 'item',
},
grid: {
top: '0',
right: '0',
},
legend: {
itemWidth: 12,
itemHeight: 4,
textStyle: {
fontSize: 12,
},
left: 'center',
bottom: '0',
orient: 'horizontal', // legend 横向布局。
},
series: [
{
name: '销售渠道',
type: 'pie',
radius: ['48%', '60%'],
avoidLabelOverlap: false,
silent: true,
itemStyle: {
borderWidth: 1,
},
label: {
show: true,
position: 'center',
formatter: ['{value|{d}%}', '{name|{b}渠道占比}'].join('\n'),
rich: {
value: {
fontSize: 28,
fontWeight: 'normal',
lineHeight: 46,
},
name: {
color: '#909399',
fontSize: 12,
lineHeight: 14,
},
},
},
labelLine: {
show: false,
},
data: [
{ value: 1048, name: '线上' },
{ value: radius * 7, name: '门店' },
],
},
],
});
export const getBarChartOptions = (dateTime: Array<string> = []): EChartOption => {
const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
return {
tooltip: {
trigger: 'item',
},
xAxis: {
type: 'category',
data: timeArray,
axisLine: {
lineStyle: {
width: 1,
},
},
},
yAxis: {
type: 'value',
},
grid: {
top: '5%',
left: '25px',
right: 0,
bottom: '60px',
},
legend: {
icon: 'rect',
itemWidth: 12,
itemHeight: 4,
itemGap: 48,
textStyle: {
fontSize: 12,
color: 'rgba(0, 0, 0, 0.6)',
},
left: 'center',
bottom: '0',
orient: 'horizontal',
data: ['本月', '上月'],
},
series: [
{
name: '本月',
data: outArray,
type: 'bar',
},
{
name: '上月',
data: inArray,
type: 'bar',
},
],
};
};
// PieChartIcon Data
export const MICRO_CHART_OPTIONS_LINE: EChartOption = {
xAxis: {
type: 'category',
show: false,
data: ONE_WEEK_LIST,
},
yAxis: {
show: false,
type: 'value',
},
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0,
tooltip: {
show: false,
},
},
color: ['#fff'],
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line',
showSymbol: false,
},
],
};
// BarChartIcon Data
export const MICRO_CHART_OPTIONS_BAR: EChartOption = {
xAxis: {
type: 'category',
show: false,
data: ONE_WEEK_LIST,
},
yAxis: {
show: false,
type: 'value',
},
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0,
tooltip: {
show: false,
},
},
series: [
{
data: [
100,
130,
184,
218,
{
value: 135,
itemStyle: {
opacity: 0.2,
},
},
{
value: 118,
itemStyle: {
opacity: 0.2,
},
},
{
value: 60,
itemStyle: {
opacity: 0.2,
},
},
],
type: 'bar',
barWidth: 9,
},
],
};
@@ -1,3 +0,0 @@
.middleChartPanel {
margin-top: 16px;
}
@@ -1,47 +0,0 @@
import React, { useState } from 'react';
import { Col, Row, Card } from 'tdesign-react';
import ReactEcharts from 'echarts-for-react';
import useDynamicChart from 'hooks/useDynamicChart';
import LastWeekDatePicker from 'components/DatePicker';
import { getLineChartOptions, getPieChartOptions } from '../chart';
import Style from './MiddleChart.module.less';
const lineOptions = getLineChartOptions();
const pieOptions = getPieChartOptions();
const MiddleChart = () => {
const [customOptions, setCustomOptions] = useState(lineOptions);
const onTimeChange = (value: Array<string>) => {
const options = getLineChartOptions(value);
setCustomOptions(options);
};
const dynamicLineChartOption = useDynamicChart(customOptions, {
placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
borderColor: ['series.0.itemStyle.borderColor', 'series.1.itemStyle.borderColor'],
});
const dynamicPieChartOption = useDynamicChart(pieOptions, {
placeholderColor: ['legend.textStyle.color'],
containerColor: ['series.0.itemStyle.borderColor'],
textColor: ['label.color', 'label.color'],
});
return (
<Row gutter={[16, 16]} className={Style.middleChartPanel}>
<Col xs={12} xl={9}>
<Card title='统计数据' subtitle='(万元)' actions={LastWeekDatePicker(onTimeChange)} bordered={false}>
<ReactEcharts option={dynamicLineChartOption} notMerge={true} lazyUpdate={false} />
</Card>
</Col>
<Col xs={12} xl={3}>
<Card title='销售渠道' subtitle='2021-12' bordered={false}>
<ReactEcharts option={dynamicPieChartOption} notMerge={true} lazyUpdate={true} />
</Card>
</Col>
</Row>
);
};
export default React.memo(MiddleChart);
@@ -1,5 +0,0 @@
.overviewPanel {
background-color: var(--td-bg-color-container);
border-radius: 3px;
margin-top: 16px;
}
@@ -1,69 +0,0 @@
import React, { useState } from 'react';
import { Button, Col, Row, Card } from 'tdesign-react';
import ReactEcharts from 'echarts-for-react';
import useDynamicChart from 'hooks/useDynamicChart';
import Board, { ETrend } from 'components/Board';
import LastWeekDatePicker from 'components/DatePicker';
import { getBarChartOptions } from '../chart';
import Style from './Overview.module.less';
const options = getBarChartOptions();
const Overview = (): React.ReactElement => {
const [customOptions, setCustomOptions] = useState(options);
const onTimeChange = (value: any) => {
// eslint-disable-next-line no-shadow
const options = getBarChartOptions(value);
setCustomOptions(options);
};
const dynamicChartOption = useDynamicChart(customOptions, {
placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
});
return (
<div className={Style.overviewPanel}>
<Row>
<Col xs={12} xl={9} span={12}>
<Card title=' 出入库概览 ' subtitle='(件)' actions={LastWeekDatePicker(onTimeChange)} bordered={false}>
<ReactEcharts
option={dynamicChartOption} // option:图表配置项
notMerge={true}
lazyUpdate={true}
style={{ height: 351 }}
/>
</Card>
</Col>
<Col xs={12} xl={3} span={12}>
<Card actions={<Button>导出数据</Button>} bordered={false}>
<Row>
<Col xl={12} xs={6} span={12}>
<Board
title='本月出库总计(件)'
count='1726'
trend={ETrend.down}
trendNum='20.3%'
desc='自从上周以来'
border={false}
style={{ height: '168px' }}
/>
</Col>
<Col xl={12} xs={6} span={12}>
<Board
title='活跃用户(个)'
count='1126'
trend={ETrend.down}
trendNum='20.5%'
desc='自从上周以来'
border={false}
style={{ height: '168px' }}
/>
</Col>
</Row>
</Card>
</Col>
</Row>
</div>
);
};
export default React.memo(Overview);
@@ -1,19 +0,0 @@
.rankListPanel {
margin-top: 16px;
}
.rankIndex {
display: inline-flex;
width: 24px;
height: 24px;
border-radius: 50%;
color: #fff;
font-size: 14px;
background-color: var(--td-gray-color-5);
align-items: center;
justify-content: center;
font-weight: 700;
}
.rankIndexTop {
background: var(--td-brand-color);
}
@@ -1,133 +0,0 @@
import React from 'react';
import { Col, Radio, Row, Table, Button, Card } from 'tdesign-react';
import { TdPrimaryTableProps } from 'tdesign-react/es/table';
import classnames from 'classnames';
import { TrendIcon, ETrend } from 'components/Board';
import { PURCHASE_TREND_LIST, SALE_TREND_LIST } from '../constant';
import Style from './RankList.module.less';
const DateRadioGroup = (
<Radio.Group defaultValue='recent_week'>
<Radio.Button value='recent_week'>本周</Radio.Button>
<Radio.Button value='recent_month'>近三个月</Radio.Button>
</Radio.Group>
);
const SALE_COLUMNS: TdPrimaryTableProps['columns'] = [
{
align: 'center',
colKey: 'index',
title: '排名',
width: 80,
fixed: 'left',
cell: ({ rowIndex }) => (
<span className={classnames(Style.rankIndex, { [Style.rankIndexTop]: rowIndex < 3 })}>{rowIndex + 1}</span>
),
},
{
align: 'left',
ellipsis: true,
colKey: 'productName',
title: '客户名称',
width: 200,
},
{
align: 'center',
colKey: 'growUp',
width: 100,
title: '较上周',
cell: ({ row }) => <TrendIcon trend={row.growUp < 0 ? ETrend.down : ETrend.up} trendNum={Math.abs(row.growUp)} />,
},
{
align: 'center',
colKey: 'count',
title: '订单量',
width: 100,
},
{
align: 'center',
colKey: 'date',
width: 140,
title: '合同签订日期',
},
{
align: 'center',
colKey: 'operation',
fixed: 'right',
title: '操作',
width: 80,
cell: ({ row }) => (
<Button variant='text' theme='primary' onClick={() => console.log(row)}>
操作
</Button>
),
},
];
const PURCHASE_COLUMNS: TdPrimaryTableProps['columns'] = [
{
align: 'center',
colKey: 'index',
title: '排名',
width: 80,
fixed: 'left',
cell: ({ rowIndex }) => (
<span className={classnames(Style.rankIndex, { [Style.rankIndexTop]: rowIndex < 3 })}>{rowIndex + 1}</span>
),
},
{
align: 'left',
ellipsis: true,
colKey: 'productName',
title: '供应商名称',
width: 200,
},
{
align: 'center',
colKey: 'growUp',
width: 100,
title: '较上周',
cell: ({ row }) => <TrendIcon trend={row.growUp < 0 ? ETrend.down : ETrend.up} trendNum={Math.abs(row.growUp)} />,
},
{
align: 'center',
colKey: 'count',
title: '订单量',
width: 100,
},
{
align: 'center',
colKey: 'date',
width: 140,
title: '合同签订日期',
},
{
align: 'center',
colKey: 'operation',
title: '操作',
fixed: 'right',
width: 80,
cell: ({ row }) => (
<Button variant='text' theme='primary' onClick={() => console.log(row)}>
操作
</Button>
),
},
];
const RankList = () => (
<Row gutter={[16, 16]} className={Style.rankListPanel}>
<Col xs={12} xl={6} span={12}>
<Card title='销售订单排名' actions={DateRadioGroup} bordered={false}>
<Table columns={SALE_COLUMNS} rowKey='productName' size='medium' data={SALE_TREND_LIST} />
</Card>
</Col>
<Col xs={12} xl={6} span={12}>
<Card title='采购订单排名' actions={DateRadioGroup} bordered={false}>
<Table columns={PURCHASE_COLUMNS} rowKey='productName' size='medium' data={PURCHASE_TREND_LIST} />
</Card>
</Col>
</Row>
);
export default React.memo(RankList);
@@ -1,35 +0,0 @@
.iconWrap {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
background: var(--td-brand-color-1);
border-radius: 50%;
position: absolute;
top: 32px;
right: 32px;
}
.svgIcon {
font-size: 24px;
color: var(--td-brand-color);
}
.paneLineChart {
width: 103px;
height: 56px;
overflow: hidden;
position: absolute;
top: 55px;
right: 32px;
}
.paneBarChart {
width: 103px;
height: 36px;
-webkit-tap-highlight-color: transparent;
user-select: none;
position: absolute;
top: 55px;
right: 32px;
}
@@ -1,92 +0,0 @@
import React, { memo } from 'react';
import { Col, Row } from 'tdesign-react';
import { UsergroupIcon, FileIcon } from 'tdesign-icons-react';
import ReactEcharts from 'echarts-for-react';
import Board, { ETrend, IBoardProps } from 'components/Board';
import useDynamicChart from 'hooks/useDynamicChart';
import { MICRO_CHART_OPTIONS_BAR, MICRO_CHART_OPTIONS_LINE } from '../chart';
import Style from './TopPanel.module.less';
const BarChartIcon = memo(() => {
const dynamicChartOption = useDynamicChart(MICRO_CHART_OPTIONS_BAR);
return (
<div className={Style.paneBarChart}>
<ReactEcharts
option={dynamicChartOption} // option:图表配置项
notMerge={true}
lazyUpdate={true}
style={{ height: 36 }}
/>
</div>
);
});
const PieChartIcon = memo(() => (
<div className={Style.paneLineChart}>
<ReactEcharts
option={MICRO_CHART_OPTIONS_LINE} // option:图表配置项
notMerge={true}
lazyUpdate={true}
style={{ height: 56 }}
/>
</div>
));
const PANE_LIST: Array<IBoardProps> = [
{
title: '总收入',
count: '¥ 28,425.00',
trend: ETrend.up,
trendNum: '20.5%',
Icon: <PieChartIcon />,
},
{
title: '总退款',
count: '¥ 768.00',
trend: ETrend.down,
trendNum: '20.5%',
Icon: <BarChartIcon />,
},
{
title: '活跃用户(个)',
count: '1126',
trend: ETrend.down,
trendNum: '20.5%',
Icon: (
<div className={Style.iconWrap}>
<UsergroupIcon className={Style.svgIcon} />
</div>
),
},
{
title: '产生订单(个)',
count: '527',
trend: ETrend.down,
trendNum: '20.5%',
Icon: (
<div className={Style.iconWrap}>
<FileIcon className={Style.svgIcon} />
</div>
),
},
];
const TopPanel = () => (
<Row gutter={[16, 16]}>
{PANE_LIST.map((item, index) => (
<Col key={item.title} xs={6} xl={3}>
<Board
title={item.title}
trend={item.trend}
trendNum={item.trendNum}
count={item.count}
desc={'自从上周以来'}
Icon={item.Icon}
dark={index === 0}
/>
</Col>
))}
</Row>
);
export default React.memo(TopPanel);
@@ -1,77 +0,0 @@
export const SALE_TREND_LIST = [
{
growUp: 1,
productName: '国家电网有限公司',
count: 7059,
date: '2021-09-01',
},
{
growUp: -1,
productName: '深圳燃气集团股份有限公司',
count: 6437,
date: '2021-09-01',
},
{
growUp: 4,
productName: '国家烟草专卖局',
count: 4221,
date: '2021-09-01',
},
{
growUp: 3,
productName: '中国电信集团有限公司',
count: 3317,
date: '2021-09-01',
},
{
growUp: -3,
productName: '中国移动通信集团有限公司',
count: 3015,
date: '2021-09-01',
},
{
growUp: -3,
productName: '新余市办公用户采购项目',
count: 2015,
date: '2021-09-12',
},
];
export const PURCHASE_TREND_LIST = [
{
growUp: 1,
productName: '腾讯科技(深圳)有限公司',
count: 3015,
date: '2021-09-01',
},
{
growUp: -1,
productName: '大润发有限公司',
count: 2015,
date: '2021-09-01',
},
{
growUp: 6,
productName: '四川海底捞股份有限公司',
count: 1815,
date: '2021-09-11',
},
{
growUp: -3,
productName: '索尼(中国)有限公司',
count: 1015,
date: '2021-09-21',
},
{
growUp: -4,
productName: '松下电器(中国)有限公司',
count: 445,
date: '2021-09-19',
},
{
growUp: -3,
productName: '新余市办公用户采购项目',
count: 2015,
date: '2021-09-12',
},
];
-16
View File
@@ -1,16 +0,0 @@
import React, { memo } from 'react';
import TopPanel from './components/TopPanel';
import MiddleChart from './components/MiddleChart';
import RankList from './components/RankList';
import Overview from './components/Overview';
const DashBoard = () => (
<div style={{ overflowX: 'hidden' }}>
<TopPanel />
<MiddleChart />
<RankList />
<Overview />
</div>
);
export default memo(DashBoard);
-203
View File
@@ -1,203 +0,0 @@
import type { EChartOption } from 'echarts';
import { ONE_WEEK_LIST, getChartDataSet, getTimeArray, getRandomInt } from 'utils/chart';
// line chart options
export const getLineChartOptions = (dateTime: Array<string> = []): EChartOption => {
let dateArray: Array<string> = ONE_WEEK_LIST;
if (dateTime.length > 0) {
const dividedNum = 7;
dateArray = getTimeArray(dateTime, dividedNum, 'YYYY-MM-DD');
}
return {
grid: {
top: '5%',
right: '10px',
left: '30px',
bottom: '60px',
},
legend: {
left: 'center',
bottom: '0',
orient: 'horizontal', // legend 横向布局。
data: ['杯子', '茶叶', '蜂蜜', '面粉'],
},
xAxis: {
type: 'category',
data: dateArray,
boundaryGap: false,
axisLabel: {
color: 'rgba(0, 0, 0, 0.4)',
},
axisLine: {
lineStyle: {
color: '#E3E6EB',
width: 1,
},
},
},
yAxis: {
type: 'value',
axisLabel: {
color: 'rgba(0, 0, 0, 0.4)',
},
},
tooltip: {
trigger: 'item',
},
series: [
{
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
name: '杯子',
stack: '总量',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
itemStyle: {
borderColor: '#ffffff',
borderWidth: 1,
},
},
{
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
name: '茶叶',
stack: '总量',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
itemStyle: {
borderColor: '#ffffff',
borderWidth: 1,
},
},
{
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
name: '蜂蜜',
stack: '总量',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
itemStyle: {
borderColor: '#ffffff',
borderWidth: 1,
},
},
{
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
name: '面粉',
stack: '总量',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
itemStyle: {
borderColor: '#ffffff',
borderWidth: 1,
},
},
],
};
};
export const getScatterChartOptions = (dateTime: Array<string> = []): EChartOption => {
const [timeArray, inArray, outArray] = getChartDataSet(dateTime);
return {
xAxis: {
data: timeArray,
axisLabel: {
color: 'rgba(0, 0, 0, 0.4)',
},
splitLine: { show: false },
axisLine: {
lineStyle: {
color: '#E3E6EB',
width: 1,
},
},
},
yAxis: {
type: 'value',
// splitLine: { show: false},
axisLabel: {
color: 'rgba(0, 0, 0, 0.4)',
},
nameTextStyle: {
padding: [0, 0, 0, 60],
},
axisTick: {
show: false,
},
axisLine: {
show: false,
},
},
tooltip: {
trigger: 'item',
},
grid: {
top: '5px',
left: '25px',
right: '5px',
bottom: '60px',
},
legend: {
left: 'center',
bottom: '0',
orient: 'horizontal', // legend 横向布局。
data: ['按摩仪', '咖啡机'],
itemHeight: 8,
itemWidth: 8,
},
series: [
{
name: '按摩仪',
symbolSize: 10,
data: outArray,
type: 'scatter',
},
{
name: '咖啡机',
symbolSize: 10,
data: inArray,
type: 'scatter',
},
],
};
};
@@ -1,25 +0,0 @@
import React from 'react';
import { Col, Row, Card } from 'tdesign-react';
import Board from 'components/Board';
import { PANE_LIST } from '../constant';
const MonthPurchase = () => (
<Card title='本月采购申请情况' bordered={false}>
<Row gutter={[16, 16]}>
{PANE_LIST.map((item) => (
<Col key={item.title} xs={6} xl={3} span={12}>
<Board
title={item.title}
trend={item.trend}
trendNum={item.trendNum}
count={item.count}
desc={'环比'}
border
/>
</Col>
))}
</Row>
</Card>
);
export default React.memo(MonthPurchase);
@@ -1,56 +0,0 @@
.purchaseTrendPanel {
margin-top: 16px;
}
.productTrendPanel {
display: flex;
flex-direction: column;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
color: var(--td-text-color-secondary);
background: var(--td-bg-color-container);
padding: 24px 32px;
}
.productLogo {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: var(--td-brand-color-1);
font-size: 32px;
color: var(--td-brand-color);
}
.productName {
margin: 24px 0 8px;
font-size: 16px;
font-weight: 400;
color: var(--td-text-color-primary);
}
.productDesc {
font-size: 12px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 24px;
height: 40px;
}
.iconWrap {
position: relative;
.lightBtn {
position: absolute;
left: 22px;
background-color: var(--td-brand-color-2);
--ripple-color: var(--td-brand-color-2);
color: var(--td-brand-color);
}
}
@@ -1,101 +0,0 @@
import React, { useState } from 'react';
import { Col, Dropdown, Row, Tag, Card, Avatar } from 'tdesign-react';
import { Icon, AddIcon } from 'tdesign-icons-react';
import ReactEcharts from 'echarts-for-react';
import LastWeekDatePicker from 'components/DatePicker';
import useDynamicChart from 'hooks/useDynamicChart';
import { PRODUCT_LIST } from '../constant';
import { getLineChartOptions } from '../chart';
import Style from './PurchaseThrend.module.less';
interface IProductTrendProps {
description: string;
isSetup: boolean;
name: string;
type: string;
icon: string;
}
const ProductTrend = ({ type, isSetup, description, name, icon }: IProductTrendProps) => (
<div className={Style.productTrendPanel}>
<Row justify='space-between'>
<div className={Style.productLogo}>
<Icon name={icon} />
</div>
<Tag theme='success'>{isSetup ? '已启用' : '已停用'}</Tag>
</Row>
<p className={Style.productName}>{name}</p>
<p className={Style.productDesc}>{description}</p>
<Row justify='space-between' align='middle'>
<Avatar.Group cascading='left-up' max={2}>
<Avatar>{type}</Avatar>
<Avatar icon={<AddIcon />}></Avatar>
</Avatar.Group>
<Dropdown
disabled={!isSetup}
options={[
{
content: '管理',
value: 'manage',
onClick: () => {},
},
{
content: '删除',
value: 'delete',
onClick: () => {},
},
]}
>
<Icon name='more' />
</Dropdown>
</Row>
</div>
);
const options = getLineChartOptions();
const PurchaseTrend = () => {
const [customOptions, setCustomOptions] = useState(options);
const onTimeChange = (value: Array<string>) => {
const options = getLineChartOptions(value);
setCustomOptions(options);
};
const dynamicChartOptions = useDynamicChart(customOptions, {
placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
});
return (
<Row className={Style.purchaseTrendPanel} gutter={[16, 16]}>
<Col xs={12} xl={9}>
<Card title='采购商品申请趋势' subtitle='(件)' actions={LastWeekDatePicker(onTimeChange)} bordered={false}>
<ReactEcharts
option={dynamicChartOptions} // option:图表配置项
notMerge={true}
lazyUpdate={true}
style={{ height: 453 }}
/>
</Card>
</Col>
<Col xs={12} xl={3}>
<Row gutter={[16, 16]}>
{PRODUCT_LIST.map((item, index) => (
<Col xs={12} key={index}>
<ProductTrend
type={item.type}
isSetup={item.isSetup}
description={item.description}
name={item.name}
icon={item.icon}
/>
</Col>
))}
</Row>
</Col>
</Row>
);
};
export default React.memo(PurchaseTrend);
@@ -1,14 +0,0 @@
.satisfactionPanel {
margin-top: 16px;
}
.operation {
display: flex;
}
.exportBtn {
margin-left: 8px;
}
@@ -1,44 +0,0 @@
import React, { useState } from 'react';
import { Button, Card } from 'tdesign-react';
import ReactEcharts from 'echarts-for-react';
import LastWeekDatePicker from 'components/DatePicker';
import useDynamicChart from 'hooks/useDynamicChart';
import { getScatterChartOptions } from '../chart';
import Style from './Satisfaction.module.less';
const Satisfaction = () => {
const options = getScatterChartOptions();
const [customOptions, setCustomOptions] = useState(options);
const onTimeChange = (value: Array<string>) => {
const options = getScatterChartOptions(value);
setCustomOptions(options);
};
const dynamicChartOption = useDynamicChart(customOptions, {
placeholderColor: ['legend.textStyle.color', 'xAxis.axisLabel.color', 'yAxis.axisLabel.color'],
});
return (
<div className={Style.satisfactionPanel}>
<Card
title='采购商品满意度分布'
actions={
<div className={Style.operation}>
{LastWeekDatePicker(onTimeChange)}
<Button className={Style.exportBtn}>导出数据</Button>
</div>
}
bordered={false}
>
<ReactEcharts
option={dynamicChartOption} // option:图表配置项
notMerge={true}
lazyUpdate={true}
style={{ height: 374 }}
/>
</Card>
</div>
);
};
export default React.memo(Satisfaction);
@@ -1,59 +0,0 @@
import { IBoardProps, ETrend } from 'components/Board';
export const PANE_LIST: Array<IBoardProps> = [
{
title: '总申请数(次)',
count: '1126',
trendNum: '10%',
trend: ETrend.up,
},
{
title: '供应商数量(个)',
count: '13',
trendNum: '13%',
trend: ETrend.down,
},
{
title: '采购商品品类(类)',
count: '4',
trendNum: '10%',
trend: ETrend.up,
},
{
title: '申请人数量(人)',
count: '90',
trendNum: '44%',
trend: ETrend.down,
},
{
title: '申请完成率(%)',
count: '80.5',
trendNum: '70%',
trend: ETrend.up,
},
{
title: '到货及时率(%)',
count: '78',
trendNum: '16%',
trend: ETrend.up,
},
];
export const PRODUCT_LIST = [
{
description: 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部',
index: 1,
isSetup: true,
name: 'SSL证书',
type: 'D',
icon: 'user',
},
{
description: 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部',
index: 1,
isSetup: true,
name: 'SSL证书',
type: 'C',
icon: 'calendar',
},
];
@@ -1,12 +0,0 @@
import React, { memo } from 'react';
import MonthPurchase from './components/MonthPurchase';
import PurchaseTrend from './components/PurchaseTrend';
import PurchaseSatisfaction from './components/Satisfaction';
export default memo(() => (
<div>
<MonthPurchase />
<PurchaseTrend />
<PurchaseSatisfaction />
</div>
));
@@ -1,41 +0,0 @@
.infoBox {
column-count: 2;
.infoBoxItem {
padding: 12px 0;
display: flex;
color: var(--td-text-color-primary);
h1 {
font-weight: 400;
font-size: 14px;
color: var(--td-text-color-secondary);
text-align: left;
line-height: 22px;
margin: 0;
}
span {
margin-left: 24px;
font-size: 14px;
line-height: 1.5;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
i {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--td-success-color-5);
margin-right: 5px;
}
.inProgress {
color: var(--td-success-color-5);
}
.pdf {
color: var(--td-brand-color);
&:hover {
cursor: pointer;
}
}
}
}
@@ -1,28 +0,0 @@
import React from 'react';
import { Card } from 'tdesign-react';
import classnames from 'classnames';
import { dataInfo } from '../consts';
import Style from './Base.module.less';
const Base = () => (
<Card title='基本信息' bordered={false}>
<div className={Style.infoBox}>
{dataInfo.map((item) => (
<div key={item.id} className={Style.infoBoxItem}>
<h1>{item.name}</h1>
<span
className={classnames({
[Style.inProgress]: item.type === 'status',
[Style.pdf]: item.type === 'link',
})}
>
{item.type === 'status' && <i />}
{item.value}
</span>
</div>
))}
</div>
</Card>
);
export default React.memo(Base);
@@ -1,110 +0,0 @@
import React from 'react';
import { Button, Table, Tag, Card } from 'tdesign-react';
import { dataBuyList, total } from '../consts';
import Style from '../index.module.less';
const Detail = () => (
<Card title='产品采购明细' className={Style.cardBox} bordered={false}>
<Table
data={dataBuyList}
columns={[
{
align: 'left',
width: 300,
ellipsis: true,
colKey: 'number',
title: '申请号',
},
{
align: 'left',
width: 200,
ellipsis: true,
colKey: 'name',
title: '产品名称',
cell({ row }) {
return (
<>
{row.name}
<Tag theme='default' size='small'>
{row.tag}
</Tag>
</>
);
},
},
{
align: 'left',
width: 200,
ellipsis: true,
colKey: 'code',
title: '产品编号',
},
{
align: 'left',
width: 100,
ellipsis: true,
colKey: 'amount',
title: '采购数量',
cell({ row }) {
return row.amount;
},
},
{
align: 'left',
width: 200,
ellipsis: true,
colKey: 'department',
title: '申请部门',
},
{
align: 'left',
width: 500,
ellipsis: true,
colKey: 'createtime',
title: '创建时间',
},
{
align: 'left',
fixed: 'right',
width: 300,
colKey: 'op',
title: '操作',
cell() {
return (
<>
<Button theme='primary' variant='text'>
管理
</Button>
<Button theme='primary' variant='text'>
删除
</Button>
</>
);
},
},
]}
rowKey='id'
verticalAlign='top'
hover
pagination={{
defaultPageSize: 10,
total,
defaultCurrent: 1,
showJumper: true,
onChange(pageInfo) {
console.log(pageInfo, 'onChange pageInfo');
},
onCurrentChange(current, pageInfo) {
console.log(current, 'onCurrentChange current');
console.log(pageInfo, 'onCurrentChange pageInfo');
},
onPageSizeChange(size, pageInfo) {
console.log(size, 'onPageSizeChange size');
console.log(pageInfo, 'onPageSizeChange pageInfo');
},
}}
/>
</Card>
);
export default React.memo(Detail);
@@ -1,41 +0,0 @@
import React from 'react';
import { Col, Row, Card } from 'tdesign-react';
import ProductCard from './ProductCard';
import Style from '../index.module.less';
const Product = () => (
<Card title='产品目录' className={Style.cardBox} bordered={false}>
<div>
<Row gutter={[16, 16]}>
<Col xs={12} xl={4}>
<ProductCard isAdd={true} />
</Col>
<Col xs={12} xl={4}>
<ProductCard
title='MacBook Pro 2021'
tags={['13.3 英寸', 'Apple M1', 'RAM 16GB']}
desc='最高可选配 16GB 内存 · 最高可选配 2TB 存储设备 电池续航最长达 18 小时'
percent='1420 / 1500(台)'
Icon='cart'
progress={(1420 / 1500) * 100}
trackColor='#D4E3FC'
/>
</Col>
<Col xs={12} xl={4}>
<ProductCard
title='Surface Laptop Go'
tags={['12.4 英寸', 'Core i7', 'RAM 16GB']}
desc='常规使用 Surface,续航时间最长可达13小时 随时伴您工作'
percent='120 / 2000(台)'
Icon='cart'
progress={(120 / 2000) * 100}
color='#E24D59'
trackColor='#FCD4D4'
/>
</Col>
</Row>
</div>
</Card>
);
export default React.memo(Product);
@@ -1,108 +0,0 @@
.productionCard {
position: relative;
background-color: var(--td-bg-color-container);
border: 1px solid var(--td-component-border);
border-radius: 3px;
padding: 20px 32px 24px;
height: 212px;
box-sizing: content-box;
}
.productionTitle {
margin-bottom: 25px;
position: relative;
}
.title {
font-weight: 500;
font-size: 24px;
color: var(--td-text-color-primary);
}
.productionIcon {
position: absolute;
top: 0px;
right: 0px;
background: var(--td-brand-color-1);
color: var(--td-brand-color);
font-size: 56px;
padding: 14px;
border-radius: 100%;
}
.tag {
margin-right: 4px;
margin-top: 8px;
margin-left: unset;
border: unset;
}
.item {
position: relative;
padding-top: 8px;
padding-bottom: 8px;
}
.info {
display: inline-block;
width: 60%;
text-align: left;
font-size: 14px;
color: var(--td-text-color-placeholder);
}
.icon {
position: absolute;
bottom: 8px;
right: 0;
color: rgba(0, 0, 0, 0.26);
}
.footer {
position: absolute;
width: 100%;
bottom: 0px;
left: 0;
.percent {
position: absolute;
bottom: 15px;
right: 32px;
color: var(--td-text-color-placeholder);
}
.progress {
margin: 0;
}
}
.productionAdd {
position: relative;
background-color: var(--td-bg-color-container);
border: 1px dashed var(--td-component-border);
border-radius: 3px;
padding: 20px 32px 24px;
height: 212px;
box-sizing: content-box;
display: flex;
justify-content: center;
align-items: center;
}
.productionAddBtn {
font-size: 14px;
color: var(--td-text-color-secondary);
margin: 0 auto;
text-align: center;
line-height: 22px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
.productionAddIcon {
background: var(--td-brand-color-1);
color: var(--td-brand-color);
font-size: 33px;
padding: 8px;
border-radius: 100%;
margin-bottom: 12px;
}
}
@@ -1,55 +0,0 @@
import React from 'react';
import { Progress, Tag } from 'tdesign-react';
import { AddIcon, ChevronRightIcon, Icon } from 'tdesign-icons-react';
import Styles from './ProductCard.module.less';
interface IProps {
isAdd?: boolean;
title?: string;
tags?: string[];
desc?: string;
progress?: number;
percent?: string;
Icon?: string;
color?: string;
trackColor?: string;
}
const ProductCard = (props: IProps) => (
<div className={Styles.productCard}>
{props.isAdd ? (
<div className={Styles.productionAdd}>
<div className={Styles.productionAddBtn}>
<AddIcon className={Styles.productionAddIcon} />
<span>新增产品</span>
</div>
</div>
) : (
<div className={Styles.productionCard}>
<div className={Styles.productionTitle}>
<Icon name={props.Icon} className={Styles.productionIcon} />
<div className={Styles.title}>{props.title}</div>
<div className={Styles.tags}>
{props?.tags?.map((tag, index) => (
<Tag key={index} className={Styles.tag} theme='success' variant='dark' size='small'>
{tag}
</Tag>
))}
</div>
</div>
<div className={Styles.item}>
<span className={Styles.info}>{props.desc}</span>
<ChevronRightIcon className={Styles.icon} />
</div>
<div className={Styles.footer}>
<span className={Styles.percent}>{props.percent}</span>
<div className={Styles.progress}>
<Progress percentage={props.progress} label={false} color={props.color} trackColor={props.trackColor} />
</div>
</div>
</div>
)}
</div>
);
export default React.memo(ProductCard);
@@ -1,18 +0,0 @@
import React from 'react';
import { Steps, Card } from 'tdesign-react';
import { dataStep, stepCurrent } from '../consts';
import Style from '../index.module.less';
const { StepItem } = Steps;
const Progress = () => (
<Card title='发票进度' className={Style.cardBox} bordered={false}>
<Steps current={stepCurrent}>
{dataStep.map((item) => (
<StepItem key={item.id} title={item.name} content={item.detail} />
))}
</Steps>
</Card>
);
export default React.memo(Progress);
-54
View File
@@ -1,54 +0,0 @@
// Mock Data of 基本信息
interface InfoItem {
id: number;
name: string;
value: string;
type?: string;
}
export const dataInfo: InfoItem[] = [
{ id: 1, name: '合同名称', value: '总部办公用品采购项目' },
{ id: 2, name: '合同状态', value: '履行中', type: 'status' },
{ id: 3, name: '合同编号', value: 'BH00010' },
{ id: 4, name: '合同类型', value: '主合同' },
{ id: 5, name: '合同收付类型', value: '付款' },
{ id: 6, name: '合同金额', value: '5,000,000元' },
{ id: 7, name: '甲方', value: '腾讯科技(深圳)有限公司' },
{ id: 8, name: '乙方', value: '欧尚' },
{ id: 9, name: '合同签订日期', value: '2020-12-20' },
{ id: 10, name: '合同生效日期', value: '2021-01-20' },
{ id: 11, name: '合同结束日期', value: '2022-12-20' },
{ id: 12, name: '合同附件', value: '总部办公用品采购项目合同.pdf', type: 'link' },
{ id: 13, name: '备注', value: '--' },
{ id: 14, name: '创建时间', value: '2020-12-22 10:00:00' },
];
// Mock Data of 变更记录
interface IStepItem {
id: number;
name: string;
detail?: string;
}
export const dataStep: IStepItem[] = [
{ id: 1, name: '申请提交', detail: '已于12月21日提交' },
{ id: 2, name: '电子发票', detail: '预计1~3个工作日' },
{ id: 3, name: '发票已邮寄', detail: '电子发票开出后7个工作日内联系' },
{ id: 4, name: '完成', detail: '' },
];
export const stepCurrent = 2;
// Mock Data of 产品采购明细
export const total = 36;
const listTables: any = [];
for (let i = 0; i < total; i++) {
listTables.push({
id: i,
number: 'S20201228115950963',
name: 'Macbook ',
tag: '电子产品',
code: 'p_tmp_60a637cd0d ',
amount: 52,
department: '财务部',
createtime: '2021-12-30 10:43:56',
});
}
export const dataBuyList = listTables;
@@ -1,3 +0,0 @@
.cardBox {
margin-top: 15px;
}
-14
View File
@@ -1,14 +0,0 @@
import React, { memo } from 'react';
import Base from './components/Base';
import ProgressComp from './components/Progress';
import Product from './components/Product';
import Detail from './components/Detail';
export default memo(() => (
<div>
<Base />
<ProgressComp />
<Product />
<Detail />
</div>
));
-22
View File
@@ -1,22 +0,0 @@
export const dataInfo = [
{ id: 1, name: '合同名称', value: '总部办公用品采购项目' },
{ id: 2, name: '合同状态', value: '履行中', type: 'status' },
{ id: 3, name: '合同编号', value: 'BH00010' },
{ id: 4, name: '合同类型', value: '主合同' },
{ id: 5, name: '合同收付类型', value: '付款' },
{ id: 6, name: '合同金额', value: '5,000,000元' },
{ id: 7, name: '甲方', value: '腾讯科技(深圳)有限公司' },
{ id: 8, name: '乙方', value: '欧尚' },
{ id: 9, name: '合同签订日期', value: '2020-12-20' },
{ id: 10, name: '合同生效日期', value: '2021-01-20' },
{ id: 11, name: '合同结束日期', value: '2022-12-20' },
{ id: 12, name: '合同附件', value: '总部办公用品采购项目合同.pdf', type: 'link' },
{ id: 13, name: '备注', value: '--' },
{ id: 14, name: '创建时间', value: '2020-12-22 10:00:00' },
];
export const dataStep = [
{ id: 1, name: '上传合同附件', detail: '这里是提示文字' },
{ id: 2, name: '修改合同金额', detail: '这里是提示文字' },
{ id: 3, name: '新建合同', detail: '2020-12-01 15:00:00 管理员-李川操作' },
];
@@ -1,57 +0,0 @@
@Wrap: infoBox;
.@{Wrap} {
column-count: 2;
&Item {
padding: 12px 0;
display: flex;
color: var(--td-text-color-primary);
h1 {
font-weight: 400;
font-size: 14px;
color: var(--td-text-color-secondary);
text-align: left;
line-height: 22px;
margin: 0;
}
span {
margin-left: 24px;
font-size: 14px;
line-height: 1.5;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
i {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--td-success-color-5);
margin-right: 5px;
}
.inProgress {
color: var(--td-success-color-5);
}
.pdf {
color: var(--td-brand-color);
&:hover {
cursor: pointer;
}
}
}
}
.logBox {
margin-top: 15px;
}
@media (min-width: 992px) {
.@{Wrap} {
&Item {
h1 {
width: 200px;
}
}
}
}
-39
View File
@@ -1,39 +0,0 @@
import React, { memo } from 'react';
import { Steps, Card } from 'tdesign-react';
import classnames from 'classnames';
import { dataInfo, dataStep } from './consts';
import Style from './index.module.less';
const { StepItem } = Steps;
export default memo(() => (
<div>
<Card title='基本信息' bordered={false}>
<div className={classnames(Style.infoBox)}>
{dataInfo.map((item) => (
<div key={item.id} className={classnames(Style.infoBoxItem)}>
<h1>{item.name}</h1>
<span
className={classnames({
[Style.inProgress]: item.type === 'status',
[Style.pdf]: item.type === 'link',
})}
>
{item.type === 'status' && <i />}
{item.value}
</span>
</div>
))}
</div>
</Card>
<Card title='变更记录' className={Style.logBox} bordered={false}>
<div>
<Steps layout='vertical' theme='dot' current={1}>
{dataStep.map((item) => (
<StepItem key={item.id} title={item.name} content={item.detail} />
))}
</Steps>
</div>
</Card>
</div>
));
-158
View File
@@ -1,158 +0,0 @@
import type { EChartOption } from 'echarts';
import { getTimeArray, getRandomInt } from 'utils/chart';
export function getLineOptions(dateTime: any = []): EChartOption {
let dateArray: Array<string> = ['00:00', '02:00', '04:00', '06:00'];
if (dateTime.length > 0) {
const divideNum = 7;
dateArray = getTimeArray(dateTime, divideNum);
}
return {
tooltip: {
trigger: 'item',
},
grid: {
top: '10px',
left: '0',
right: '20px',
bottom: '36px',
containLabel: true,
},
xAxis: {
type: 'category',
data: dateArray,
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#E3E6EB',
width: 1,
},
},
},
yAxis: {
type: 'value',
},
legend: {
data: ['本月', '上月'],
icon: 'circle',
bottom: '0',
itemGap: 48,
itemHeight: 8,
itemWidth: 8,
},
series: [
{
name: '上月',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
smooth: true,
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
areaStyle: {
color: '#0053D92F',
},
},
{
name: '本月',
data: [
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
getRandomInt(),
],
type: 'line',
smooth: true,
showSymbol: true,
symbol: 'circle',
symbolSize: 8,
},
],
};
}
export const lastYearList: Array<number> = [100, 120, 140, 160, 180, 200, 210];
export function getBarOptions(isMonth = false): EChartOption {
let lastYearListCopy = lastYearList.concat([]);
let thisYearListCopy = lastYearList.concat([]);
if (isMonth) {
lastYearListCopy = lastYearListCopy.reverse();
thisYearListCopy = thisYearListCopy.reverse();
}
return {
tooltip: {
trigger: 'item',
},
grid: {
top: '10px',
left: '0',
right: '0',
bottom: '36px',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
axisTick: {
alignWithLabel: true,
},
axisLine: {
lineStyle: {
color: '#E3E6EB',
width: 1,
},
},
},
],
yAxis: [
{
type: 'value',
axisLabel: {
color: 'rgba(0, 0, 0, 0.4)',
},
},
],
legend: {
data: ['去年', '今年'],
bottom: '0',
icon: 'rect',
itemGap: 48,
itemHeight: 4,
itemWidth: 12,
// itemStyle: {},
},
series: [
{
name: '去年',
type: 'bar',
barWidth: '30%',
data: lastYearListCopy,
itemStyle: {
color: '#BCC4D0',
},
},
{
name: '今年',
type: 'bar',
barWidth: '30%',
data: thisYearListCopy,
},
],
};
}
@@ -1,10 +0,0 @@
.operationLink {
color: var(--td-brand-color);
text-decoration: none;
margin-right: 24px;
cursor: pointer;
transition: color 0.2s cubic-bezier(0.38, 0, 0.24, 1);
&:last-child {
margin-right: 0;
}
}

Some files were not shown because too many files have changed in this diff Show More