Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
159
README.md
159
README.md
@@ -88,27 +88,27 @@
|
|||||||
|
|
||||||
### 系统功能
|
### 系统功能
|
||||||
|
|
||||||
| | 功能 | 描述 |
|
| | 功能 | 描述 |
|
||||||
| --- | --- | --- |
|
|----|-------|---------------------------------|
|
||||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
||||||
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
|
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
|
||||||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
||||||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
||||||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
||||||
| | 岗位管理 | 配置系统用户所属担任职务 |
|
| | 岗位管理 | 配置系统用户所属担任职务 |
|
||||||
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
|
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
|
||||||
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
|
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
|
||||||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
|
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
|
||||||
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
|
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
|
||||||
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
|
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
|
||||||
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 |
|
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 |
|
||||||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
|
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
|
||||||
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
|
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
|
||||||
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
|
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
|
||||||
| | 通知公告 | 系统通知公告信息发布维护 |
|
| | 通知公告 | 系统通知公告信息发布维护 |
|
||||||
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
|
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
|
||||||
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
|
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
|
||||||
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
|
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -126,32 +126,32 @@
|
|||||||
>
|
>
|
||||||
> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
|
> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
|
||||||
|
|
||||||
| 功能列表 | 功能描述 | 是否完成 |
|
| 功能列表 | 功能描述 | 是否完成 |
|
||||||
| --- | --- | --- |
|
|------------|-------------------------------------------------------------------------------------|------|
|
||||||
| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置 | ✅ |
|
| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置 | ✅ |
|
||||||
| BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ |
|
| BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ |
|
||||||
| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ |
|
| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ |
|
||||||
| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ |
|
| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ |
|
||||||
| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ |
|
| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ |
|
||||||
| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ |
|
| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ |
|
||||||
| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ |
|
| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ |
|
||||||
| 转办 | A 转给其 B 审批,B 审批后,进入下一节点 | ✅ |
|
| 转办 | A 转给其 B 审批,B 审批后,进入下一节点 | ✅ |
|
||||||
| 委派 | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点 | ✅ |
|
| 委派 | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点 | ✅ |
|
||||||
| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ |
|
| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ |
|
||||||
| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ |
|
| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ |
|
||||||
| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ |
|
| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ |
|
||||||
| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ |
|
| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ |
|
||||||
| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ |
|
| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ |
|
||||||
| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ |
|
| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ |
|
||||||
| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ |
|
| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ |
|
||||||
| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ |
|
| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ |
|
||||||
| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ |
|
| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ |
|
||||||
| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ |
|
| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ |
|
||||||
| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ |
|
| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ |
|
||||||
| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ |
|
| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ |
|
||||||
| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ |
|
| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ |
|
||||||
| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ |
|
| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ |
|
||||||
| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ |
|
| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ |
|
||||||
|
|
||||||
### 支付系统
|
### 支付系统
|
||||||
|
|
||||||
@@ -165,26 +165,26 @@
|
|||||||
|
|
||||||
### 基础设施
|
### 基础设施
|
||||||
|
|
||||||
| | 功能 | 描述 |
|
| | 功能 | 描述 |
|
||||||
| --- | --- | --- |
|
|----|-----------|----------------------------------------------|
|
||||||
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
|
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
|
||||||
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
|
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
|
||||||
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
|
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
|
||||||
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
|
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
|
||||||
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
|
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
|
||||||
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
|
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
|
||||||
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 |
|
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 |
|
||||||
| 🚀 | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式 |
|
| 🚀 | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式 |
|
||||||
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
|
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
|
||||||
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
|
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
|
||||||
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
|
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
|
||||||
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
|
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
|
||||||
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
|
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
|
||||||
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
|
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
|
||||||
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
|
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
|
||||||
| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
|
| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
|
||||||
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
|
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
|
||||||
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
|
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -197,18 +197,19 @@
|
|||||||
|
|
||||||
### 微信公众号
|
### 微信公众号
|
||||||
|
|
||||||
| | 功能 | 描述 |
|
| | 功能 | 描述 |
|
||||||
| --- | --- | --- |
|
|----|--------|-------------------------------|
|
||||||
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
|
||||||
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
|
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
|
||||||
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
|
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
|
||||||
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
|
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
|
||||||
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
|
| 🚀 | 模版消息 | 配置和发送模版消息,用于向粉丝推送通知类消息 |
|
||||||
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
|
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
|
||||||
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
|
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
|
||||||
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
|
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
|
||||||
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
|
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
|
||||||
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
|
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
|
||||||
|
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
|
||||||
|
|
||||||
### 商城系统
|
### 商城系统
|
||||||
|
|
||||||
|
|||||||
@@ -62,13 +62,6 @@ export namespace MallSpuApi {
|
|||||||
valueName?: string; // 属性值名称
|
valueName?: string; // 属性值名称
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这个还要么?
|
|
||||||
/** 优惠券模板 */
|
|
||||||
export interface GiveCouponTemplate {
|
|
||||||
id?: number; // 优惠券编号
|
|
||||||
name?: string; // 优惠券名称
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 商品状态更新请求 */
|
/** 商品状态更新请求 */
|
||||||
export interface SpuStatusUpdateReqVO {
|
export interface SpuStatusUpdateReqVO {
|
||||||
id: number; // 商品编号
|
id: number; // 商品编号
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MallBargainActivityApi {
|
export namespace MallBargainActivityApi {
|
||||||
@@ -32,17 +30,6 @@ export namespace MallBargainActivityApi {
|
|||||||
bargainMinPrice: number; // 砍价底价
|
bargainMinPrice: number; // 砍价底价
|
||||||
stock: number; // 活动库存
|
stock: number; // 活动库存
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:要不要删除?
|
|
||||||
/** 扩展 SKU 配置 */
|
|
||||||
export type SkuExtension = {
|
|
||||||
productConfig: BargainProduct; // 砍价活动配置
|
|
||||||
} & MallSpuApi.Sku;
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置 */
|
|
||||||
export interface SpuExtension extends MallSpuApi.Spu {
|
|
||||||
skus: SkuExtension[]; // SKU 列表
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询砍价活动列表 */
|
/** 查询砍价活动列表 */
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MallCombinationActivityApi {
|
export namespace MallCombinationActivityApi {
|
||||||
@@ -25,23 +23,12 @@ export namespace MallCombinationActivityApi {
|
|||||||
products: CombinationProduct[]; // 商品列表
|
products: CombinationProduct[]; // 商品列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:要不要删除?
|
|
||||||
/** 拼团活动所需属性 */
|
/** 拼团活动所需属性 */
|
||||||
export interface CombinationProduct {
|
export interface CombinationProduct {
|
||||||
spuId: number; // 商品 SPU 编号
|
spuId: number; // 商品 SPU 编号
|
||||||
skuId: number; // 商品 SKU 编号
|
skuId: number; // 商品 SKU 编号
|
||||||
combinationPrice: number; // 拼团价格
|
combinationPrice: number; // 拼团价格
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 扩展 SKU 配置 */
|
|
||||||
export type SkuExtension = {
|
|
||||||
productConfig: CombinationProduct; // 拼团活动配置
|
|
||||||
} & MallSpuApi.Sku;
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置 */
|
|
||||||
export interface SpuExtension extends MallSpuApi.Spu {
|
|
||||||
skus: SkuExtension[]; // SKU 列表
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询拼团活动列表 */
|
/** 查询拼团活动列表 */
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MallDiscountActivityApi {
|
export namespace MallDiscountActivityApi {
|
||||||
@@ -25,17 +23,6 @@ export namespace MallDiscountActivityApi {
|
|||||||
endTime?: Date; // 结束时间
|
endTime?: Date; // 结束时间
|
||||||
products?: DiscountProduct[]; // 商品列表
|
products?: DiscountProduct[]; // 商品列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:要不要删除?
|
|
||||||
/** 扩展 SKU 配置 */
|
|
||||||
export type SkuExtension = {
|
|
||||||
productConfig: DiscountProduct; // 限时折扣配置
|
|
||||||
} & MallSpuApi.Sku;
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置 */
|
|
||||||
export interface SpuExtension extends MallSpuApi.Spu {
|
|
||||||
skus: SkuExtension[]; // SKU 列表
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询限时折扣活动列表 */
|
/** 查询限时折扣活动列表 */
|
||||||
|
|||||||
@@ -36,17 +36,6 @@ export namespace MallPointActivityApi {
|
|||||||
price: number; // 兑换金额,单位:分
|
price: number; // 兑换金额,单位:分
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这些还需要么?
|
|
||||||
/** 扩展 SKU 配置 */
|
|
||||||
export type SkuExtension = {
|
|
||||||
productConfig: PointProduct; // 积分商城商品配置
|
|
||||||
} & MallSpuApi.Sku;
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置 */
|
|
||||||
export interface SpuExtension extends MallSpuApi.Spu {
|
|
||||||
skus: SkuExtension[]; // SKU 列表
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置(带积分信息) */
|
/** 扩展 SPU 配置(带积分信息) */
|
||||||
export interface SpuExtensionWithPoint extends MallSpuApi.Spu {
|
export interface SpuExtensionWithPoint extends MallSpuApi.Spu {
|
||||||
pointStock: number; // 积分商城活动库存
|
pointStock: number; // 积分商城活动库存
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MallSeckillActivityApi {
|
export namespace MallSeckillActivityApi {
|
||||||
@@ -34,17 +32,6 @@ export namespace MallSeckillActivityApi {
|
|||||||
seckillPrice?: number; // 秒杀价格
|
seckillPrice?: number; // 秒杀价格
|
||||||
products?: SeckillProduct[]; // 秒杀商品列表
|
products?: SeckillProduct[]; // 秒杀商品列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这些还需要么?
|
|
||||||
/** 扩展 SKU 配置 */
|
|
||||||
export type SkuExtension = {
|
|
||||||
productConfig: SeckillProduct; // 秒杀商品配置
|
|
||||||
} & MallSpuApi.Sku;
|
|
||||||
|
|
||||||
/** 扩展 SPU 配置 */
|
|
||||||
export interface SpuExtension extends MallSpuApi.Spu {
|
|
||||||
skus: SkuExtension[]; // SKU 列表
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询秒杀活动列表 */
|
/** 查询秒杀活动列表 */
|
||||||
|
|||||||
@@ -68,5 +68,5 @@ export function generateAccountQrCode(id: number) {
|
|||||||
|
|
||||||
/** 清空公众号账号 API 配额 */
|
/** 清空公众号账号 API 配额 */
|
||||||
export function clearAccountQuota(id: number) {
|
export function clearAccountQuota(id: number) {
|
||||||
return requestClient.post(`/mp/account/clear-quota?id=${id}`);
|
return requestClient.put(`/mp/account/clear-quota?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
57
apps/web-antd/src/api/mp/messageTemplate/index.ts
Normal file
57
apps/web-antd/src/api/mp/messageTemplate/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace MpMessageTemplateApi {
|
||||||
|
/** 消息模板信息 */
|
||||||
|
export interface MessageTemplate {
|
||||||
|
id: number;
|
||||||
|
accountId: number;
|
||||||
|
appId: string;
|
||||||
|
templateId: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
example: string;
|
||||||
|
primaryIndustry: string;
|
||||||
|
deputyIndustry: string;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送消息模板请求 */
|
||||||
|
export interface MessageTemplateSendVO {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
data?: Record<string, string>;
|
||||||
|
url?: string;
|
||||||
|
miniProgramAppId?: string;
|
||||||
|
miniProgramPagePath?: string;
|
||||||
|
miniprogram?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询消息模板列表 */
|
||||||
|
export function getMessageTemplateList(params: { accountId: number }) {
|
||||||
|
return requestClient.get<MpMessageTemplateApi.MessageTemplate[]>(
|
||||||
|
'/mp/message-template/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除消息模板 */
|
||||||
|
export function deleteMessageTemplate(id: number) {
|
||||||
|
return requestClient.delete('/mp/message-template/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步公众号模板 */
|
||||||
|
export function syncMessageTemplate(accountId: number) {
|
||||||
|
return requestClient.post('/mp/message-template/sync', null, {
|
||||||
|
params: { accountId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送消息模板 */
|
||||||
|
export function sendMessageTemplate(
|
||||||
|
data: MpMessageTemplateApi.MessageTemplateSendVO,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/mp/message-template/send', data);
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ defineExpose({
|
|||||||
<div class="inline-block text-center" :style="getStyle">
|
<div class="inline-block text-center" :style="getStyle">
|
||||||
<!-- 图片包装器 -->
|
<!-- 图片包装器 -->
|
||||||
<div
|
<div
|
||||||
class="bg-card group relative cursor-pointer overflow-hidden rounded-full border border-gray-200"
|
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-card"
|
||||||
:style="getImageWrapperStyle"
|
:style="getImageWrapperStyle"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export function useApiSelect(option: ApiSelectProps) {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'label',
|
default: 'label',
|
||||||
},
|
},
|
||||||
|
// 返回值类型(用于部门选择器等):id 返回 ID,name 返回名称
|
||||||
|
returnType: {
|
||||||
|
type: String,
|
||||||
|
default: 'id',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
@@ -129,10 +134,21 @@ export function useApiSelect(option: ApiSelectProps) {
|
|||||||
|
|
||||||
function parseOptions0(data: any[]) {
|
function parseOptions0(data: any[]) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
options.value = data.map((item: any) => ({
|
options.value = data.map((item: any) => {
|
||||||
label: parseExpression(item, props.labelField),
|
const label = parseExpression(item, props.labelField);
|
||||||
value: parseExpression(item, props.valueField),
|
let value = parseExpression(item, props.valueField);
|
||||||
}));
|
|
||||||
|
// 根据 returnType 决定返回值
|
||||||
|
// 如果设置了 returnType 为 'name',则返回 label 作为 value
|
||||||
|
if (props.returnType === 'name') {
|
||||||
|
value = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
value: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn(`接口[${props.url}] 返回结果不是一个数组`);
|
console.warn(`接口[${props.url}] 返回结果不是一个数组`);
|
||||||
|
|||||||
@@ -194,6 +194,18 @@ export async function useFormCreateDesigner(designer: Ref) {
|
|||||||
name: 'DeptSelect',
|
name: 'DeptSelect',
|
||||||
label: '部门选择器',
|
label: '部门选择器',
|
||||||
icon: 'icon-tree',
|
icon: 'icon-tree',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'returnType',
|
||||||
|
title: '返回值类型',
|
||||||
|
value: 'id',
|
||||||
|
options: [
|
||||||
|
{ label: '部门编号', value: 'id' },
|
||||||
|
{ label: '部门名称', value: 'name' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
const dictSelectRule = useDictSelectRule();
|
const dictSelectRule = useDictSelectRule();
|
||||||
const apiSelectRule0 = useSelectRule({
|
const apiSelectRule0 = useSelectRule({
|
||||||
|
|||||||
@@ -302,9 +302,9 @@ function getValue() {
|
|||||||
class="mt-2 flex flex-wrap items-center"
|
class="mt-2 flex flex-wrap items-center"
|
||||||
>
|
>
|
||||||
请上传不超过
|
请上传不超过
|
||||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||||
的
|
的
|
||||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||||
格式文件
|
格式文件
|
||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|||||||
@@ -312,9 +312,9 @@ function getValue() {
|
|||||||
class="mt-2 flex flex-wrap items-center text-sm"
|
class="mt-2 flex flex-wrap items-center text-sm"
|
||||||
>
|
>
|
||||||
请上传不超过
|
请上传不超过
|
||||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||||
的
|
的
|
||||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||||
格式文件
|
格式文件
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"register": "Register",
|
"register": "Register",
|
||||||
"codeLogin": "Code Login",
|
"codeLogin": "Code Login",
|
||||||
"qrcodeLogin": "Qr Code Login",
|
"qrcodeLogin": "Qr Code Login",
|
||||||
"forgetPassword": "Forget Password"
|
"forgetPassword": "Forget Password",
|
||||||
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"register": "注册",
|
"register": "注册",
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"qrcodeLogin": "二维码登录",
|
"qrcodeLogin": "二维码登录",
|
||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码",
|
||||||
|
"profile": "个人中心"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "概览",
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const useMallKefuStore = defineStore('mall-kefu', {
|
|||||||
},
|
},
|
||||||
conversationSort() {
|
conversationSort() {
|
||||||
// 按置顶属性和最后消息时间排序
|
// 按置顶属性和最后消息时间排序
|
||||||
this.conversationList.sort((a, b) => {
|
this.conversationList.toSorted((a, b) => {
|
||||||
// 按照置顶排序,置顶的会在前面
|
// 按照置顶排序,置顶的会在前面
|
||||||
if (a.adminPinned !== b.adminPinned) {
|
if (a.adminPinned !== b.adminPinned) {
|
||||||
return a.adminPinned ? -1 : 1;
|
return a.adminPinned ? -1 : 1;
|
||||||
|
|||||||
102
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
102
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { SystemUserProfileApi } from '#/api/system/user/profile';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileBaseSetting, z } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { updateUserProfile } from '#/api/system/user/profile';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
profile?: SystemUserProfileApi.UserProfileRespVO;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'success'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const profileBaseSettingRef = ref();
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '用户昵称',
|
||||||
|
fieldName: 'nickname',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户昵称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户手机',
|
||||||
|
fieldName: 'mobile',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户手机',
|
||||||
|
},
|
||||||
|
rules: z.string(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户邮箱',
|
||||||
|
fieldName: 'email',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户邮箱',
|
||||||
|
},
|
||||||
|
rules: z.string().email('请输入正确的邮箱'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户性别',
|
||||||
|
fieldName: 'sex',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(values: Recordable<any>) {
|
||||||
|
try {
|
||||||
|
profileBaseSettingRef.value.getFormApi().setLoading(true);
|
||||||
|
// 提交表单
|
||||||
|
await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReqVO);
|
||||||
|
// 关闭并提示
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
profileBaseSettingRef.value.getFormApi().setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听 profile 变化 */
|
||||||
|
watch(
|
||||||
|
() => props.profile,
|
||||||
|
(newProfile) => {
|
||||||
|
if (newProfile) {
|
||||||
|
profileBaseSettingRef.value.getFormApi().setValues(newProfile);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileBaseSetting
|
||||||
|
ref="profileBaseSettingRef"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '其他用户的消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'systemMessage',
|
||||||
|
label: '系统消息',
|
||||||
|
description: '系统消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'todoTask',
|
||||||
|
label: '待办任务',
|
||||||
|
description: '待办任务将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
66
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
66
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const profilePasswordSettingRef = ref();
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'oldPassword',
|
||||||
|
label: '旧密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入旧密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请输入新密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请再次输入新密码',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { newPassword } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: '请再次输入新密码' })
|
||||||
|
.min(1, { message: '请再次输入新密码' })
|
||||||
|
.refine((value) => value === newPassword, {
|
||||||
|
message: '两次输入的密码不一致',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['newPassword'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
message.success('密码修改成功');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfilePasswordSetting
|
||||||
|
ref="profilePasswordSettingRef"
|
||||||
|
class="w-1/3"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '当前密码强度:强',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityPhone',
|
||||||
|
label: '密保手机',
|
||||||
|
description: '已绑定手机:138****8293',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityQuestion',
|
||||||
|
label: '密保问题',
|
||||||
|
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityEmail',
|
||||||
|
label: '备用邮箱',
|
||||||
|
description: '已绑定邮箱:ant***sign.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
fieldName: 'securityMfa',
|
||||||
|
label: 'MFA 设备',
|
||||||
|
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
@@ -403,8 +403,8 @@ async function doSendMessageStream(userMessage: AiChatMessageApi.ChatMessage) {
|
|||||||
const lastMessage =
|
const lastMessage =
|
||||||
activeMessageList.value[activeMessageList.value.length - 1];
|
activeMessageList.value[activeMessageList.value.length - 1];
|
||||||
// 累加推理内容
|
// 累加推理内容
|
||||||
lastMessage.reasoningContent =
|
lastMessage!.reasoningContent =
|
||||||
(lastMessage.reasoningContent || '') +
|
(lastMessage!.reasoningContent || '') +
|
||||||
data.receive.reasoningContent;
|
data.receive.reasoningContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,9 +552,9 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 右侧:详情部分 -->
|
<!-- 右侧:详情部分 -->
|
||||||
<Layout class="bg-card mx-4">
|
<Layout class="mx-4 bg-card">
|
||||||
<Layout.Header
|
<Layout.Header
|
||||||
class="!bg-card border-border flex !h-12 items-center justify-between border-b !px-4"
|
class="flex !h-12 items-center justify-between border-b border-border !bg-card !px-4"
|
||||||
>
|
>
|
||||||
<div class="text-lg font-bold">
|
<div class="text-lg font-bold">
|
||||||
{{ activeConversation?.title ? activeConversation?.title : '对话' }}
|
{{ activeConversation?.title ? activeConversation?.title : '对话' }}
|
||||||
@@ -613,9 +613,9 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
|
||||||
<Layout.Footer class="!bg-card flex flex-col !p-0">
|
<Layout.Footer class="flex flex-col !bg-card !p-0">
|
||||||
<form
|
<form
|
||||||
class="border-border mx-4 mb-8 mt-2 flex flex-col rounded-xl border p-2"
|
class="mx-4 mb-8 mt-2 flex flex-col rounded-xl border border-border p-2"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
class="box-border h-24 resize-none overflow-auto rounded-md p-2 focus:outline-none"
|
class="box-border h-24 resize-none overflow-auto rounded-md p-2 focus:outline-none"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function getChatConversationList() {
|
|||||||
// 1.1 获取 对话数据
|
// 1.1 获取 对话数据
|
||||||
conversationList.value = await getChatConversationMyList();
|
conversationList.value = await getChatConversationMyList();
|
||||||
// 1.2 排序
|
// 1.2 排序
|
||||||
conversationList.value.sort((a, b) => {
|
conversationList.value.toSorted((a, b) => {
|
||||||
return Number(b.createTime) - Number(a.createTime);
|
return Number(b.createTime) - Number(a.createTime);
|
||||||
});
|
});
|
||||||
// 1.3 没有任何对话情况
|
// 1.3 没有任何对话情况
|
||||||
@@ -414,7 +414,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- 左底部:工具栏 -->
|
<!-- 左底部:工具栏 -->
|
||||||
<div
|
<div
|
||||||
class="bg-card absolute bottom-1 left-0 right-0 mb-4 flex items-center justify-between px-5 leading-9 text-gray-400 shadow-sm"
|
class="absolute bottom-1 left-0 right-0 mb-4 flex items-center justify-between bg-card px-5 leading-9 text-gray-400 shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer items-center text-gray-400"
|
class="flex cursor-pointer items-center text-gray-400"
|
||||||
|
|||||||
@@ -138,14 +138,14 @@ async function uploadFile(fileItem: FileItem) {
|
|||||||
fileItem.progress = 100;
|
fileItem.progress = 100;
|
||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
console.log('上传响应:', response);
|
console.warn('上传响应:', response);
|
||||||
|
|
||||||
// 兼容不同的返回格式:{ url: '...' } 或 { data: '...' } 或直接是字符串
|
// 兼容不同的返回格式:{ url: '...' } 或 { data: '...' } 或直接是字符串
|
||||||
const fileUrl =
|
const fileUrl =
|
||||||
(response as any)?.url || (response as any)?.data || response;
|
(response as any)?.url || (response as any)?.data || response;
|
||||||
fileItem.url = fileUrl;
|
fileItem.url = fileUrl;
|
||||||
|
|
||||||
console.log('提取的文件 URL:', fileUrl);
|
console.warn('提取的文件 URL:', fileUrl);
|
||||||
|
|
||||||
// 只有当 URL 有效时才添加到列表
|
// 只有当 URL 有效时才添加到列表
|
||||||
if (fileUrl && typeof fileUrl === 'string') {
|
if (fileUrl && typeof fileUrl === 'string') {
|
||||||
@@ -242,7 +242,7 @@ onUnmounted(() => {
|
|||||||
<!-- Hover 显示的文件列表 -->
|
<!-- Hover 显示的文件列表 -->
|
||||||
<div
|
<div
|
||||||
v-if="hasFiles && showTooltip"
|
v-if="hasFiles && showTooltip"
|
||||||
class="animate-in fade-in slide-in-from-bottom-1 absolute bottom-[calc(100%+8px)] left-1/2 z-[1000] min-w-[240px] max-w-[320px] -translate-x-1/2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg duration-200"
|
class="absolute bottom-[calc(100%+8px)] left-1/2 z-[1000] min-w-[240px] max-w-[320px] -translate-x-1/2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg duration-200 animate-in fade-in slide-in-from-bottom-1"
|
||||||
@mouseenter="showTooltipHandler"
|
@mouseenter="showTooltipHandler"
|
||||||
@mouseleave="hideTooltipHandler"
|
@mouseleave="hideTooltipHandler"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function handleClick(doc: any) {
|
|||||||
<div
|
<div
|
||||||
v-for="(doc, index) in documentList"
|
v-for="(doc, index) in documentList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="bg-card cursor-pointer rounded-lg p-2 px-3 transition-all hover:bg-blue-50"
|
class="cursor-pointer rounded-lg bg-card p-2 px-3 transition-all hover:bg-blue-50"
|
||||||
@click="handleClick(doc)"
|
@click="handleClick(doc)"
|
||||||
>
|
>
|
||||||
<div class="mb-1 text-sm text-gray-600">
|
<div class="mb-1 text-sm text-gray-600">
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ onMounted(async () => {
|
|||||||
<!-- 回到底部按钮 -->
|
<!-- 回到底部按钮 -->
|
||||||
<div
|
<div
|
||||||
v-if="isScrolling"
|
v-if="isScrolling"
|
||||||
class="z-1000 absolute bottom-0 right-1/2"
|
class="absolute bottom-0 right-1/2 z-1000"
|
||||||
@click="handleGoBottom"
|
@click="handleGoBottom"
|
||||||
>
|
>
|
||||||
<Button shape="circle">
|
<Button shape="circle">
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ async function handleTabsScroll() {
|
|||||||
<Menu.Item @click="handleMoreClick(['edit', role])">
|
<Menu.Item @click="handleMoreClick(['edit', role])">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<IconifyIcon icon="lucide:edit" color="#787878" />
|
<IconifyIcon icon="lucide:edit" color="#787878" />
|
||||||
<span class="text-primary ml-2">编辑</span>
|
<span class="ml-2 text-primary">编辑</span>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -176,12 +176,12 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<Drawer>
|
<Drawer>
|
||||||
<Layout
|
<Layout
|
||||||
class="bg-card absolute inset-0 flex h-full w-full flex-col overflow-hidden"
|
class="absolute inset-0 flex h-full w-full flex-col overflow-hidden bg-card"
|
||||||
>
|
>
|
||||||
<FormModal @success="handlerAddRoleSuccess" />
|
<FormModal @success="handlerAddRoleSuccess" />
|
||||||
|
|
||||||
<Layout.Content class="relative m-0 flex-1 overflow-hidden p-0">
|
<Layout.Content class="relative m-0 flex-1 overflow-hidden p-0">
|
||||||
<div class="z-100 absolute right-0 top--1 mr-5 mt-5">
|
<div class="absolute right-0 top--1 z-100 mr-5 mt-5">
|
||||||
<!-- 搜索输入框 -->
|
<!-- 搜索输入框 -->
|
||||||
<Input.Search
|
<Input.Search
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<div class="absolute inset-0 m-4 flex h-full w-full flex-row">
|
<div class="absolute inset-0 m-4 flex h-full w-full flex-row">
|
||||||
<div class="bg-card left-0 mr-4 flex w-96 flex-col rounded-lg p-4">
|
<div class="left-0 mr-4 flex w-96 flex-col rounded-lg bg-card p-4">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<Segmented
|
<Segmented
|
||||||
v-model:value="selectPlatform"
|
v-model:value="selectPlatform"
|
||||||
@@ -123,7 +123,7 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-card flex-1">
|
<div class="flex-1 bg-card">
|
||||||
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ defineExpose({ settingValues });
|
|||||||
@click="handleSizeClick(imageSize)"
|
@click="handleSizeClick(imageSize)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-card flex h-12 w-12 flex-col items-center justify-center rounded-lg border p-0"
|
class="flex h-12 w-12 flex-col items-center justify-center rounded-lg border bg-card p-0"
|
||||||
:class="[
|
:class="[
|
||||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-card sticky bottom-0 z-50 flex h-16 items-center justify-center shadow-sm"
|
class="sticky bottom-0 z-50 flex h-16 items-center justify-center bg-card shadow-sm"
|
||||||
>
|
>
|
||||||
<Pagination
|
<Pagination
|
||||||
:total="pageTotal"
|
:total="pageTotal"
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ defineExpose({ settingValues });
|
|||||||
@click="handleSizeClick(imageSize)"
|
@click="handleSizeClick(imageSize)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-card flex h-12 w-12 items-center justify-center rounded-lg border p-0"
|
class="flex h-12 w-12 items-center justify-center rounded-lg border bg-card p-0"
|
||||||
:class="[
|
:class="[
|
||||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ onMounted(async () => {
|
|||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="bg-card grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5 shadow-sm"
|
class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5 bg-card shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in list"
|
v-for="item in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="bg-card relative cursor-pointer overflow-hidden transition-transform duration-300 hover:scale-105"
|
class="relative cursor-pointer overflow-hidden bg-card transition-transform duration-300 hover:scale-105"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
:src="item.picUrl"
|
:src="item.picUrl"
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<!-- 头部导航栏 -->
|
<!-- 头部导航栏 -->
|
||||||
<div
|
<div
|
||||||
class="bg-card absolute left-0 right-0 top-0 z-10 flex h-12 items-center border-b px-4"
|
class="absolute left-0 right-0 top-0 z-10 flex h-12 items-center border-b bg-card px-4"
|
||||||
>
|
>
|
||||||
<!-- 左侧标题 -->
|
<!-- 左侧标题 -->
|
||||||
<div class="flex w-48 items-center overflow-hidden">
|
<div class="flex w-48 items-center overflow-hidden">
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ onMounted(async () => {
|
|||||||
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
|
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
|
||||||
{{ segment.tokens || 0 }} Token
|
{{ segment.tokens || 0 }} Token
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-card rounded-md p-2">
|
<div class="rounded-md bg-card p-2">
|
||||||
{{ segment.content }}
|
{{ segment.content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-card flex w-80 flex-col rounded-lg p-5">
|
<div class="flex w-80 flex-col rounded-lg bg-card p-5">
|
||||||
<h3 class="text-primary h-7 w-full text-center text-xl leading-7">
|
<h3 class="h-7 w-full text-center text-xl leading-7 text-primary">
|
||||||
思维导图创作中心
|
思维导图创作中心
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-4 flex-grow overflow-y-auto">
|
<div class="mt-4 flex-grow overflow-y-auto">
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
getChatRole,
|
getChatRole,
|
||||||
updateChatRole,
|
updateChatRole,
|
||||||
} from '#/api/ai/model/chatRole';
|
} from '#/api/ai/model/chatRole';
|
||||||
import {} from '#/api/bpm/model';
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { message } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { createModel, getModel, updateModel } from '#/api/ai/model/model';
|
import { createModel, getModel, updateModel } from '#/api/ai/model/model';
|
||||||
import {} from '#/api/bpm/model';
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function audioTimeUpdate(args: any) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="b-1 b-l-none h-18 bg-card flex items-center justify-between border border-solid border-rose-100 px-2"
|
class="b-1 b-l-none h-18 flex items-center justify-between border border-solid border-rose-100 bg-card px-2"
|
||||||
>
|
>
|
||||||
<!-- 歌曲信息 -->
|
<!-- 歌曲信息 -->
|
||||||
<div class="flex gap-2.5">
|
<div class="flex gap-2.5">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const currentSong = ref({}); // 当前音乐
|
|||||||
const mySongList = ref<Recordable<any>[]>([]);
|
const mySongList = ref<Recordable<any>[]>([]);
|
||||||
const squareSongList = ref<Recordable<any>[]>([]);
|
const squareSongList = ref<Recordable<any>[]>([]);
|
||||||
|
|
||||||
function generateMusic(formData: Recordable<any>) {
|
function generateMusic(_formData: Recordable<any>) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mySongList.value = Array.from({ length: 20 }, (_, index) => {
|
mySongList.value = Array.from({ length: 20 }, (_, index) => {
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<!-- 头部导航栏 -->
|
<!-- 头部导航栏 -->
|
||||||
<div
|
<div
|
||||||
class="bg-card absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b px-5"
|
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-card px-5"
|
||||||
>
|
>
|
||||||
<!-- 左侧标题 -->
|
<!-- 左侧标题 -->
|
||||||
<div class="flex w-48 items-center overflow-hidden">
|
<div class="flex w-48 items-center overflow-hidden">
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ defineExpose({ validate });
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset
|
<fieldset
|
||||||
class="bg-card m-0 mt-10 rounded-lg border border-gray-200 px-3 py-4"
|
class="m-0 mt-10 rounded-lg border border-gray-200 bg-card px-3 py-4"
|
||||||
>
|
>
|
||||||
<legend class="ml-2 px-2.5 text-base font-semibold text-gray-600">
|
<legend class="ml-2 px-2.5 text-base font-semibold text-gray-600">
|
||||||
<h3>运行结果</h3>
|
<h3>运行结果</h3>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ function handleSubmit() {
|
|||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="hint"
|
v-if="hint"
|
||||||
class="text-primary-500 flex cursor-pointer select-none items-center text-xs"
|
class="flex cursor-pointer select-none items-center text-xs text-primary-500"
|
||||||
@click="hintClick"
|
@click="hintClick"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:circle-help" />
|
<IconifyIcon icon="lucide:circle-help" />
|
||||||
@@ -145,14 +145,14 @@ function handleSubmit() {
|
|||||||
</h3>
|
</h3>
|
||||||
</DefineLabel>
|
</DefineLabel>
|
||||||
<div class="flex flex-col" v-bind="$attrs">
|
<div class="flex flex-col" v-bind="$attrs">
|
||||||
<div class="bg-card flex w-full justify-center pt-2">
|
<div class="flex w-full justify-center bg-card pt-2">
|
||||||
<div class="bg-card z-10 w-72 rounded-full p-1">
|
<div class="z-10 w-72 rounded-full bg-card p-1">
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
selectedTab === AiWriteTypeEnum.REPLY &&
|
selectedTab === AiWriteTypeEnum.REPLY &&
|
||||||
'after:translate-x-[100%] after:transform'
|
'after:translate-x-[100%] after:transform'
|
||||||
"
|
"
|
||||||
class="after:bg-card relative flex items-center after:absolute after:left-0 after:top-0 after:block after:h-7 after:w-1/2 after:rounded-full after:transition-transform after:content-['']"
|
class="relative flex items-center after:absolute after:left-0 after:top-0 after:block after:h-7 after:w-1/2 after:rounded-full after:bg-card after:transition-transform after:content-['']"
|
||||||
>
|
>
|
||||||
<ReuseTab
|
<ReuseTab
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
@@ -166,7 +166,7 @@ function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="bg-card box-border h-full w-96 flex-grow overflow-y-auto px-7 pb-2 lg:block"
|
class="box-border h-full w-96 flex-grow overflow-y-auto bg-card px-7 pb-2 lg:block"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="selectedTab === AiWriteTypeEnum.WRITING">
|
<template v-if="selectedTab === AiWriteTypeEnum.WRITING">
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ watch(copied, (val) => {
|
|||||||
class="hide-scroll-bar box-border h-full overflow-y-auto"
|
class="hide-scroll-bar box-border h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-card relative box-border min-h-full w-full flex-grow p-2 sm:p-5"
|
class="relative box-border min-h-full w-full flex-grow bg-card p-2 sm:p-5"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
v-show="isWriting"
|
v-show="isWriting"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const emits = defineEmits<{
|
|||||||
<span
|
<span
|
||||||
v-for="tag in props.tags"
|
v-for="tag in props.tags"
|
||||||
:key="tag.value"
|
:key="tag.value"
|
||||||
class="bg-card border-card-100 mb-2 cursor-pointer rounded border-2 border-solid px-1 text-xs leading-6"
|
class="border-card-100 mb-2 cursor-pointer rounded border-2 border-solid bg-card px-1 text-xs leading-6"
|
||||||
:class="
|
:class="
|
||||||
modelValue === tag.value && '!border-primary-500 !text-primary-500'
|
modelValue === tag.value && '!border-primary-500 !text-primary-500'
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ const resetElement = () => {
|
|||||||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
||||||
|
|
||||||
// 是否开启自定义用户任务超时处理
|
// 是否开启自定义用户任务超时处理
|
||||||
boundaryEventType.value = elExtensionElements.value.values?.filter(
|
boundaryEventType.value = elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:BoundaryEventType`,
|
(ex: any) => ex.$type === `${prefix}:BoundaryEventType`,
|
||||||
)?.[0];
|
);
|
||||||
if (boundaryEventType.value && boundaryEventType.value.value === 1) {
|
if (boundaryEventType.value && boundaryEventType.value.value === 1) {
|
||||||
timeoutHandlerEnable.value = true;
|
timeoutHandlerEnable.value = true;
|
||||||
configExtensions.value.push(boundaryEventType.value);
|
configExtensions.value.push(boundaryEventType.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行动作
|
// 执行动作
|
||||||
timeoutHandlerType.value = elExtensionElements.value.values?.filter(
|
timeoutHandlerType.value = elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:TimeoutHandlerType`,
|
(ex: any) => ex.$type === `${prefix}:TimeoutHandlerType`,
|
||||||
)?.[0];
|
)?.[0];
|
||||||
if (timeoutHandlerType.value) {
|
if (timeoutHandlerType.value) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 审批类型
|
// 审批类型
|
||||||
approveType.value =
|
approveType.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:ApproveType`,
|
(ex: any) => ex.$type === `${prefix}:ApproveType`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
|
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
|
||||||
@@ -121,7 +121,7 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 审批人与提交人为同一人时
|
// 审批人与提交人为同一人时
|
||||||
assignStartUserHandlerTypeEl.value =
|
assignStartUserHandlerTypeEl.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
|
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
|
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
|
||||||
@@ -131,13 +131,13 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 审批人拒绝时
|
// 审批人拒绝时
|
||||||
rejectHandlerTypeEl.value =
|
rejectHandlerTypeEl.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
|
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
|
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
|
||||||
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
|
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
|
||||||
returnNodeIdEl.value =
|
returnNodeIdEl.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
|
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
|
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
|
||||||
@@ -147,7 +147,7 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 审批人为空时
|
// 审批人为空时
|
||||||
assignEmptyHandlerTypeEl.value =
|
assignEmptyHandlerTypeEl.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
|
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
|
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
|
||||||
@@ -155,7 +155,7 @@ const resetCustomConfigList = () => {
|
|||||||
});
|
});
|
||||||
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value;
|
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value;
|
||||||
assignEmptyUserIdsEl.value =
|
assignEmptyUserIdsEl.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
|
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
|
||||||
)?.[0] ||
|
)?.[0] ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
|
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
|
||||||
@@ -172,7 +172,7 @@ const resetCustomConfigList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
|
buttonsSettingEl.value = elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
||||||
);
|
);
|
||||||
if (buttonsSettingEl.value.length === 0) {
|
if (buttonsSettingEl.value.length === 0) {
|
||||||
@@ -189,7 +189,7 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 字段权限
|
// 字段权限
|
||||||
if (formType.value === BpmModelFormType.NORMAL) {
|
if (formType.value === BpmModelFormType.NORMAL) {
|
||||||
const fieldsPermissionList = elExtensionElements.value.values?.filter(
|
const fieldsPermissionList = elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
||||||
);
|
);
|
||||||
fieldsPermissionEl.value = [];
|
fieldsPermissionEl.value = [];
|
||||||
@@ -206,21 +206,21 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 是否需要签名
|
// 是否需要签名
|
||||||
signEnable.value =
|
signEnable.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:SignEnable`,
|
(ex: any) => ex.$type === `${prefix}:SignEnable`,
|
||||||
)?.[0] ||
|
) ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false });
|
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false });
|
||||||
|
|
||||||
// 审批意见
|
// 审批意见
|
||||||
reasonRequire.value =
|
reasonRequire.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:ReasonRequire`,
|
(ex: any) => ex.$type === `${prefix}:ReasonRequire`,
|
||||||
)?.[0] ||
|
) ||
|
||||||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false });
|
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false });
|
||||||
|
|
||||||
// 保留剩余扩展元素,便于后面更新该元素对应属性
|
// 保留剩余扩展元素,便于后面更新该元素对应属性
|
||||||
otherExtensions.value =
|
otherExtensions.value =
|
||||||
elExtensionElements.value.values?.filter(
|
elExtensionElements.value.values?.find(
|
||||||
(ex: any) =>
|
(ex: any) =>
|
||||||
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
|
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
|
||||||
ex.$type !== `${prefix}:RejectHandlerType` &&
|
ex.$type !== `${prefix}:RejectHandlerType` &&
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ const resetTaskForm = () => {
|
|||||||
const extensionElements =
|
const extensionElements =
|
||||||
businessObject?.extensionElements ??
|
businessObject?.extensionElements ??
|
||||||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
||||||
userTaskForm.value.candidateStrategy = extensionElements.values?.filter(
|
userTaskForm.value.candidateStrategy = extensionElements.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:CandidateStrategy`,
|
(ex: any) => ex.$type === `${prefix}:CandidateStrategy`,
|
||||||
)?.[0]?.value;
|
)?.[0]?.value;
|
||||||
const candidateParamStr = extensionElements.values?.filter(
|
const candidateParamStr = extensionElements.values?.find(
|
||||||
(ex: any) => ex.$type === `${prefix}:CandidateParam`,
|
(ex: any) => ex.$type === `${prefix}:CandidateParam`,
|
||||||
)?.[0]?.value;
|
)?.[0]?.value;
|
||||||
if (candidateParamStr && candidateParamStr.length > 0) {
|
if (candidateParamStr && candidateParamStr.length > 0) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function setDuration(type, val) {
|
|||||||
// 组装ISO 8601字符串
|
// 组装ISO 8601字符串
|
||||||
let d = isoDuration.value;
|
let d = isoDuration.value;
|
||||||
if (d.includes(type)) {
|
if (d.includes(type)) {
|
||||||
d = d.replace(new RegExp(`\\d+${type}`), val + type);
|
d = d.replace(new RegExp(String.raw`\d+${type}`), val + type);
|
||||||
} else {
|
} else {
|
||||||
d += val + type;
|
d += val + type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,10 +82,12 @@ export function updateElementExtensions(element, extensionList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个id
|
// 创建一个id
|
||||||
export function uuid(length = 8, chars?) {
|
export function uuid(
|
||||||
|
length = 8,
|
||||||
|
charsString = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||||
|
) {
|
||||||
let result = '';
|
let result = '';
|
||||||
const charsString =
|
|
||||||
chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
for (let i = length; i > 0; --i) {
|
for (let i = length; i > 0; --i) {
|
||||||
result += charsString[Math.floor(Math.random() * charsString.length)];
|
result += charsString[Math.floor(Math.random() * charsString.length)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="simple-process-model-container">
|
<div class="simple-process-model-container">
|
||||||
<div class="bg-card absolute right-0 top-0">
|
<div class="absolute right-0 top-0 bg-card">
|
||||||
<Row type="flex" justify="end">
|
<Row type="flex" justify="end">
|
||||||
<ButtonGroup key="scale-control">
|
<ButtonGroup key="scale-control">
|
||||||
<Button v-if="!readonly" @click="exportJson">
|
<Button v-if="!readonly" @click="exportJson">
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<!-- 头部导航栏 -->
|
<!-- 头部导航栏 -->
|
||||||
<div
|
<div
|
||||||
class="bg-card absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b px-5"
|
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-card px-5"
|
||||||
>
|
>
|
||||||
<!-- 左侧标题 -->
|
<!-- 左侧标题 -->
|
||||||
<div class="flex w-48 items-center overflow-hidden">
|
<div class="flex w-48 items-center overflow-hidden">
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ function handleRenameSuccess() {
|
|||||||
size="small"
|
size="small"
|
||||||
class="px-1"
|
class="px-1"
|
||||||
@click="modelOperation('update', row.id)"
|
@click="modelOperation('update', row.id)"
|
||||||
:disabled="!isManagerUser(row) || !hasPermiUpdate"
|
:disabled="!isManagerUser(row) && !hasPermiUpdate"
|
||||||
>
|
>
|
||||||
修改
|
修改
|
||||||
</Button>
|
</Button>
|
||||||
@@ -678,7 +678,7 @@ function handleRenameSuccess() {
|
|||||||
size="small"
|
size="small"
|
||||||
class="px-1"
|
class="px-1"
|
||||||
@click="handleDeploy(row)"
|
@click="handleDeploy(row)"
|
||||||
:disabled="!isManagerUser(row) || !hasPermiDeploy"
|
:disabled="!isManagerUser(row) && !hasPermiDeploy"
|
||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</Button>
|
</Button>
|
||||||
@@ -718,7 +718,7 @@ function handleRenameSuccess() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
danger
|
danger
|
||||||
key="handleDelete"
|
key="handleDelete"
|
||||||
:disabled="!isManagerUser(row) || !hasPermiDelete"
|
:disabled="!isManagerUser(row) && !hasPermiDelete"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ onMounted(async () => {
|
|||||||
<span class="text-gray-500">编号:{{ id || '-' }}</span>
|
<span class="text-gray-500">编号:{{ id || '-' }}</span>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="lucide:printer"
|
icon="lucide:printer"
|
||||||
class="hover:text-primary cursor-pointer"
|
class="cursor-pointer hover:text-primary"
|
||||||
@click="handlePrint"
|
@click="handlePrint"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
row.status === BpmProcessInstanceStatus.RUNNING &&
|
row.status === BpmProcessInstanceStatus.RUNNING &&
|
||||||
row.tasks!.length > 0
|
row.tasks?.length! > 0
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<!-- 单人审批 -->
|
<!-- 单人审批 -->
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -54,7 +54,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -102,7 +102,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -150,7 +150,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -198,7 +198,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -246,7 +246,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -294,7 +294,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
@@ -342,7 +342,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
|||||||
return {
|
return {
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['nickname', 'count'],
|
dimensions: ['nickname', 'count'],
|
||||||
source: cloneDeep(res).reverse(),
|
source: cloneDeep(res).toReversed(),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ onMounted(() => {
|
|||||||
{ name: '定制', value: 310 },
|
{ name: '定制', value: 310 },
|
||||||
{ name: '技术支持', value: 274 },
|
{ name: '技术支持', value: 274 },
|
||||||
{ name: '远程', value: 400 },
|
{ name: '远程', value: 400 },
|
||||||
].sort((a, b) => {
|
].toSorted((a, b) => {
|
||||||
return a.value - b.value;
|
return a.value - b.value;
|
||||||
}),
|
}),
|
||||||
name: '商业占比',
|
name: '商业占比',
|
||||||
|
|||||||
@@ -249,9 +249,9 @@ defineExpose({ validate });
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>
|
<span>
|
||||||
合计付款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
|
合计付款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ function handleOpenSaleOut() {
|
|||||||
|
|
||||||
function handleAddSaleOut(rows: ErpSaleOutApi.SaleOut[]) {
|
function handleAddSaleOut(rows: ErpSaleOutApi.SaleOut[]) {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
|
// TODO 芋艿
|
||||||
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
bizType: ErpBizType.SALE_OUT,
|
bizType: ErpBizType.SALE_OUT,
|
||||||
@@ -153,6 +154,7 @@ function handleOpenSaleReturn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleAddSaleReturn(rows: ErpSaleReturnApi.SaleReturn[]) {
|
function handleAddSaleReturn(rows: ErpSaleReturnApi.SaleReturn[]) {
|
||||||
|
// TODO 芋艿
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
||||||
bizId: row.id,
|
bizId: row.id,
|
||||||
@@ -249,9 +251,9 @@ defineExpose({ validate });
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>
|
<span>
|
||||||
合计收款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
|
合计收款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ async function handleWarehouseChange(row: ErpPurchaseInApi.PurchaseInItem) {
|
|||||||
|
|
||||||
/** 处理行数据变更 */
|
/** 处理行数据变更 */
|
||||||
function handleRowChange(row: any) {
|
function handleRowChange(row: any) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
tableData.value.push(row);
|
tableData.value.push(row);
|
||||||
@@ -273,9 +274,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpPurchaseOrderApi.PurchaseOrderItem) {
|
function handleDelete(row: ErpPurchaseOrderApi.PurchaseOrderItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -285,9 +286,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ watch(
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpPurchaseReturnApi.PurchaseReturnItem) {
|
function handleDelete(row: ErpPurchaseReturnApi.PurchaseReturnItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -153,6 +154,7 @@ async function handleWarehouseChange(
|
|||||||
|
|
||||||
/** 处理行数据变更 */
|
/** 处理行数据变更 */
|
||||||
function handleRowChange(row: any) {
|
function handleRowChange(row: any) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
tableData.value.push(row);
|
tableData.value.push(row);
|
||||||
@@ -275,9 +277,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpSaleOrderApi.SaleOrderItem) {
|
function handleDelete(row: ErpSaleOrderApi.SaleOrderItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -285,9 +286,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ watch(
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpSaleOutApi.SaleOutItem) {
|
function handleDelete(row: ErpSaleOutApi.SaleOutItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -273,9 +274,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ watch(
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpSaleReturnApi.SaleReturnItem) {
|
function handleDelete(row: ErpSaleReturnApi.SaleReturnItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -273,9 +274,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpStockCheckApi.StockCheckItem) {
|
function handleDelete(row: ErpStockCheckApi.StockCheckItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -280,9 +281,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ function handleAdd() {
|
|||||||
totalPrice: undefined,
|
totalPrice: undefined,
|
||||||
remark: undefined,
|
remark: undefined,
|
||||||
};
|
};
|
||||||
|
// TODO 芋艿
|
||||||
tableData.value.push(newRow);
|
tableData.value.push(newRow);
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:items', [...tableData.value]);
|
emit('update:items', [...tableData.value]);
|
||||||
@@ -105,6 +106,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpStockInApi.StockInItem) {
|
function handleDelete(row: ErpStockInApi.StockInItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -269,9 +271,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpStockMoveApi.StockMoveItem) {
|
function handleDelete(row: ErpStockMoveApi.StockMoveItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -290,9 +291,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ function handleAdd() {
|
|||||||
|
|
||||||
/** 处理删除 */
|
/** 处理删除 */
|
||||||
function handleDelete(row: ErpStockOutApi.StockOutItem) {
|
function handleDelete(row: ErpStockOutApi.StockOutItem) {
|
||||||
|
// TODO 芋艿
|
||||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tableData.value.splice(index, 1);
|
tableData.value.splice(index, 1);
|
||||||
@@ -267,9 +268,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<div class="border-border bg-muted mt-2 rounded border p-2">
|
<div class="mt-2 rounded border border-border bg-muted p-2">
|
||||||
<div class="text-muted-foreground flex justify-between text-sm">
|
<div class="flex justify-between text-sm text-muted-foreground">
|
||||||
<span class="text-foreground font-medium">合计:</span>
|
<span class="font-medium text-foreground">合计:</span>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
<span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ getDetail();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height v-loading="loading">
|
<Page auto-content-height v-loading="loading">
|
||||||
<div class="bg-card flex h-[95%] flex-col rounded-md p-4">
|
<div class="flex h-[95%] flex-col rounded-md bg-card p-4">
|
||||||
<Steps
|
<Steps
|
||||||
type="navigation"
|
type="navigation"
|
||||||
v-model:current="currentStep"
|
v-model:current="currentStep"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const { status, data, send, close, open } = useWebSocket(server.value, {
|
|||||||
const messageList = ref(
|
const messageList = ref(
|
||||||
[] as { text: string; time: number; type?: string; userId?: string }[],
|
[] as { text: string; time: number; type?: string; userId?: string }[],
|
||||||
); // 消息列表
|
); // 消息列表
|
||||||
const messageReverseList = computed(() => [...messageList.value].reverse());
|
const messageReverseList = computed(() => [...messageList.value].toReversed());
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (!data.value) {
|
if (!data.value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ onMounted(async () => {
|
|||||||
<!-- 所属产品列 -->
|
<!-- 所属产品列 -->
|
||||||
<template #product="{ row }">
|
<template #product="{ row }">
|
||||||
<a
|
<a
|
||||||
class="text-primary cursor-pointer"
|
class="cursor-pointer text-primary"
|
||||||
@click="openProductDetail(row.productId)"
|
@click="openProductDetail(row.productId)"
|
||||||
>
|
>
|
||||||
{{ products.find((p: any) => p.id === row.productId)?.name || '-' }}
|
{{ products.find((p: any) => p.id === row.productId)?.name || '-' }}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function handleAuthInfoDialogClose() {
|
|||||||
<Card class="h-full">
|
<Card class="h-full">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<IconifyIcon icon="ep:info-filled" class="text-primary mr-2" />
|
<IconifyIcon icon="ep:info-filled" class="mr-2 text-primary" />
|
||||||
<span>设备信息</span>
|
<span>设备信息</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,7 +141,7 @@ function handleAuthInfoDialogClose() {
|
|||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<IconifyIcon icon="ep:location" class="text-primary mr-2" />
|
<IconifyIcon icon="ep:location" class="mr-2 text-primary" />
|
||||||
<span>设备位置</span>
|
<span>设备位置</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -553,17 +553,17 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
|||||||
|
|
||||||
.toolbar-wrapper {
|
.toolbar-wrapper {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: hsl(var(--card) / 0.9);
|
background-color: hsl(var(--card) / 90%);
|
||||||
|
border: 1px solid hsl(var(--border) / 60%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid hsl(var(--border) / 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container,
|
.chart-container,
|
||||||
.table-container {
|
.table-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border) / 60%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid hsl(var(--border) / 0.6);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ import {
|
|||||||
|
|
||||||
import { getLatestDeviceProperties } from '#/api/iot/device/device';
|
import { getLatestDeviceProperties } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import DeviceDetailsThingModelPropertyHistory
|
import DeviceDetailsThingModelPropertyHistory from './device-details-thing-model-property-history.vue';
|
||||||
from './device-details-thing-model-property-history.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ deviceId: number }>();
|
const props = defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
@@ -168,13 +167,13 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<!-- 添加渐变背景层 -->
|
<!-- 添加渐变背景层 -->
|
||||||
<div
|
<div
|
||||||
class="from-muted pointer-events-none absolute left-0 right-0 top-0 h-12 bg-gradient-to-b to-transparent"
|
class="pointer-events-none absolute left-0 right-0 top-0 h-12 bg-gradient-to-b from-muted to-transparent"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative p-4">
|
<div class="relative p-4">
|
||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<div class="mb-3 flex items-center">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="mr-2.5 flex items-center">
|
<div class="mr-2.5 flex items-center">
|
||||||
<IconifyIcon icon="ep:cpu" class="text-primary text-lg" />
|
<IconifyIcon icon="ep:cpu" class="text-lg text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 text-base font-bold">{{ item.name }}</div>
|
<div class="flex-1 text-base font-bold">{{ item.name }}</div>
|
||||||
<!-- 标识符 -->
|
<!-- 标识符 -->
|
||||||
@@ -198,7 +197,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="ep:data-line"
|
icon="ep:data-line"
|
||||||
class="text-primary text-lg"
|
class="text-lg text-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,14 +205,14 @@ onMounted(() => {
|
|||||||
<!-- 信息区域 -->
|
<!-- 信息区域 -->
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div class="mb-2.5 last:mb-0">
|
<div class="mb-2.5 last:mb-0">
|
||||||
<span class="text-muted-foreground mr-2.5">属性值</span>
|
<span class="mr-2.5 text-muted-foreground">属性值</span>
|
||||||
<span class="text-foreground font-bold">
|
<span class="font-bold text-foreground">
|
||||||
{{ formatValueWithUnit(item) }}
|
{{ formatValueWithUnit(item) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2.5 last:mb-0">
|
<div class="mb-2.5 last:mb-0">
|
||||||
<span class="text-muted-foreground mr-2.5">更新时间</span>
|
<span class="mr-2.5 text-muted-foreground">更新时间</span>
|
||||||
<span class="text-foreground text-sm">
|
<span class="text-sm text-foreground">
|
||||||
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ function getDeviceTypeColor(deviceType: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取设备状态信息
|
// 获取设备状态信息
|
||||||
function getStatusInfo(state: number | string | null | undefined) {
|
function getStatusInfo(state: null | number | string | undefined) {
|
||||||
const parsedState = Number(state);
|
const parsedState = Number(state);
|
||||||
const hasNumericState = Number.isFinite(parsedState);
|
const hasNumericState = Number.isFinite(parsedState);
|
||||||
const fallback = hasNumericState
|
const fallback = hasNumericState
|
||||||
@@ -396,21 +396,21 @@ defineExpose({
|
|||||||
.device-card {
|
.device-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: hsl(var(--card) / 0.95);
|
background: hsl(var(--card) / 95%);
|
||||||
border: 1px solid hsl(var(--border) / 0.6);
|
border: 1px solid hsl(var(--border) / 60%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px 0 hsl(var(--foreground) / 0.04),
|
0 1px 2px 0 hsl(var(--foreground) / 4%),
|
||||||
0 1px 6px -1px hsl(var(--foreground) / 0.05),
|
0 1px 6px -1px hsl(var(--foreground) / 5%),
|
||||||
0 2px 4px 0 hsl(var(--foreground) / 0.05);
|
0 2px 4px 0 hsl(var(--foreground) / 5%);
|
||||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px -2px hsl(var(--foreground) / 0.12),
|
0 1px 2px -2px hsl(var(--foreground) / 12%),
|
||||||
0 3px 6px 0 hsl(var(--foreground) / 0.1),
|
0 3px 6px 0 hsl(var(--foreground) / 10%),
|
||||||
0 5px 12px 4px hsl(var(--foreground) / 0.08);
|
0 5px 12px 4px hsl(var(--foreground) / 8%);
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ defineExpose({
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: hsl(var(--foreground) / 0.9);
|
color: hsl(var(--foreground) / 90%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ defineExpose({
|
|||||||
.label {
|
.label {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: hsl(var(--foreground) / 0.6);
|
color: hsl(var(--foreground) / 60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
@@ -505,7 +505,7 @@ defineExpose({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: hsl(var(--foreground) / 0.85);
|
color: hsl(var(--foreground) / 85%);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@ defineExpose({
|
|||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: hsl(var(--primary) / 0.85);
|
color: hsl(var(--primary) / 85%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,7 +524,7 @@ defineExpose({
|
|||||||
'SF Mono', Monaco, Inconsolata, 'Fira Code', Consolas, monospace;
|
'SF Mono', Monaco, Inconsolata, 'Fira Code', Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: hsl(var(--foreground) / 0.6);
|
color: hsl(var(--foreground) / 60%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,7 +537,7 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid hsl(var(--border) / 0.4);
|
border-top: 1px solid hsl(var(--border) / 40%);
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -561,8 +561,8 @@ defineExpose({
|
|||||||
|
|
||||||
&.btn-edit {
|
&.btn-edit {
|
||||||
color: hsl(var(--primary));
|
color: hsl(var(--primary));
|
||||||
background: hsl(var(--primary) / 0.12);
|
background: hsl(var(--primary) / 12%);
|
||||||
border-color: hsl(var(--primary) / 0.25);
|
border-color: hsl(var(--primary) / 25%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: hsl(var(--primary-foreground));
|
color: hsl(var(--primary-foreground));
|
||||||
@@ -573,8 +573,8 @@ defineExpose({
|
|||||||
|
|
||||||
&.btn-view {
|
&.btn-view {
|
||||||
color: hsl(var(--warning));
|
color: hsl(var(--warning));
|
||||||
background: hsl(var(--warning) / 0.12);
|
background: hsl(var(--warning) / 12%);
|
||||||
border-color: hsl(var(--warning) / 0.25);
|
border-color: hsl(var(--warning) / 25%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -590,11 +590,7 @@ defineExpose({
|
|||||||
hsl(var(--accent)) 40%,
|
hsl(var(--accent)) 40%,
|
||||||
hsl(var(--card)) 60%
|
hsl(var(--card)) 60%
|
||||||
);
|
);
|
||||||
border-color: color-mix(
|
border-color: color-mix(in srgb, hsl(var(--accent)) 55%, transparent);
|
||||||
in srgb,
|
|
||||||
hsl(var(--accent)) 55%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: hsl(var(--accent-foreground));
|
color: hsl(var(--accent-foreground));
|
||||||
@@ -607,8 +603,8 @@ defineExpose({
|
|||||||
flex: 0 0 32px;
|
flex: 0 0 32px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
background: hsl(var(--destructive) / 0.12);
|
background: hsl(var(--destructive) / 12%);
|
||||||
border-color: hsl(var(--destructive) / 0.3);
|
border-color: hsl(var(--destructive) / 30%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: hsl(var(--destructive-foreground));
|
color: hsl(var(--destructive-foreground));
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async function handleDownloadTemplate() {
|
|||||||
<Modal :title="getTitle" class="w-1/3">
|
<Modal :title="getTitle" class="w-1/3">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
<div class="mx-4 mt-4 text-center">
|
<div class="mx-4 mt-4 text-center">
|
||||||
<a class="text-primary cursor-pointer" @click="handleDownloadTemplate">
|
<a class="cursor-pointer text-primary" @click="handleDownloadTemplate">
|
||||||
下载导入模板
|
下载导入模板
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,72 @@
|
|||||||
/** 设备数量饼图配置 */
|
/** 消息趋势图表配置 */
|
||||||
// TODO @haohao:貌似没用到??
|
export function getMessageTrendChartOptions(
|
||||||
export function getDeviceCountChartOptions(
|
times: string[],
|
||||||
productCategoryDeviceCounts: Record<string, number>,
|
upstreamData: number[],
|
||||||
|
downstreamData: number[],
|
||||||
): any {
|
): any {
|
||||||
const data = Object.entries(productCategoryDeviceCounts).map(
|
|
||||||
([name, value]) => ({ name, value }),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'axis',
|
||||||
formatter: '{b}: {c} 个 ({d}%)',
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
label: {
|
||||||
|
backgroundColor: '#6a7985',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
|
data: ['上行消息', '下行消息'],
|
||||||
top: '5%',
|
top: '5%',
|
||||||
right: '10%',
|
|
||||||
orient: 'vertical',
|
|
||||||
},
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
top: '15%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: times,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '消息数量',
|
||||||
|
},
|
||||||
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '设备数量',
|
name: '上行消息',
|
||||||
type: 'pie',
|
type: 'line',
|
||||||
radius: ['50%', '80%'],
|
smooth: true,
|
||||||
center: ['30%', '50%'],
|
areaStyle: {
|
||||||
data,
|
opacity: 0.3,
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
label: {
|
emphasis: {
|
||||||
show: true,
|
focus: 'series',
|
||||||
formatter: '{b}: {c}',
|
},
|
||||||
|
data: upstreamData,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1890ff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '下行消息',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
},
|
||||||
|
data: downstreamData,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#52c41a',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -41,9 +74,9 @@ export function getDeviceCountChartOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仪表盘图表配置
|
* 设备状态仪表盘图表配置
|
||||||
*/
|
*/
|
||||||
export function getGaugeChartOptions(
|
export function getDeviceStateGaugeChartOptions(
|
||||||
value: number,
|
value: number,
|
||||||
max: number,
|
max: number,
|
||||||
color: string,
|
color: string,
|
||||||
@@ -53,12 +86,12 @@ export function getGaugeChartOptions(
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
startAngle: 180,
|
startAngle: 225,
|
||||||
endAngle: 0,
|
endAngle: -45,
|
||||||
min: 0,
|
min: 0,
|
||||||
max,
|
max,
|
||||||
center: ['50%', '70%'],
|
center: ['50%', '50%'],
|
||||||
radius: '120%',
|
radius: '80%',
|
||||||
progress: {
|
progress: {
|
||||||
show: true,
|
show: true,
|
||||||
width: 12,
|
width: 12,
|
||||||
@@ -69,29 +102,95 @@ export function getGaugeChartOptions(
|
|||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 12,
|
width: 12,
|
||||||
color: [[1, '#E5E7EB']],
|
color: [[1, '#E5E7EB']] as [number, string][],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
axisLabel: { show: false },
|
axisLabel: { show: false },
|
||||||
pointer: { show: false },
|
pointer: { show: false },
|
||||||
detail: {
|
|
||||||
valueAnimation: true,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color,
|
|
||||||
offsetCenter: [0, '-20%'],
|
|
||||||
formatter: '{value}',
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
show: true,
|
show: true,
|
||||||
offsetCenter: [0, '20%'],
|
offsetCenter: [0, '80%'],
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
},
|
},
|
||||||
|
detail: {
|
||||||
|
valueAnimation: true,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color,
|
||||||
|
offsetCenter: [0, '10%'],
|
||||||
|
formatter: (val: number) => `${val} 个`,
|
||||||
|
},
|
||||||
data: [{ value, name: title }],
|
data: [{ value, name: title }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备数量饼图配置
|
||||||
|
*/
|
||||||
|
export function getDeviceCountPieChartOptions(
|
||||||
|
data: Array<{ name: string; value: number }>,
|
||||||
|
): any {
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{b}: {c} 个 ({d}%)',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'horizontal',
|
||||||
|
bottom: '10px',
|
||||||
|
left: 'center',
|
||||||
|
icon: 'circle',
|
||||||
|
itemWidth: 10,
|
||||||
|
itemHeight: 10,
|
||||||
|
itemGap: 12,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
pageButtonPosition: 'end',
|
||||||
|
pageIconSize: 12,
|
||||||
|
pageTextStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
pageFormatter: '{current}/{total}',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '设备数量',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['35%', '55%'],
|
||||||
|
center: ['50%', '40%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,7 @@
|
|||||||
/**
|
|
||||||
* IoT 首页数据配置文件
|
|
||||||
*
|
|
||||||
* 该文件封装了 IoT 首页所需的:
|
|
||||||
* - 统计数据接口定义
|
|
||||||
* - 业务逻辑函数
|
|
||||||
* - 工具函数
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
import { getStatisticsSummary } from '#/api/iot/statistics';
|
|
||||||
|
|
||||||
/** 统计数据接口 - 使用 API 定义的类型 */
|
|
||||||
export type StatsData = IotStatisticsApi.StatisticsSummary;
|
|
||||||
|
|
||||||
/** 默认统计数据 */
|
/** 默认统计数据 */
|
||||||
export const defaultStatsData: StatsData = {
|
export const defaultStatsData: IotStatisticsApi.StatisticsSummary = {
|
||||||
productCategoryCount: 0,
|
productCategoryCount: 0,
|
||||||
productCount: 0,
|
productCount: 0,
|
||||||
deviceCount: 0,
|
deviceCount: 0,
|
||||||
@@ -31,84 +15,3 @@ export const defaultStatsData: StatsData = {
|
|||||||
deviceInactiveCount: 0,
|
deviceInactiveCount: 0,
|
||||||
productCategoryDeviceCounts: {},
|
productCategoryDeviceCounts: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载统计数据
|
|
||||||
* @returns Promise<StatsData>
|
|
||||||
*/
|
|
||||||
export async function loadStatisticsData(): Promise<StatsData> {
|
|
||||||
try {
|
|
||||||
const data = await getStatisticsSummary();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取统计数据出错:', error);
|
|
||||||
console.warn('使用 Mock 数据,请检查后端接口是否已实现');
|
|
||||||
|
|
||||||
// 返回 Mock 数据用于开发调试
|
|
||||||
return {
|
|
||||||
productCategoryCount: 12,
|
|
||||||
productCount: 45,
|
|
||||||
deviceCount: 328,
|
|
||||||
deviceMessageCount: 15_678,
|
|
||||||
productCategoryTodayCount: 2,
|
|
||||||
productTodayCount: 5,
|
|
||||||
deviceTodayCount: 23,
|
|
||||||
deviceMessageTodayCount: 1234,
|
|
||||||
deviceOnlineCount: 256,
|
|
||||||
deviceOfflineCount: 48,
|
|
||||||
deviceInactiveCount: 24,
|
|
||||||
productCategoryDeviceCounts: {
|
|
||||||
智能家居: 120,
|
|
||||||
工业设备: 98,
|
|
||||||
环境监测: 65,
|
|
||||||
智能穿戴: 45,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 首页业务逻辑 Hook
|
|
||||||
* 封装了首页的所有业务逻辑和状态管理
|
|
||||||
*/
|
|
||||||
export function useIotHome() {
|
|
||||||
const loading = ref(true);
|
|
||||||
const statsData = ref<StatsData>(defaultStatsData);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载数据
|
|
||||||
*/
|
|
||||||
async function loadData() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
statsData.value = await loadStatisticsData();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取统计数据出错:', error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
statsData,
|
|
||||||
loadData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:是不是删除下哈;
|
|
||||||
/** 格式化数字 - 大数字显示为 K/M */
|
|
||||||
export function formatNumber(num: number): string {
|
|
||||||
if (num >= 1_000_000) {
|
|
||||||
return `${(num / 1_000_000).toFixed(1)}M`;
|
|
||||||
}
|
|
||||||
if (num >= 1000) {
|
|
||||||
return `${(num / 1000).toFixed(1)}K`;
|
|
||||||
}
|
|
||||||
return num.toString();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,78 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Page } from '@vben/common-ui';
|
// TODO @芋艿
|
||||||
|
import type { StatsData } from './data';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ComparisonCard, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Col, Row } from 'ant-design-vue';
|
import { Col, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useIotHome } from './data';
|
import { getStatisticsSummary } from '#/api/iot/statistics';
|
||||||
import ComparisonCard from './modules/comparison-card.vue';
|
|
||||||
|
import { defaultStatsData } from './data';
|
||||||
import DeviceCountCard from './modules/device-count-card.vue';
|
import DeviceCountCard from './modules/device-count-card.vue';
|
||||||
import DeviceStateCountCard from './modules/device-state-count-card.vue';
|
import DeviceStateCountCard from './modules/device-state-count-card.vue';
|
||||||
import MessageTrendCard from './modules/message-trend-card.vue';
|
import MessageTrendCard from './modules/message-trend-card.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTHome' });
|
defineOptions({ name: 'IoTHome' });
|
||||||
|
|
||||||
// TODO @haohao:相关的方法,拿到 index.vue 里,data.ts 只放 schema;
|
const loading = ref(true);
|
||||||
const { loading, statsData } = useIotHome();
|
const statsData = ref<StatsData>(defaultStatsData);
|
||||||
|
|
||||||
|
/** 加载统计数据 */
|
||||||
|
async function loadStatisticsData(): Promise<StatsData> {
|
||||||
|
try {
|
||||||
|
return await getStatisticsSummary();
|
||||||
|
} catch (error) {
|
||||||
|
// TODO @haohao:后续记得删除下哈。catch 部分可以删除
|
||||||
|
// 开发环境:记录错误信息,便于调试
|
||||||
|
console.error('获取统计数据出错:', error);
|
||||||
|
// 开发环境:提示使用 Mock 数据,提醒检查后端接口
|
||||||
|
console.warn('使用 Mock 数据,请检查后端接口是否已实现');
|
||||||
|
|
||||||
|
// TODO @haohao:后续记得删除下哈。
|
||||||
|
// 开发调试:返回 Mock 数据,确保前端功能正常开发
|
||||||
|
// 生产环境:建议移除 Mock 数据,直接抛出错误或返回空数据
|
||||||
|
return {
|
||||||
|
productCategoryCount: 12,
|
||||||
|
productCount: 45,
|
||||||
|
deviceCount: 328,
|
||||||
|
deviceMessageCount: 15_678,
|
||||||
|
productCategoryTodayCount: 2,
|
||||||
|
productTodayCount: 5,
|
||||||
|
deviceTodayCount: 23,
|
||||||
|
deviceMessageTodayCount: 1234,
|
||||||
|
deviceOnlineCount: 256,
|
||||||
|
deviceOfflineCount: 48,
|
||||||
|
deviceInactiveCount: 24,
|
||||||
|
productCategoryDeviceCounts: {
|
||||||
|
智能家居: 120,
|
||||||
|
工业设备: 98,
|
||||||
|
环境监测: 65,
|
||||||
|
智能穿戴: 45,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载数据 */
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
statsData.value = await loadStatisticsData();
|
||||||
|
} catch (error) {
|
||||||
|
// TODO @haohao:后续记得删除下哈。catch 部分可以删除
|
||||||
|
console.error('获取统计数据出错:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时加载数据 */
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { CountTo } from '@vben/common-ui';
|
|
||||||
import { createIconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { Card } from 'ant-design-vue';
|
|
||||||
|
|
||||||
// TODO @haohao:这个可以迁移到 packages/effects/common-ui/src/components/card/comparison-card
|
|
||||||
|
|
||||||
defineOptions({ name: 'ComparisonCard' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
icon: string;
|
|
||||||
iconColor?: string;
|
|
||||||
loading?: boolean;
|
|
||||||
title: string;
|
|
||||||
todayCount: number;
|
|
||||||
value: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const iconMap: Record<string, any> = {
|
|
||||||
menu: createIconifyIcon('ant-design:appstore-outlined'),
|
|
||||||
box: createIconifyIcon('ant-design:box-plot-outlined'),
|
|
||||||
cpu: createIconifyIcon('ant-design:cluster-outlined'),
|
|
||||||
message: createIconifyIcon('ant-design:message-outlined'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card class="stat-card" :loading="loading">
|
|
||||||
<div class="flex h-full flex-col">
|
|
||||||
<div class="mb-4 flex items-start justify-between">
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<span class="mb-2 text-sm font-medium text-gray-500">
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
<span class="text-3xl font-bold text-gray-800">
|
|
||||||
<span v-if="value === -1">--</span>
|
|
||||||
<CountTo v-else :end-val="value" :duration="1000" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div :class="`text-4xl ${iconColor}`">
|
|
||||||
<IconComponent />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-auto border-t border-gray-100 pt-3">
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<span class="text-gray-400">今日新增</span>
|
|
||||||
<span v-if="todayCount === -1" class="text-gray-400">--</span>
|
|
||||||
<span v-else class="font-medium text-green-500">
|
|
||||||
+{{ todayCount }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/** TODO tindwind */
|
|
||||||
.stat-card {
|
|
||||||
height: 160px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card:hover {
|
|
||||||
box-shadow: 0 6px 20px rgb(0 0 0 / 8%);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card :deep(.ant-card-body) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,6 +7,8 @@ import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|||||||
|
|
||||||
import { Card, Empty } from 'ant-design-vue';
|
import { Card, Empty } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDeviceCountPieChartOptions } from '../chart-options';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceCountCard' });
|
defineOptions({ name: 'DeviceCountCard' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -27,77 +29,16 @@ const hasData = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** 初始化图表 */
|
/** 初始化图表 */
|
||||||
function initChart() {
|
async function initChart() {
|
||||||
if (!hasData.value) {
|
if (!hasData.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @haohao:await nextTick();
|
await nextTick();
|
||||||
nextTick(() => {
|
const data = Object.entries(props.statsData.productCategoryDeviceCounts).map(
|
||||||
const data = Object.entries(
|
([name, value]) => ({ name, value }),
|
||||||
props.statsData.productCategoryDeviceCounts,
|
);
|
||||||
).map(([name, value]) => ({ name, value }));
|
await renderEcharts(getDeviceCountPieChartOptions(data));
|
||||||
|
|
||||||
// TODO @haohao:看看 chart-options 怎么提取出去,类似 apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts 写法
|
|
||||||
renderEcharts({
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} 个 ({d}%)',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
type: 'scroll',
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: '10px',
|
|
||||||
left: 'center',
|
|
||||||
icon: 'circle',
|
|
||||||
itemWidth: 10,
|
|
||||||
itemHeight: 10,
|
|
||||||
itemGap: 12,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
pageButtonPosition: 'end',
|
|
||||||
pageIconSize: 12,
|
|
||||||
pageTextStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
pageFormatter: '{current}/{total}',
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '设备数量',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['35%', '55%'],
|
|
||||||
center: ['50%', '40%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 8,
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监听数据变化 */
|
/** 监听数据变化 */
|
||||||
@@ -116,7 +57,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card title="设备数量统计" :loading="loading" class="chart-card">
|
<Card title="设备数量统计" :loading="loading" class="h-full">
|
||||||
<div
|
<div
|
||||||
v-if="loading && !hasData"
|
v-if="loading && !hasData"
|
||||||
class="flex h-[300px] items-center justify-center"
|
class="flex h-[300px] items-center justify-center"
|
||||||
@@ -136,12 +77,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/** TODO tindwind */
|
:deep(.ant-card-body) {
|
||||||
.chart-card {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card :deep(.ant-card-body) {
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|||||||
|
|
||||||
import { Card, Col, Empty, Row } from 'ant-design-vue';
|
import { Card, Col, Empty, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDeviceStateGaugeChartOptions } from '../chart-options';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceStateCountCard' });
|
defineOptions({ name: 'DeviceStateCountCard' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -30,81 +32,41 @@ const hasData = computed(() => {
|
|||||||
return props.statsData.deviceCount !== 0;
|
return props.statsData.deviceCount !== 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 获取仪表盘配置 */
|
|
||||||
// TODO @haohao:看看 chart-options 怎么提取出去,类似 apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts 写法
|
|
||||||
const getGaugeOption = (value: number, color: string, title: string): any => {
|
|
||||||
return {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'gauge',
|
|
||||||
startAngle: 225,
|
|
||||||
endAngle: -45,
|
|
||||||
min: 0,
|
|
||||||
max: props.statsData.deviceCount || 100,
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
radius: '80%',
|
|
||||||
progress: {
|
|
||||||
show: true,
|
|
||||||
width: 12,
|
|
||||||
itemStyle: {
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
width: 12,
|
|
||||||
color: [[1, '#E5E7EB']] as [number, string][],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: { show: false },
|
|
||||||
splitLine: { show: false },
|
|
||||||
axisLabel: { show: false },
|
|
||||||
pointer: { show: false },
|
|
||||||
title: {
|
|
||||||
show: true,
|
|
||||||
offsetCenter: [0, '80%'],
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#666',
|
|
||||||
},
|
|
||||||
detail: {
|
|
||||||
valueAnimation: true,
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color,
|
|
||||||
offsetCenter: [0, '10%'],
|
|
||||||
formatter: (val: number) => `${val} 个`,
|
|
||||||
},
|
|
||||||
data: [{ value, name: title }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 初始化图表 */
|
/** 初始化图表 */
|
||||||
function initCharts() {
|
async function initCharts() {
|
||||||
if (!hasData.value) {
|
if (!hasData.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @haohao:await nextTick();
|
await nextTick();
|
||||||
nextTick(() => {
|
const max = props.statsData.deviceCount || 100;
|
||||||
// 在线设备
|
// 在线设备
|
||||||
renderOnlineChart(
|
await renderOnlineChart(
|
||||||
getGaugeOption(props.statsData.deviceOnlineCount, '#52c41a', '在线设备'),
|
getDeviceStateGaugeChartOptions(
|
||||||
);
|
props.statsData.deviceOnlineCount,
|
||||||
// 离线设备
|
max,
|
||||||
renderOfflineChart(
|
'#52c41a',
|
||||||
getGaugeOption(props.statsData.deviceOfflineCount, '#ff4d4f', '离线设备'),
|
'在线设备',
|
||||||
);
|
),
|
||||||
// 待激活设备
|
);
|
||||||
renderInactiveChart(
|
// 离线设备
|
||||||
getGaugeOption(
|
await renderOfflineChart(
|
||||||
props.statsData.deviceInactiveCount,
|
getDeviceStateGaugeChartOptions(
|
||||||
'#1890ff',
|
props.statsData.deviceOfflineCount,
|
||||||
'待激活设备',
|
max,
|
||||||
),
|
'#ff4d4f',
|
||||||
);
|
'离线设备',
|
||||||
});
|
),
|
||||||
|
);
|
||||||
|
// 待激活设备
|
||||||
|
await renderInactiveChart(
|
||||||
|
getDeviceStateGaugeChartOptions(
|
||||||
|
props.statsData.deviceInactiveCount,
|
||||||
|
max,
|
||||||
|
'#1890ff',
|
||||||
|
'待激活设备',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监听数据变化 */
|
/** 监听数据变化 */
|
||||||
@@ -123,7 +85,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card title="设备状态统计" :loading="loading" class="chart-card">
|
<Card title="设备状态统计" :loading="loading" class="h-full">
|
||||||
<div
|
<div
|
||||||
v-if="loading && !hasData"
|
v-if="loading && !hasData"
|
||||||
class="flex h-[300px] items-center justify-center"
|
class="flex h-[300px] items-center justify-center"
|
||||||
@@ -151,12 +113,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/** TODO tindwind */
|
:deep(.ant-card-body) {
|
||||||
.chart-card {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card :deep(.ant-card-body) {
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,256 +5,177 @@ import type { IotStatisticsApi } from '#/api/iot/statistics';
|
|||||||
|
|
||||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
import { Button, Card, DatePicker, Empty, Space } from 'ant-design-vue';
|
import { Card, Empty, Select } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { getDeviceMessageSummaryByDate } from '#/api/iot/statistics';
|
import { getDeviceMessageSummaryByDate } from '#/api/iot/statistics';
|
||||||
|
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
|
|
||||||
|
import { getMessageTrendChartOptions } from '../chart-options';
|
||||||
|
|
||||||
defineOptions({ name: 'MessageTrendCard' });
|
defineOptions({ name: 'MessageTrendCard' });
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
|
|
||||||
const messageChartRef = ref();
|
const messageChartRef = ref();
|
||||||
const { renderEcharts } = useEcharts(messageChartRef);
|
const { renderEcharts } = useEcharts(messageChartRef);
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const messageData = ref<IotStatisticsApi.DeviceMessageSummaryByDate[]>([]);
|
const messageData = ref<IotStatisticsApi.DeviceMessageSummaryByDate[]>([]);
|
||||||
const activeTimeRange = ref('7d'); // 当前选中的时间范围
|
|
||||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
|
|
||||||
|
|
||||||
// TODO @haohao:这个貌似没迁移对。它是时间范围、事件间隔;
|
/** 时间范围(仅日期,不包含时分秒) */
|
||||||
|
const dateRange = ref<[string, string]>([
|
||||||
|
// 默认显示最近一周的数据(包含今天和前六天)
|
||||||
|
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||||
|
dayjs().format('YYYY-MM-DD'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 将日期范围转换为带时分秒的格式 */
|
||||||
|
function formatDateRangeWithTime(dates: [string, string]): [string, string] {
|
||||||
|
return [`${dates[0]} 00:00:00`, `${dates[1]} 23:59:59`];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询参数 */
|
||||||
const queryParams = reactive<IotStatisticsApi.DeviceMessageReq>({
|
const queryParams = reactive<IotStatisticsApi.DeviceMessageReq>({
|
||||||
interval: 1, // 按天
|
interval: 1, // 默认按天
|
||||||
times: [],
|
times: formatDateRangeWithTime(dateRange.value),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 是否有数据
|
/** 是否有数据 */
|
||||||
const hasData = computed(() => {
|
const hasData = computed(() => {
|
||||||
return messageData.value && messageData.value.length > 0;
|
return messageData.value && messageData.value.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:注释风格,应该是 /** */ 在方法上;然后变量在字段后面 // 。。。
|
/** 时间间隔字典选项 */
|
||||||
// 设置时间范围
|
const intervalOptions = computed(() =>
|
||||||
function setTimeRange(range: string) {
|
getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number').map((item) => ({
|
||||||
activeTimeRange.value = range;
|
label: item.label,
|
||||||
dateRange.value = undefined; // 清空自定义时间选择
|
value: item.value as number,
|
||||||
|
})),
|
||||||
let start: Dayjs;
|
);
|
||||||
const end = dayjs();
|
|
||||||
|
|
||||||
switch (range) {
|
|
||||||
case '1h': {
|
|
||||||
start = dayjs().subtract(1, 'hour');
|
|
||||||
queryParams.interval = 1; // 按分钟
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '7d': {
|
|
||||||
start = dayjs().subtract(7, 'day');
|
|
||||||
queryParams.interval = 1; // 按天
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '24h': {
|
|
||||||
start = dayjs().subtract(24, 'hour');
|
|
||||||
queryParams.interval = 1; // 按小时
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
start = dayjs().subtract(7, 'day');
|
|
||||||
queryParams.interval = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:可以使用 formatDateTime;
|
|
||||||
queryParams.times = [
|
|
||||||
start.format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
end.format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
];
|
|
||||||
|
|
||||||
|
/** 处理查询操作 */
|
||||||
|
function handleQuery() {
|
||||||
fetchMessageData();
|
fetchMessageData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理自定义日期选择
|
/** 处理时间范围变化 */
|
||||||
function handleDateChange() {
|
function handleDateRangeChange(times?: [Dayjs, Dayjs]) {
|
||||||
if (dateRange.value && dateRange.value.length === 2) {
|
if (!times || times.length !== 2) {
|
||||||
activeTimeRange.value = ''; // 清空快捷选择
|
return;
|
||||||
queryParams.interval = 1; // 按天
|
|
||||||
queryParams.times = [
|
|
||||||
// TODO @haohao:可以使用 formatDateTime;
|
|
||||||
dateRange.value[0].startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
dateRange.value[1].endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
];
|
|
||||||
fetchMessageData();
|
|
||||||
}
|
}
|
||||||
|
dateRange.value = [
|
||||||
|
dayjs(times[0]).format('YYYY-MM-DD'),
|
||||||
|
dayjs(times[1]).format('YYYY-MM-DD'),
|
||||||
|
];
|
||||||
|
// 将选择的日期转换为带时分秒的格式(开始日期 00:00:00,结束日期 23:59:59)
|
||||||
|
queryParams.times = formatDateRangeWithTime(dateRange.value);
|
||||||
|
handleQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取消息统计数据
|
/** 处理时间间隔变化 */
|
||||||
|
function handleIntervalChange() {
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息统计数据 */
|
||||||
async function fetchMessageData() {
|
async function fetchMessageData() {
|
||||||
if (!queryParams.times || queryParams.times.length !== 2) return;
|
if (!queryParams.times || queryParams.times.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
messageData.value = await getDeviceMessageSummaryByDate(queryParams);
|
messageData.value = await getDeviceMessageSummaryByDate(queryParams);
|
||||||
await nextTick();
|
|
||||||
initChart();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// TODO @haohao:catch 可以删除哈;
|
||||||
|
// 开发环境:记录错误信息,便于调试
|
||||||
console.error('获取消息统计数据失败:', error);
|
console.error('获取消息统计数据失败:', error);
|
||||||
|
// 错误时清空数据,避免显示错误的数据
|
||||||
messageData.value = [];
|
messageData.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
await renderChartWhenReady();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
/** 初始化图表 */
|
||||||
function initChart() {
|
function initChart() {
|
||||||
if (!hasData.value) return;
|
// 检查数据是否存在
|
||||||
|
if (!hasData.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const times = messageData.value.map((item) => item.time);
|
const times = messageData.value.map((item) => item.time);
|
||||||
const upstreamData = messageData.value.map((item) => item.upstreamCount);
|
const upstreamData = messageData.value.map((item) => item.upstreamCount);
|
||||||
const downstreamData = messageData.value.map((item) => item.downstreamCount);
|
const downstreamData = messageData.value.map((item) => item.downstreamCount);
|
||||||
|
renderEcharts(
|
||||||
// TODO @haohao:看看 chart-options 怎么提取出去,类似 apps/web-antd/src/views/mall/statistics/member/modules/area-chart-options.ts 写法
|
getMessageTrendChartOptions(times, upstreamData, downstreamData),
|
||||||
renderEcharts({
|
);
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'cross',
|
|
||||||
label: {
|
|
||||||
backgroundColor: '#6a7985',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: ['上行消息', '下行消息'],
|
|
||||||
top: '5%',
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '3%',
|
|
||||||
top: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
boundaryGap: false,
|
|
||||||
data: times,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
yAxis: [
|
|
||||||
{
|
|
||||||
type: 'value',
|
|
||||||
name: '消息数量',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '上行消息',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
opacity: 0.3,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series',
|
|
||||||
},
|
|
||||||
data: upstreamData,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#1890ff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '下行消息',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
opacity: 0.3,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series',
|
|
||||||
},
|
|
||||||
data: downstreamData,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#52c41a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时查询数据
|
/** 确保图表容器已经可见后再渲染 */
|
||||||
|
async function renderChartWhenReady() {
|
||||||
|
if (!hasData.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 等待 Card loading 状态、v-show 等 DOM 更新完成
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时查询数据 */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeRange('7d'); // 默认显示近一周数据
|
fetchMessageData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="chart-card" :loading="loading">
|
<Card class="h-full">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<span class="text-base font-medium">上下行消息量统计</span>
|
<span class="text-base font-medium text-gray-600">消息量统计</span>
|
||||||
<Space :size="8">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<Button
|
<div class="flex items-center gap-3">
|
||||||
:type="activeTimeRange === '1h' ? 'primary' : 'default'"
|
<span class="whitespace-nowrap text-sm text-gray-500">
|
||||||
size="small"
|
时间范围
|
||||||
@click="setTimeRange('1h')"
|
</span>
|
||||||
>
|
<ShortcutDateRangePicker @change="handleDateRangeChange" />
|
||||||
最近1小时
|
</div>
|
||||||
</Button>
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<span class="text-sm text-gray-500">时间间隔</span>
|
||||||
:type="activeTimeRange === '24h' ? 'primary' : 'default'"
|
<Select
|
||||||
size="small"
|
v-model:value="queryParams.interval"
|
||||||
@click="setTimeRange('24h')"
|
:options="intervalOptions"
|
||||||
>
|
placeholder="间隔类型"
|
||||||
最近24小时
|
:style="{ width: '80px' }"
|
||||||
</Button>
|
@change="handleIntervalChange"
|
||||||
<Button
|
/>
|
||||||
:type="activeTimeRange === '7d' ? 'primary' : 'default'"
|
</div>
|
||||||
size="small"
|
</div>
|
||||||
@click="setTimeRange('7d')"
|
|
||||||
>
|
|
||||||
近一周
|
|
||||||
</Button>
|
|
||||||
<RangePicker
|
|
||||||
v-model:value="dateRange"
|
|
||||||
format="YYYY-MM-DD"
|
|
||||||
:placeholder="['开始时间', '结束时间']"
|
|
||||||
@change="handleDateChange"
|
|
||||||
size="small"
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="loading" class="flex h-[350px] items-center justify-center">
|
<!-- 加载中状态 -->
|
||||||
|
<div
|
||||||
|
v-show="loading && !hasData"
|
||||||
|
class="flex h-[300px] items-center justify-center"
|
||||||
|
>
|
||||||
<Empty description="加载中..." />
|
<Empty description="加载中..." />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 无数据状态 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="!hasData"
|
v-show="!loading && !hasData"
|
||||||
class="flex h-[350px] items-center justify-center"
|
class="flex h-[300px] items-center justify-center"
|
||||||
>
|
>
|
||||||
<Empty description="暂无数据" />
|
<Empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<!-- 图表容器 - 使用 v-show 而非 v-if,确保组件始终挂载 -->
|
||||||
<EchartsUI ref="messageChartRef" class="h-[350px] w-full" />
|
<div v-show="hasData">
|
||||||
|
<EchartsUI ref="messageChartRef" class="h-[300px] w-full" />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.chart-card {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card :deep(.ant-card-body) {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card :deep(.ant-card-head) {
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -123,13 +123,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="ant-design:download-outlined"
|
icon="ant-design:download-outlined"
|
||||||
class="text-primary shrink-0 align-middle text-base"
|
class="shrink-0 align-middle text-base text-primary"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
:href="row.fileUrl"
|
:href="row.fileUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
download
|
download
|
||||||
class="text-primary cursor-pointer align-middle hover:underline"
|
class="cursor-pointer align-middle text-primary hover:underline"
|
||||||
>
|
>
|
||||||
下载固件
|
下载固件
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export {default as HttpConfigForm} from './http-config-form.vue';
|
export { default as HttpConfigForm } from './http-config-form.vue';
|
||||||
export {default as KafkaMqConfigForm} from './kafka-mq-config-form.vue';
|
export { default as KafkaMqConfigForm } from './kafka-mq-config-form.vue';
|
||||||
export {default as MqttConfigForm} from './mqtt-config-form.vue';
|
export { default as MqttConfigForm } from './mqtt-config-form.vue';
|
||||||
export {default as RabbitMqConfigForm} from './rabbit-mq-config-form.vue';
|
export { default as RabbitMqConfigForm } from './rabbit-mq-config-form.vue';
|
||||||
export {default as RedisStreamConfigForm} from './redis-stream-config-form.vue';
|
export { default as RedisStreamConfigForm } from './redis-stream-config-form.vue';
|
||||||
export {default as RocketMqConfigForm} from './rocket-mq-config-form.vue';
|
export { default as RocketMqConfigForm } from './rocket-mq-config-form.vue';
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ function handleDeviceChange(_: any) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理属性变化事件
|
* 处理属性变化事件
|
||||||
* @param propertyInfo 属性信息对象
|
* @param propertyInfo.config 属性配置
|
||||||
|
* @param propertyInfo.type 属性类型
|
||||||
*/
|
*/
|
||||||
function handlePropertyChange(propertyInfo: { config: any; type: string }) {
|
function handlePropertyChange(propertyInfo: { config: any; type: string }) {
|
||||||
propertyType.value = propertyInfo.type;
|
propertyType.value = propertyInfo.type;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ watch(
|
|||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<div v-else class="text-secondary text-sm">无需设置时间值</div>
|
<div v-else class="text-sm text-secondary">无需设置时间值</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
|||||||
@@ -365,10 +365,10 @@ function handlePropertyChange(propertyInfo: any) {
|
|||||||
|
|
||||||
<!-- 其他触发类型的提示 -->
|
<!-- 其他触发类型的提示 -->
|
||||||
<div v-else class="py-5 text-center">
|
<div v-else class="py-5 text-center">
|
||||||
<p class="text-secondary mb-1 text-sm">
|
<p class="mb-1 text-sm text-secondary">
|
||||||
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-secondary text-xs">此触发类型暂不需要配置额外条件</p>
|
<p class="text-xs text-secondary">此触发类型暂不需要配置额外条件</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -108,18 +108,18 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
|||||||
>
|
>
|
||||||
<!-- 条件配置 -->
|
<!-- 条件配置 -->
|
||||||
<div
|
<div
|
||||||
class="rounded-3px border-border bg-fill-color-blank border shadow-sm"
|
class="rounded-3px bg-fill-color-blank border border-border shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="rounded-t-1 border-border bg-fill-color-blank flex items-center justify-between border-b p-3"
|
class="rounded-t-1 bg-fill-color-blank flex items-center justify-between border-b border-border p-3"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="bg-primary flex size-5 items-center justify-center rounded-full text-xs font-bold text-white"
|
class="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-white"
|
||||||
>
|
>
|
||||||
{{ conditionIndex + 1 }}
|
{{ conditionIndex + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-primary text-base font-bold">
|
<span class="text-base font-bold text-primary">
|
||||||
条件 {{ conditionIndex + 1 }}
|
条件 {{ conditionIndex + 1 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
|||||||
<IconifyIcon icon="lucide:plus" />
|
<IconifyIcon icon="lucide:plus" />
|
||||||
继续添加条件
|
继续添加条件
|
||||||
</Button>
|
</Button>
|
||||||
<span class="text-secondary mt-2 block text-xs">
|
<span class="mt-2 block text-xs text-secondary">
|
||||||
最多可添加 {{ maxConditions }} 个条件
|
最多可添加 {{ maxConditions }} 个条件
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -451,8 +451,8 @@ watch(
|
|||||||
<!-- 弹出层内容 -->
|
<!-- 弹出层内容 -->
|
||||||
<div class="json-params-detail-content">
|
<div class="json-params-detail-content">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<div class="mb-4 flex items-center gap-2">
|
||||||
<IconifyIcon :icon="titleIcon" class="text-primary text-lg" />
|
<IconifyIcon :icon="titleIcon" class="text-lg text-primary" />
|
||||||
<span class="text-primary text-base font-bold">
|
<span class="text-base font-bold text-primary">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -463,9 +463,9 @@ watch(
|
|||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="mb-2 flex items-center gap-2">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="paramsIcon"
|
:icon="paramsIcon"
|
||||||
class="text-primary text-base"
|
class="text-base text-primary"
|
||||||
/>
|
/>
|
||||||
<span class="text-primary text-base font-bold">
|
<span class="text-base font-bold text-primary">
|
||||||
{{ paramsLabel }}
|
{{ paramsLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -473,10 +473,10 @@ watch(
|
|||||||
<div
|
<div
|
||||||
v-for="param in paramsList"
|
v-for="param in paramsList"
|
||||||
:key="param.identifier"
|
:key="param.identifier"
|
||||||
class="bg-card flex items-center justify-between rounded-lg p-2"
|
class="flex items-center justify-between rounded-lg bg-card p-2"
|
||||||
>
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-primary text-base font-bold">
|
<div class="text-base font-bold text-primary">
|
||||||
{{ param.name }}
|
{{ param.name }}
|
||||||
<Tag
|
<Tag
|
||||||
v-if="param.required"
|
v-if="param.required"
|
||||||
@@ -487,7 +487,7 @@ watch(
|
|||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-secondary text-xs">
|
<div class="text-xs text-secondary">
|
||||||
{{ param.identifier }}
|
{{ param.identifier }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -495,7 +495,7 @@ watch(
|
|||||||
<Tag :type="getParamTypeTag(param.dataType)" size="small">
|
<Tag :type="getParamTypeTag(param.dataType)" size="small">
|
||||||
{{ getParamTypeName(param.dataType) }}
|
{{ getParamTypeName(param.dataType) }}
|
||||||
</Tag>
|
</Tag>
|
||||||
<span class="text-secondary text-xs">
|
<span class="text-xs text-secondary">
|
||||||
{{ getExampleValue(param) }}
|
{{ getExampleValue(param) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -503,11 +503,11 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-6 mt-3">
|
<div class="ml-6 mt-3">
|
||||||
<div class="text-secondary mb-1 text-xs">
|
<div class="mb-1 text-xs text-secondary">
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
class="bg-card border-l-3px border-primary text-primary overflow-x-auto rounded-lg p-3 text-sm"
|
class="border-l-3px overflow-x-auto rounded-lg border-primary bg-card p-3 text-sm text-primary"
|
||||||
>
|
>
|
||||||
<code>{{ generateExampleJson() }}</code>
|
<code>{{ generateExampleJson() }}</code>
|
||||||
</pre>
|
</pre>
|
||||||
@@ -517,7 +517,7 @@ watch(
|
|||||||
<!-- 无参数提示 -->
|
<!-- 无参数提示 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="py-4 text-center">
|
<div class="py-4 text-center">
|
||||||
<p class="text-secondary text-sm">
|
<p class="text-sm text-secondary">
|
||||||
{{ emptyMessage }}
|
{{ emptyMessage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -550,7 +550,7 @@ watch(
|
|||||||
|
|
||||||
<!-- 快速填充按钮 -->
|
<!-- 快速填充按钮 -->
|
||||||
<div v-if="paramsList.length > 0" class="flex items-center gap-2">
|
<div v-if="paramsList.length > 0" class="flex items-center gap-2">
|
||||||
<span class="text-secondary text-xs">
|
<span class="text-xs text-secondary">
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL }}
|
||||||
</span>
|
</span>
|
||||||
<Button size="small" type="primary" plain @click="fillExampleJson">
|
<Button size="small" type="primary" plain @click="fillExampleJson">
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ watch(
|
|||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
style="width: auto !important"
|
style="width: auto !important"
|
||||||
/>
|
/>
|
||||||
<span class="text-secondary whitespace-nowrap text-xs"> 至 </span>
|
<span class="whitespace-nowrap text-xs text-secondary"> 至 </span>
|
||||||
<Input
|
<Input
|
||||||
v-model="rangeEnd"
|
v-model="rangeEnd"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
@@ -231,7 +231,7 @@ watch(
|
|||||||
v-if="listPreview.length > 0"
|
v-if="listPreview.length > 0"
|
||||||
class="mt-2 flex flex-wrap items-center gap-1"
|
class="mt-2 flex flex-wrap items-center gap-1"
|
||||||
>
|
>
|
||||||
<span class="text-secondary text-xs"> 解析结果: </span>
|
<span class="text-xs text-secondary"> 解析结果: </span>
|
||||||
<Tag
|
<Tag
|
||||||
v-for="(item, index) in listPreview"
|
v-for="(item, index) in listPreview"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -282,7 +282,7 @@ watch(
|
|||||||
:content="`单位:${propertyConfig.unit}`"
|
:content="`单位:${propertyConfig.unit}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="text-secondary px-1 text-xs">
|
<span class="px-1 text-xs text-secondary">
|
||||||
{{ propertyConfig.unit }}
|
{{ propertyConfig.unit }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function onActionTypeChange(action: Action, type: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="border-primary rounded-lg border" shadow="never">
|
<Card class="rounded-lg border border-primary" shadow="never">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-8px flex items-center">
|
<div class="gap-8px flex items-center">
|
||||||
@@ -275,14 +275,14 @@ function onActionTypeChange(action: Action, type: any) {
|
|||||||
action.type ===
|
action.type ===
|
||||||
IotRuleSceneActionTypeEnum.ALERT_TRIGGER.toString()
|
IotRuleSceneActionTypeEnum.ALERT_TRIGGER.toString()
|
||||||
"
|
"
|
||||||
class="border-border bg-fill-color-blank rounded-lg border p-4"
|
class="bg-fill-color-blank rounded-lg border border-border p-4"
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="mb-2 flex items-center gap-2">
|
||||||
<IconifyIcon icon="ep:warning" class="text-warning text-base" />
|
<IconifyIcon icon="ep:warning" class="text-base text-warning" />
|
||||||
<span class="font-600 text-primary text-sm">触发告警</span>
|
<span class="font-600 text-sm text-primary">触发告警</span>
|
||||||
<Tag size="small" type="warning">自动执行</Tag>
|
<Tag size="small" type="warning">自动执行</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-secondary text-xs leading-relaxed">
|
<div class="text-xs leading-relaxed text-secondary">
|
||||||
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
||||||
告警配置] 管理。
|
告警配置] 管理。
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { IconifyIcon } from '@vben/icons';
|
|||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Card, Col, Form, Input, Radio, Row } from 'ant-design-vue';
|
import { Card, Col, Form, Input, Radio, Row } from 'ant-design-vue';
|
||||||
import { DictTag } from "#/components/dict-tag";
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
/** 基础信息配置组件 */
|
/** 基础信息配置组件 */
|
||||||
defineOptions({ name: 'BasicInfoSection' });
|
defineOptions({ name: 'BasicInfoSection' });
|
||||||
@@ -26,7 +27,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="rounded-8px mb-10px border-primary border" shadow="never">
|
<Card class="rounded-8px mb-10px border border-primary" shadow="never">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-8px flex items-center">
|
<div class="gap-8px flex items-center">
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="rounded-8px mb-10px border-primary border" shadow="never">
|
<Card class="rounded-8px mb-10px border border-primary" shadow="never">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-8px flex items-center">
|
<div class="gap-8px flex items-center">
|
||||||
@@ -201,7 +201,7 @@ onMounted(() => {
|
|||||||
class="gap-16px flex flex-col"
|
class="gap-16px flex flex-col"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="gap-8px p-12px px-16px rounded-6px border-primary bg-background flex items-center border"
|
class="gap-8px p-12px px-16px rounded-6px flex items-center border border-primary bg-background"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="lucide:timer"
|
icon="lucide:timer"
|
||||||
@@ -214,7 +214,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- CRON 表达式配置 -->
|
<!-- CRON 表达式配置 -->
|
||||||
<div
|
<div
|
||||||
class="p-16px rounded-6px border-primary bg-background border"
|
class="p-16px rounded-6px border border-primary bg-background"
|
||||||
>
|
>
|
||||||
<Form.Item label="CRON表达式" required>
|
<Form.Item label="CRON表达式" required>
|
||||||
<CronTab
|
<CronTab
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ watch(
|
|||||||
{{ operator.label }}
|
{{ operator.label }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-12px px-6px py-2px rounded-4px bg-primary-light-9 text-primary font-mono"
|
class="text-12px px-6px py-2px rounded-4px bg-primary-light-9 font-mono text-primary"
|
||||||
>
|
>
|
||||||
{{ operator.symbol }}
|
{{ operator.symbol }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { DICT_TYPE } from '@vben/constants';
|
|||||||
import { Select } from 'ant-design-vue';
|
import { Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
import { DictTag } from "#/components/dict-tag";
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
/** 产品选择器组件 */
|
/** 产品选择器组件 */
|
||||||
defineOptions({ name: 'ProductSelector' });
|
defineOptions({ name: 'ProductSelector' });
|
||||||
@@ -78,7 +78,7 @@ onMounted(() => {
|
|||||||
{{ product.productKey }}
|
{{ product.productKey }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
||||||
</div>
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'change', value: { config: any; type: string }): void;
|
(e: 'change', value: { config: any; type: string }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// TODO 芋艿
|
||||||
/** 属性选择器内部使用的统一数据结构 */
|
/** 属性选择器内部使用的统一数据结构 */
|
||||||
interface PropertySelectorItem {
|
interface PropertySelectorItem {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
@@ -296,7 +297,7 @@ watch(
|
|||||||
:value="property.identifier"
|
:value="property.identifier"
|
||||||
>
|
>
|
||||||
<div class="py-2px flex w-full items-center justify-between">
|
<div class="py-2px flex w-full items-center justify-between">
|
||||||
<span class="text-14px font-500 text-primary flex-1 truncate">
|
<span class="text-14px font-500 flex-1 truncate text-primary">
|
||||||
{{ property.name }}
|
{{ property.name }}
|
||||||
</span>
|
</span>
|
||||||
<Tag
|
<Tag
|
||||||
@@ -351,10 +352,10 @@ watch(
|
|||||||
|
|
||||||
<div class="space-y-8px ml-24px">
|
<div class="space-y-8px ml-24px">
|
||||||
<div class="gap-8px flex items-start">
|
<div class="gap-8px flex items-start">
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
标识符:
|
标识符:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ selectedProperty.identifier }}
|
{{ selectedProperty.identifier }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,28 +364,28 @@ watch(
|
|||||||
v-if="selectedProperty.description"
|
v-if="selectedProperty.description"
|
||||||
class="gap-8px flex items-start"
|
class="gap-8px flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
描述:
|
描述:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ selectedProperty.description }}
|
{{ selectedProperty.description }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.unit" class="gap-8px flex items-start">
|
<div v-if="selectedProperty.unit" class="gap-8px flex items-start">
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
单位:
|
单位:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ selectedProperty.unit }}
|
{{ selectedProperty.unit }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.range" class="gap-8px flex items-start">
|
<div v-if="selectedProperty.range" class="gap-8px flex items-start">
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
取值范围:
|
取值范围:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ selectedProperty.range }}
|
{{ selectedProperty.range }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,10 +398,10 @@ watch(
|
|||||||
"
|
"
|
||||||
class="gap-8px flex items-start"
|
class="gap-8px flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
访问模式:
|
访问模式:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,10 +413,10 @@ watch(
|
|||||||
"
|
"
|
||||||
class="gap-8px flex items-start"
|
class="gap-8px flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
事件类型:
|
事件类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -427,10 +428,10 @@ watch(
|
|||||||
"
|
"
|
||||||
class="gap-8px flex items-start"
|
class="gap-8px flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-12px min-w-60px text-secondary flex-shrink-0">
|
<span class="text-12px min-w-60px flex-shrink-0 text-secondary">
|
||||||
调用类型:
|
调用类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-primary flex-1">
|
<span class="text-12px flex-1 text-primary">
|
||||||
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export {default as ThingModelArrayDataSpecs} from './thing-model-array-data-specs.vue';
|
export { default as ThingModelArrayDataSpecs } from './thing-model-array-data-specs.vue';
|
||||||
export {default as ThingModelEnumDataSpecs} from './thing-model-enum-data-specs.vue';
|
export { default as ThingModelEnumDataSpecs } from './thing-model-enum-data-specs.vue';
|
||||||
export {default as ThingModelNumberDataSpecs} from './thing-model-number-data-specs.vue';
|
export { default as ThingModelNumberDataSpecs } from './thing-model-number-data-specs.vue';
|
||||||
export {default as ThingModelStructDataSpecs} from './thing-model-struct-data-specs.vue';
|
export { default as ThingModelStructDataSpecs } from './thing-model-struct-data-specs.vue';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||||||
import type { MallCommentApi } from '#/api/mall/product/comment';
|
import type { MallCommentApi } from '#/api/mall/product/comment';
|
||||||
|
|
||||||
import { z } from '#/adapter/form';
|
import { z } from '#/adapter/form';
|
||||||
import { getSpuSimpleList } from '#/api/mall/product/spu';
|
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
@@ -17,19 +16,16 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
show: () => false,
|
show: () => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO @puhui999:商品的选择
|
|
||||||
{
|
{
|
||||||
fieldName: 'spuId',
|
fieldName: 'spuId',
|
||||||
label: '商品',
|
label: '商品',
|
||||||
component: 'ApiSelect',
|
component: 'Input',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
api: getSpuSimpleList,
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'id',
|
|
||||||
placeholder: '请选择商品',
|
placeholder: '请选择商品',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
// TODO @puhui999:商品的选择:上面 spuId 可以选择了,下面的 skuId 打开后,没商品。
|
||||||
{
|
{
|
||||||
fieldName: 'skuId',
|
fieldName: 'skuId',
|
||||||
label: '商品规格',
|
label: '商品规格',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user