first commit

This commit is contained in:
2026-09-02 16:31:50 +08:00
commit a461727193
911 changed files with 692450 additions and 0 deletions
+355
View File
@@ -0,0 +1,355 @@
# PaperMES — 原纸物流 MES 系统(武威金士泰 AGV)
造纸厂原纸(纸卷/纸板)智能仓储物流系统。打通「生产线 → 立体库 → 跨楼层搬运 → 销售出库」全流程,
由 **AGV 自动导引车 + 电梯 PLC + 条码扫描** 协同完成原纸的自动存取与调度。
> 解决方案文件:`WMS.sln`(Visual Studio 2022,.NET 8)
---
## 1. 技术栈
| 类别 | 技术 |
|------|------|
| 运行时 | .NET 8(主工程)、.NET 6-windows(WPF 客户端 `Modules`)、.NET 6(`MqttClient`) |
| Web | ASP.NET Core,以 **Windows 服务** 方式部署(`UseWindowsService`),监听 `http://*:8078` |
| ORM | SqlSugar(SQL Server,库 `wms_jst2.0`) |
| IoC | Autofac(按程序集批量注册 `*Repository/*Service/*Strategy/*Handler`) |
| 任务调度 | Quartz |
| 通信 | DotNetty(TCP/UDP 条码)、NModbus(电梯 PLC)、RestSharp(HTTP 调 ERP/RCS)、SignalR.Client、MQTT |
| 其他 | AutoMapper、FluentValidation、NLog、Polly、SSE(服务端推送)、NPOI(Excel)、FastReport |
---
## 2. 解决方案结构
### 逻辑分组
| 分组 | 包含项目 | 职责 |
|------|---------|------|
| DataBase | Domain、App | 领域模型与数据访问 |
| Utils | Infrastructure、Business、Common、Job | 基础设施、业务、公共、后台作业 |
| CommunicationMiddleware | PLC、RPC、Netty | 通信中间件 |
| WebHost | WebHost、Authorization | Web 服务与鉴权 |
| UnitTest | RPCTest | 单元测试 |
### 12 个工程项目
| 项目 | 说明 |
|------|------|
| `JSMachine.WMS.Domain` | 领域层:实体(AGVTask/ERPTask/RcsTask/StorageRack/ElevatorPlcQueue…)+ 仓储接口/实现(SqlSugar) |
| `JSMachine.WMS.App` | 应用层:Dto + IService/ServiceImpl(每个实体一个服务) |
| `JSMachine.WMS.Business` | 业务层:入库/出库/RCS 回调/SSE/公共(核心业务 + 大量设计模式) |
| `JSMachine.WMS.Infrastructure` | 基础设施:AppSettings、SqlSugar、Lambda 表达式树、日志、序列化、加密等 |
| `JSMachine.WMS.Common` | 公共:缓存、枚举、Dto、Helper、Htmls |
| `JSMachine.WMS.Job` | 后台作业(Quartz):AGV/电梯/ERP/中转区/数据清理等定时任务 |
| `JSMachine.WMS.PLC` | 电梯 PLC(Modbus 读写,NModbus) |
| `JSMachine.WMS.RPC` | 对外 HTTP 调用:ERP(晨龙/旺泰/新荣/IBS)、PMS(ACS/DCS)、RCS、IBS |
| `JSMachine.WMS.Netty` | 基于 DotNetty:条码扫描客户端/服务端/UDP(心跳、设备动态) |
| `JSMachine.WMS.WebHost` | Web 宿主(入口 `Program.cs` → `WebHostEngine1.Start()`),Controllers/Filters/Validators |
| `JSMachine.WMS.Authorization` | 身份认证/授权(IdentityServer4,当前未启用) |
| `JSMachine.WMS.RPCTest` | RPC 调用测试(MSTest) |
> 目录下另有 `JSMachine.WMS.Modules`(WPF 客户端,Prism + HandyControl)、`JSMachine.WMS.MqttClient`,
> **未纳入 `WMS.sln`**,需手动加载。
---
## 3. 分层架构(DDD)
```
WebHost(表现层,端口 8078) + Authorization(认证)
↓
Application(应用层:Dto + IService/ServiceImpl)
↓
Business(业务层:入库/出库/RCS回调/SSE)
↓
Domain(领域层:Entity + IRepository/Repository)
Infrastructure(基础设施:SqlSugar/配置/日志/Lambda)
Common(公共:缓存/枚举/Helper)
↓
通信中间件:PLC / RPC / Netty / MqttClient
后台作业:Job(Quartz)
```
依赖方向自上而下;`Domain` 定义实体与仓储接口,`Infrastructure` 提供 SqlSugar 实现。
---
## 4. 核心业务流程
### 4.1 入库(PDA 扫码入库)`Business/StockIn`
`StockInService.StockIn(PDAInParam)` 主流程:
1. 查物料信息是否存在、查库位是否存在
2. 校验同「合格证号」是否已有未完成任务(防重扫)
3. 按 **放置策略 + 责任链** 计算目标库位并**锁定**(先锁库再写任务,保证一致性)
4. 写入 `ERPTask`,触发 AGV 搬运任务
### 4.2 出库(销售出库)`Business/StockOut`
`StockOutService.Stockout` 主流程:
1. 校验销售订单号是否重复
2. 计算可出库存:同批次库存 − 锁定/禁用 − 未完结任务 − 撤单数
3. 生成 `ERPTask` 出库任务
### 4.3 AGV 状态回调 `Business/RcsCallBack`
RCS 上报 AGV 任务状态 → `RcsCallBackService.DealwithExcutionStatus`:
失败(推 PDA 报警)→ 取消 → 成功,按状态码分发到不同处理器(见第 6 节)。
---
## 5. 对接外部系统
| 系统 | 项目 | 说明 |
|------|------|------|
| ERP(四选一) | `RPC/ErpRPC` | 企望(0) / IBS(1) / 晨龙(2) / 新荣(3),当前用 **新荣**;含生产入库、销售出库、生产退库、物料查询等接口 |
| RCS 机器人调度 | `RPC/RcsRPC` | 创建任务 `ics/taskOrder/addTask`、取消任务 `ics/out/task/cancelTask` |
| PMS/DCS/ACS 备料 | `RPC/PmsRPC` | `AcsPaperPreparService`、`DcsPaperPreparService` |
| IBS 原料查询 | `RPC/IBSRPC` | 纸卷基础信息查询 |
---
## 6. 设计模式应用
### 6.1 责任链模式(入库库位计算)
责任流动方向:`StockInByExist → StockInByNew → StockInByMixing`
| 节点 | 职责 |
|------|------|
| `StockInByExist` | 优先放「已有同批次」的列;同批次放满一列再开新列(最多 2 列,超出走中转区) |
| `StockInByNew` | 放「全新空白列」 |
| `StockInByMixing` | 与其他批次货物混放 |
### 6.2 策略模式(入库放置策略)
`StrategyFactory.CreateStockInStrategy` 根据 `PlcaementMod` 配置返回不同策略:
| 模式 | 配置值 | 策略 | 责任链 |
|------|--------|------|--------|
| 完全不混放 | 0 | `NoMixingStrategy` | Exist → New |
| 混合模式(默认) | 1 | `MixedModeStrategy` | Exist → New → Mixing |
| 完全混放 | 2 | `CompleteMixingStrategy` | 直接找空列 |
> 一楼(`FirstFloor`)强制走「不混放」逻辑。
### 6.3 状态模式 + 策略模式(RCS 回调)
`RcsCallbackHandlerFactory.CreateHandler(status, destWarehouse)` 按 **AGV 状态码** 分发:
| 状态码 | 处理器 | 说明 |
|--------|--------|------|
| 21 | `PickupCompletedStateHandler` | 取货完成 |
| 23 | 按 `Transport` 二次分发 | 放货完成 |
状态 23 按目标区域二次分发:
| 目标区域(Transport) | 处理器 |
|----------------------|--------|
| 北区/南区/出库中区/退库区 | `StockOutWharfHandler`(出库月台) |
| 库区(NormalArea) | `StockAreaHandler` |
| 中转区(TransitArea) | `TransitAreaHandler` |
| 电梯(Elevator) | `ElevatorDeliveryHandler` |
---
## 7. 电梯 PLC 通信(`PLC`)
`ElevatorPlc : ModbusEngineBase` 初始化时按 IP 匹配电梯编号,注册到 `PLCManager` 后连接(NModbus,端口 502/503)。
每台电梯 8 个通道(读/写寄存器位):
| 通道 | 网带号 | 读数据位 | 写数据位 |
|------|--------|---------|---------|
| 入库放货通道2 | 1001 | 0 | 8 |
| 入库放货通道1 | 1011 | 2 | 10 |
| 入库取货通道2 | 2004 | 5 | 13 |
| 入库取货通道1 | 2014 | 7 | 15 |
| 出库放货通道2 | 2001 | 4 | 12 |
| 出库放货通道1 | 2011 | 6 | 14 |
| 出库取货通道2 | 1006 | 1 | 9 |
| 出库取货通道1 | 1016 | 3 | 11 |
- 读:`ElevatorStatusRead.ReadElevatorStatus(no)`,读 20 个 ushort(寄存器地址 1 起)
- 写:`ElevatorWrite.WriteElevator(status)`,逐通道写「放货/取货完成」信号
---
## 8. 后台作业(Quartz,`Job`)
`AgvTaskJob`、`ElevatorJob`、`ElevatorOutJob`(电梯出库)、`ErpTaskJob`、`RcsCallbackJob`、
`StockInSecondFloorJob`(二楼入库)、`TransferAreaJob`(中转区)、`DataCleanJob`(数据清理)、`XinRongErpJob`(新荣 ERP 定时回传)。
通过 `JobEngine` + `JobListener` 启动。
---
## 9. 配置说明(`JSMachine.WMS.WebHost/appSettingsWebhost.json`)
| 配置项 | 说明 | 当前值 |
|--------|------|--------|
| `urls` | 服务监听地址 | `http://*:8078` |
| `DataBaseCon` | SQL Server 连接串 | `wms_jst2.0`(本地) |
| `ErpConfig.ErpType` | ERP 类型 | `3`(新荣) |
| `ErpConfig.RetryNum` | 上传重试次数 | `5` |
| `RcsConfig` | RCS 创建/取消任务 URL | 见上 |
| `ElevatorConfig` | 电梯列表 | 2 台(端口 502/503) |
| `PlcaementMod` | 放置模式 | `1`(混合模式) |
> ⚠️ 配置文件包含明文数据库密码,生产部署前应改为环境变量/加密配置。
---
## 10. 部署与启动
1. 还原数据库:执行 `../dbo.sql`
2. 配置 `appSettingsWebhost.json`(数据库连接、ERP 类型、电梯 IP/端口)
3. 编译 `WMS.sln`(.NET 8 SDK)
4. 启动:`JSMachine.WMS.WebHost` 为 Windows 服务(`WebHostEngine1.Start()` 调用 `UseWindowsService`),
也可直接运行调试;服务监听 8078 端口
5. 前端:`../wms_front`(Web)、`../wmsjstpda`(PDA 手持终端)
---
## 11. 注意事项
1. **鉴权未启用**:`Startup.cs` 中 JWT/IdentityServer 认证逻辑整体被注释,`UseAuthentication/UseAuthorization` 空转。
2. **测试覆盖薄**:`RPCTest` 仅含 RPC 调用测试(MSTest)。
3. **两个工程未入解决方案**:`Modules`(WPF)、`MqttClient` 需手动加载。
4. **并发控制**:入库责任链用 `SemaphoreSlim(1,1)` 串行化,保证库位锁定的数据一致性。
---
## 12. 代码级深入分析
### 12.1 入库库位计算(策略 + 责任链)
入口 `StockInService` 调用 `StrategyFactory.CreateStockInStrategy`:一楼强制走「不混放」,其余楼层按 `PlcaementMod` 决定策略。
责任链节点职责:
| 节点 | 职责 |
|------|------|
| `StockInByExist` | 优先放「已有同批次」的列,同批次堆满一列再开新列(最多 2 列) |
| `StockInByNew` | 放「整列全空」的新列,取最底层 |
| `StockInByMixing` | 与其他批次混放;仍无则落中转区兜底 |
`StockInByExist` 关键逻辑:
1. `GetStorageRackExceptStockout` 先剔除「当天有出库任务的列」,避免入库与掏货出库冲突
2. `SemaphoreSlim(1,1)` 串行化,防并发抢库位
3. 列被锁时的分支:有未锁空列→直接取;空列≤1→开新列;一楼已开两列被锁→`return null` 等待解锁;物料已在中转区→`return null` 防重复
4. 选定后**先 `Lock` 再返回**,保证 DB 一致性
### 12.2 电梯 PLC 通信
`ElevatorPlc.Init` 时序关键:先 `PLCManager.AddPLC` 再 `Connect`,否则 Job 先执行拿不到实例无法重连。
每台电梯 8 通道寄存器映射(读位/写位):
| 动作 | 通道2 网带 | 通道1 网带 | 读位 | 写位 |
|------|-----------|-----------|------|------|
| 入库放货 | 1001 | 1011 | 0 / 2 | 8 / 10 |
| 入库取货 | 2004 | 2014 | 5 / 7 | 13 / 15 |
| 出库放货 | 2001 | 2011 | 4 / 6 | 12 / 14 |
| 出库取货 | 1006 | 1016 | 1 / 3 | 9 / 11 |
电梯编号靠库位名称是否含「1号」判定(`StorageRackName.Contains("1号") ? 1 : 2`)。
### 12.3 RCS 回调双重分派(状态 + 策略)
`RcsCallbackHandlerFactory.CreateHandler(status, destWarehouse)`:
- 状态 21 → `PickupCompletedStateHandler`(取货完成)
- 状态 23 → 按 `Transport` 二次分发到 `StockOutWharfHandler` / `StockAreaHandler` / `TransitAreaHandler` / `ElevatorDeliveryHandler`
`ElevatorDeliveryHandler` 完整链路:写 PLC 放货信号 → 写 PLC 队列 → 电梯计数 → 更新 AGV 任务完成 → 解锁库位 → SSE 广播刷新前端。
---
### 12.4 后台作业调度(Job,Quartz)
**声明式注册**:`JobEngine` 反射扫描所有「实现 `IJob` 且带 `[Job]` 特性」的类注册到 Quartz,新增作业只需打特性。
9 个作业(均 `[DisallowConcurrentExecution]` 禁止并发):
| 作业 | 间隔 | 职责 |
|------|------|------|
| AgvTaskJob | 1s | 把 Initial 状态 AGV 任务下发给 RCS |
| ErpTaskJob | 2s | ERP 任务拆解为 AGV 搬运任务(一楼 AGV 限流 ≤4) |
| RcsCallbackJob | 1s | 消费 RCS 回调队列,幂等去重后处理状态流转 |
| ElevatorJob | 0.5s | 轮询 PLC 入库取货信号,上升沿触发,入队二楼 |
| ElevatorOutJob | 0.5s | 轮询 PLC 出库取货信号,生成一楼出库任务 |
| StockInSecondFloorJob | 2s | 消费二楼入库队列,策略工厂算库位,无位回写队列自旋 |
| TransferAreaJob | 2s | 调度中转区滞留物料 |
| XinRongErpJob | 2s | 已完成任务回传新荣 ERP(含重试) |
| DataCleanJob | 24h | 清理 12 个月前历史数据 + 日志文件 |
作业间通信用 `ConcurrentQueue` 静态缓存做生产者-消费者(`StockInSecondFloorCache`、`RcsCallbackCache`);PLC 信号用 `static bool` 前值做**上升沿检测**。
### 12.5 Netty 条码协议
三套同构实现(客户端/服务端/UDP):
| 角色 | 场景 |
|------|------|
| NettyAsClient | WMS 主动连条码设备(基恩士/倍加福/喷码机),收扫码结果写内存缓存 |
| NettyAsServer | WMS 监听端口,发 `LON\r\n` 触发扫码,轮询缓存回读结果 |
| NettyUdp | 广播监听 503 端口,收设备心跳/动态 |
分帧:`DelimiterBasedFrameDecoder`(分隔符)或 `LengthFieldBasedFrameDecoder`(2 字节长度域)。分发:MEF `[InheritedExport]` 装配全部 Handler,责任链逐个 `Handle`,首个返回 `true` 终止。
### 12.6 出库库存计算
可出库存公式:`物理在库 + 在途AGV任务数 + 撤单数 − 已下达出库命令总数 ≥ 本次出库数`
`CalculateOutMaterial` 决策树:优先出「拖数最少」的列 → 锁定列等待解锁 → 挡路不同批次则触发**掏货**中转。
### 12.7 RPC 对接层
四家 ERP 中**只有新荣(ErpType=3)完整可用**,其余三家接口签名已漂移断链。认证方式:辰龙/企望 MD5 签名、IBS Header AppId/AppKey、新荣 body 内 user_key。RCS 返回 `code==1000` 成功。
### 12.8 WebHost 与认证
60+ HTTP 接口(入库/出库/撤单/搬运/库位维护/统计/SSE 推送)。**当前认证完全关闭**:JWT/IdentityServer 全部注释,登录为明文密码比对 + 无签名 GUID token,所有接口匿名可访问。限流过滤器、FluentValidation 规则均未启用。
### 12.9 基础设施与遗留模块
- **动态 Lambda 查询**(Infrastructure/LambdaHelp):把「字段+操作符+值」动态编译为 `Expression<Func<T,bool>>`,组内 OR、组间 AND,供 SqlSugar 动态筛选/排序——全项目最核心的通用能力。
- **ViKey 加密狗**:P/Invoke 硬件授权读取。
- **Modules(WPF)**:Prism + HandyControl + FastReport,生产现场操作终端(设备连接监控 + 新卷入库查询/标签打印)。
- **MqttClient**:空壳工程(仅 `Class1`),MQTT 未实现。
- Infrastructure 从 DCS/Accelerider 项目迁移而来,保留旧命名空间/AssemblyTitle。
---
## 13. 技术债与改进点
**并发/性能**
1. `_SemaphoreSlim(1,1)` 静态单例全局串行化所有入库计算,高并发吞吐瓶颈。
2. `GetStorageRackExceptStockout` 每次查当天全部出库任务,宜加索引/缓存。
3. 服务端扫码回读靠 100ms 轮询 + 2s 超时阻塞。
**逻辑 bug(已定位)**
4. `RcsCallBackService.HandleStocOutFirstFloor` 守卫误写 `Floor==SecondFloor`,一楼销售出库失败报警分支失效。
5. `CalculateDestWarehouse_SaleOutMiddle` 双重 `OrderBy` 覆盖,前一排序失效。
6. `LambdaBuider` 中 `long` 类型判断误写 `PropertyInfo == typeof(long)`,恒 false。
**死代码/未接线**
7. 策略模式整套(`StorageAreaStrategy`/`ElevatorStrategy`/`TaskCompletionStrategyFactory`)无调用方,与状态模式功能重叠。
8. Job:`JobListener` 未注册、`ScannerSceneAttribute` 孤儿、`JobGroup` 枚举未用、`PlateList` 遗留。
9. Netty:两个 `Decoder.cs` 死代码且有 bug、`PenmoReadCmd` 无 Handler、UDP 心跳未落地。
10. RCS 回调 `RcsCallBackService` 内 4 个私有方法、`StockOutCreationDetail`、`FailedStateHandler` 均未引用。
11. `ScheduleType` 仅实现 Simple,Corn/Calendar/XML/JSON 未落地。
**安全**
12. 认证完全关闭,60+ 接口匿名可访问;登录明文密码 + 无签名 token;CORS 全开;IdentityServer 开发签名 + 弱口令 `123456`。
13. 配置含明文数据库密码。
**工程/维护**
14. 三家 ERP(辰龙/企望/IBS)接口签名漂移断链,仅新荣可用;`ErpType`/`PmsType` 特性未接线,实现选择依赖 Autofac「最后注册生效」。
15. PmsRPC(ACS/DCS)为占位半成品;`IBSRPC` 缺 await 不可用。
16. `Modules.csproj` 引用不存在的 `..\Server.MqttClient\`;`MqttClient` 空壳。
17. Infrastructure 保留 DCS/Accelerider 迁移残留(命名空间/AssemblyTitle)。
+532645
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
# 配置项文档:https://editorconfig.org/
# 告知 EditorConfig 插件,当前即是根文件
root = true
# 适用全部文件
[*]
## 设置字符集
charset = utf-8
## 缩进风格 space | tab,建议 space
indent_style = space
## 缩进的空格数(修改这里的话需要将 prettier.config.js 和 .vscode -> settings.json 也同步修改)
indent_size = 2
## 换行符类型 lf | cr | crlf,一般都是设置为 lf
end_of_line = lf
## 是否在文件末尾插入空白行
insert_final_newline = true
## 是否删除一行中的前后空格
trim_trailing_whitespace = true
# 适用 .md 文件
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
+10
View File
@@ -0,0 +1,10 @@
# 自定义的环境变量(命名必须以 VITE_ 开头)
## 后端接口公共路径(如果解决跨域问题采用反向代理就只需写公共路径)
VITE_BASE_API = 'http://127.0.0.1:8078'
## 路由模式 hash 或 html5
VITE_ROUTER_HISTORY = 'hash'
## 开发环境地址前缀(一般 '/','./' 都可以)
VITE_PUBLIC_PATH = '/'
+10
View File
@@ -0,0 +1,10 @@
# 自定义的环境变量(命名必须以 VITE_ 开头)
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
VITE_BASE_API = 'http://localhost:5000/api/'
## 路由模式 hash 或 html5
VITE_ROUTER_HISTORY = 'hash'
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
VITE_PUBLIC_PATH = '/'
+11
View File
@@ -0,0 +1,11 @@
# 自定义的环境变量(命名必须以 VITE_ 开头)
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
# VITE_BASE_API = 'https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1'
VITE_BASE_API = 'http://192.168.2.212:5000/api/'
## 路由模式 hash 或 html5
VITE_ROUTER_HISTORY = 'hash'
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
VITE_PUBLIC_PATH = '/acs-office/'
+8
View File
@@ -0,0 +1,8 @@
# Eslint 会忽略的文件
.DS_Store
node_modules
dist
dist-ssr
*.local
.npmrc
+69
View File
@@ -0,0 +1,69 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/eslint-config-typescript"
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
ecmaFeatures: {
jsx: true,
tsx: true
}
},
rules: {
// TS
"@typescript-eslint/no-explicit-any": "off",
"no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
// Vue
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
],
// Prettier
"prettier/prettier": "off"
}
}
+37
View File
@@ -0,0 +1,37 @@
# Git 会忽略的文件
.DS_Store
node_modules
dist
dist-ssr
.eslintcache
# Local env files
*.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/*.code-snippets
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.zip
# Use the PNPM
package-lock.json
yarn.lock
1013-JSHS-ACS_WEB_dist.zip
+2
View File
@@ -0,0 +1,2 @@
# 通过该配置兜底解决组件没有类型提示的问题
shamefully-hoist = true
+8
View File
@@ -0,0 +1,8 @@
# Prettier 会忽略的文件
.DS_Store
node_modules
dist
dist-ssr
*.local
.npmrc
+6
View File
@@ -0,0 +1,6 @@
{
"editor.defaultFormatter": null,
"[vue]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-present pany <https://github.com/pany-ang>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
# WMS_Front
金世泰WMS二期前端网站
+1
View File
@@ -0,0 +1 @@
No Message
+1
View File
@@ -0,0 +1 @@
declare module 'file-saver'
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/app-loading.css" />
<title>WMS仓储管理系统</title>
</head>
<body>
<div id="app">
<div id="app-loading" style="height: 100%;"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
{
"scripts": {
"dev": "vite",
"build:dev": "vite build",
"build:stage": "vue-tsc --noEmit && vite build --mode staging",
"build:prod": "vue-tsc --noEmit && vite build",
"preview:stage": "pnpm build:stage && vite preview",
"preview:prod": "pnpm build:prod && vite preview",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx,json,css,less,scss,html,md}\"",
"lint": "pnpm lint:eslint && pnpm lint:prettier",
"test": "vitest"
},
"dependencies": {
"@dataview/datav-vue3": "^0.0.0-test.1672506674342",
"@element-plus/icons-vue": "^2.1.0",
"@microsoft/signalr": "^7.0.10",
"axios": "1.5.0",
"date-fns": "^4.1.0",
"dayjs": "1.11.9",
"echarts": "^5.4.3",
"element-plus": "^2.11.3",
"file-saver": "^2.0.5",
"js-cookie": "3.0.5",
"lodash": "^4.17.21",
"lodash-es": "4.17.21",
"mitt": "3.0.1",
"normalize.css": "8.0.1",
"nprogress": "0.2.0",
"path-browserify": "1.0.1",
"path-to-regexp": "6.2.1",
"pinia": "2.1.6",
"screenfull": "6.0.2",
"vue": "3.3.4",
"vue-router": "4.2.4",
"vxe-table": "4.4.1",
"vxe-table-plugin-element": "3.0.7",
"xe-utils": "3.5.11",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/js-cookie": "3.0.3",
"@types/lodash-es": "4.17.8",
"@types/node": "20.5.7",
"@types/nprogress": "0.2.0",
"@types/path-browserify": "1.0.0",
"@typescript-eslint/eslint-plugin": "6.4.1",
"@typescript-eslint/parser": "6.4.1",
"@vitejs/plugin-vue": "4.3.3",
"@vitejs/plugin-vue-jsx": "3.0.2",
"@vue/eslint-config-prettier": "8.0.0",
"@vue/eslint-config-typescript": "11.0.3",
"@vue/test-utils": "2.4.1",
"eslint": "8.48.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-vue": "9.17.0",
"jsdom": "22.1.0",
"lint-staged": "14.0.1",
"prettier": "3.0.2",
"sass": "1.66.1",
"typescript": "5.2.2",
"unocss": "0.55.3",
"vite": "4.4.9",
"vite-plugin-svg-icons": "2.0.1",
"vite-svg-loader": "4.0.0",
"vitest": "0.34.3",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.8"
},
"lint-staged": {
"*.{vue,js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,less,scss,html,md}": [
"prettier --write"
],
"package.json": [
"prettier --write"
]
}
}
+5323
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
/** 配置项文档:https://prettier.io/docs/en/configuration.html */
module.exports = {
/** 每一行的宽度 */
printWidth: 120,
/** Tab 键的空格数 */
tabWidth: 2,
/** 在对象中的括号之间是否用空格来间隔 */
bracketSpacing: true,
/** 箭头函数的参数无论有几个,都要括号包裹 */
arrowParens: "always",
/** 换行符的使用 */
endOfLine: "auto",
/** 是否采用单引号 */
singleQuote: false,
/** 对象或者数组的最后一个元素后面不要加逗号 */
trailingComma: "none",
/** 是否加分号 */
semi: false,
/** 是否使用 Tab 格式化 */
useTabs: false
}
+45
View File
@@ -0,0 +1,45 @@
/** 白屏阶段会执行的 CSS 加载动画 */
#app-loading {
position: relative;
top: 45vh;
margin: 0 auto;
color: #409eff;
font-size: 12px;
}
#app-loading,
#app-loading::before,
#app-loading::after {
width: 2em;
height: 2em;
border-radius: 50%;
animation: 2s ease-in-out infinite app-loading-animation;
}
#app-loading::before,
#app-loading::after {
content: "";
position: absolute;
}
#app-loading::before {
left: -4em;
animation-delay: -0.2s;
}
#app-loading::after {
left: 4em;
animation-delay: 0.2s;
}
@keyframes app-loading-animation {
0%,
80%,
100% {
box-shadow: 0 2em 0 -2em;
}
40% {
box-shadow: 0 2em 0 0;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

+3
View File
@@ -0,0 +1,3 @@
{
"baseApiUrl":"http://localhost:8078"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+31
View File
@@ -0,0 +1,31 @@
<script lang="ts" setup>
// import { h } from "vue"
import { useTheme } from "@/hooks/useTheme"
// import { ElNotification } from "element-plus"
// 将 Element Plus 的语言设置为中文
import zhCn from "element-plus/es/locale/lang/zh-cn"
const { initTheme } = useTheme()
/** 初始化主题 */
initTheme()
/** 作者小心思 */
// ElNotification({
// title: "Hello",
// type: "success",
// message: h(
// "a",
// { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite" },
// "小项目获取 star 不易,如果你喜欢这个项目的话,欢迎点击这里支持一个 star !这是作者持续维护的唯一动力(小声:毕竟是免费的)"
// ),
// duration: 0,
// position: "bottom-right"
// })
</script>
<template>
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
+14
View File
@@ -0,0 +1,14 @@
import { AgvTask } from "@/dto/agvTask/AgvTask"
import { request } from "@/utils/service"
/** 获取erp任务关联的agv任务 */
export function GetAgvTasksRelatedToErpTask(erpTaskId: string) {
return request<GetAgvTasksRelatedToErpTaskResponseData>({
url: "/api/AgvTask/GetAgvTasksRelatedToErpTask?erpTaskId="+erpTaskId,
method: "post"
})
}
export type GetAgvTasksRelatedToErpTaskResponseData = ApiResponseData<AgvTask[]>
+11
View File
@@ -0,0 +1,11 @@
import { request } from "@/utils/service"
/** 获取 班组信息 */
export function GetGroupNames() {
return request<GetGroupNameResponseData>({
url: "Report/GetGroupNames",
method: "get"
})
}
export type GetGroupNameResponseData = ApiResponseDataArray<String>
+26
View File
@@ -0,0 +1,26 @@
import { ErpTask } from "@/dto/erpTask/ErpTask"
import {ErpTaskQueryParam} from '@/dto/erpTask/ErpTaskQueryParam'
import { request } from "@/utils/service"
/** 获取erp任务关联的agv任务 */
export function GetErpTasks(param:ErpTaskQueryParam) {
return request<GetErpTasksResponseData>({
url: "/api/Erp/GetErpTasks",
method: "post",
data:param
})
}
/** 操作erp任务(更改状态)*/
export function OperateTask(id:string,taskStatus:number) {
return request<OperateTaskResponseData>({
url: "/api/Erp/OperateTask?id="+id+'&taskStatus='+taskStatus,
method: "post",
})
}
export type GetErpTasksResponseData = ApiResponseData<ErpTask[]>
export type OperateTaskResponseData = ApiResponseData<boolean>
@@ -0,0 +1,36 @@
/** 模拟接口响应数据 */
const SELECT_RESPONSE_DATA = {
code: 0,
data: [
{
label: "苹果",
value: 1
},
{
label: "香蕉",
value: 2
},
{
label: "橘子",
value: 3,
disabled: true
}
],
message: "获取 Select 数据成功"
}
/** 模拟接口 */
export function getSelectDataApi() {
return new Promise<typeof SELECT_RESPONSE_DATA>((resolve, reject) => {
// 模拟接口响应时间 2s
setTimeout(() => {
// 模拟接口调用成功
if (Math.random() < 0.8) {
resolve(SELECT_RESPONSE_DATA)
} else {
// 模拟接口调用出错
reject(new Error("接口发生错误"))
}
}, 2000)
})
}
@@ -0,0 +1,26 @@
/** 模拟接口响应数据 */
const SUCCESS_RESPONSE_DATA = {
code: 0,
data: {
list: [] as number[]
},
message: "获取成功"
}
/** 模拟请求接口成功 */
export function getSuccessApi(list: number[]) {
return new Promise<typeof SUCCESS_RESPONSE_DATA>((resolve) => {
setTimeout(() => {
resolve({ ...SUCCESS_RESPONSE_DATA, data: { list } })
}, 1000)
})
}
/** 模拟请求接口失败 */
export function getErrorApi() {
return new Promise((_resolve, reject) => {
setTimeout(() => {
reject(new Error("发生错误"))
}, 1000)
})
}
+27
View File
@@ -0,0 +1,27 @@
import { request } from "@/utils/service"
import * as Login from "./types/login"
/** 获取所有用户 Token */
export function getAllUser() {
return request<Login.AllUserInfo>({
url: "/api/UserInfo/GetAll",
method: "get"
})
}
/** 登录并返回 Token */
export function loginApi(data: Login.LoginParam) {
return request<Login.LoginResponseData>({
url: '/api/UserInfo/Login',
method: "post",
data
})
}
/** 获取用户详情 */
export function getUserInfoApi() {
return request<Login.UserInfoResponseData>({
url: "users/info",
method: "get"
})
}
+13
View File
@@ -0,0 +1,13 @@
import { UserPermission } from "@/dto/user/userPermission"
import { UserInfo } from "@/dto/user/userInfo"
export interface LoginParam {
UserId: string
Password: string
}
export type AllUserInfo = ApiResponseData<UserInfo[]>
export type LoginResponseData = ApiResponseData<UserPermission>
export type UserInfoResponseData = ApiResponseData<{ username: string; roles: string[] }>
@@ -0,0 +1,14 @@
import { OperationLog,OperationLogQueryParam } from "@/dto/operationLog/OperationLog"
import { request } from "@/utils/service"
export function GetOperationLogLst(param:OperationLogQueryParam) {
return request<GetOperationLogResponseData>({
url: "/api/OperationLog/QueryByPage",
method: "post",
data:param
})
}
export type GetOperationLogResponseData = ApiResponseData<OperationLog[]>
@@ -0,0 +1,13 @@
import { request } from "@/utils/service"
import * as PaperCostInfo from "../../dto/paperCostInfo/PaperCostInfo"
export type PaperCostInfoData = ApiResponseDataArray<PaperCostInfo.PaperCostInfo>
/** 获取日耗纸统计信息 */
export function QueryPaperReplaceInfo(data: PaperCostInfo.PaperCostInfoQueryParam) {
return request<PaperCostInfoData>({
url: "/api/PaperMesRfid/GetPaperCostByDays",
method: "post",
data
})
}
@@ -0,0 +1,21 @@
import { request } from "@/utils/service"
import * as md from "@/dto/paperfeed/paperFeed"
/** 获取进纸记录列表 */
export function GetPaperReturnList(data: md.PaperFeedSearch) {
return request<PaperFeedResponseData>({
url: "/api/PaperReturn/GetPaperReturnList",
method: "post",
data
})
}
/**补录、编辑进纸信息 */
export function SaveReturnRecord(data: md.PaperFeedModel) {
return request<PaperFeedResponseData>({
url: "/api/PaperReturn/SaveReturnRecord",
method: "post",
data
})
}
export type PaperFeedResponseData = ApiResponseDataArray<md.PaperFeedModel>
@@ -0,0 +1,12 @@
import { request } from "@/utils/service"
import * as PaperUsingRateInfo from "@/dto/paperUsingRate/PaperUsingRateInfo"
export function GetPaperUsingRate(startTime: string, endTime: string) {
return request<PaperUsingRateResponseData>({
url: "/api/Statistic/GetPaperUsingRate",
method: "post",
data: { 'startTime': startTime, 'endTime': endTime }
})
}
export type PaperUsingRateResponseData = ApiResponseDataArray<PaperUsingRateInfo.PaperUsingRateInfo>
@@ -0,0 +1,12 @@
import { request } from "@/utils/service"
import * as PaperWastageStatistic from "../../dto/paperWastageStatistic/PaperWastageStatistic"
export type PaperWastageStatisticData = ApiResponseDataArray<PaperWastageStatistic.PaperWastageStatisticInfo>
/** 获取代纸信息列表 */
export function QueryPaperWastageStatisticInfo(data: PaperWastageStatistic.PaperWastageStatisticQueryParam) {
return request<PaperWastageStatisticData>({
url: "/api/PaperOrderMap/GetPaperWastageStatistic",
method: "post",
data
})
}
@@ -0,0 +1,30 @@
import { request } from "@/utils/service"
import * as PaperOrderMap from "../../dto/paper_order_map/PaperOrderMap"
export type PaperOrderMapInfoData = ApiResponseDataArray<PaperOrderMap.PaperOrderMapInfo>
/** 获取订单用纸信息列表 */
export function QueryPaperOrderMapInfo(data: PaperOrderMap.PaperOrderMapQueryParam) {
return request<PaperOrderMapInfoData>({
url: "/api/PaperOrderMap/QueryByPage",
method: "post",
data
})
}
/** 获取订单信息列表 */
export function GetOrderListByPage(data: PaperOrderMap.PaperOrderMapQueryParam) {
return request<PaperOrderMapInfoData>({
url: "/api/PaperOrderMap/GetOrderListByPage",
method: "post",
data
})
}
export function GetOrderList(data: PaperOrderMap.PaperOrderMapQueryParam) {
return request<PaperOrderMapInfoData>({
url: "/api/PaperOrderMap/GetPaperOrderMaps",
method: "post",
data
})
}
@@ -0,0 +1,12 @@
import { request } from "@/utils/service"
import * as PaperReplace from "../../dto/paper_replace/PaperReplace"
export type PaperReplaceData = ApiResponseDataArray<PaperReplace.PaperReplace>
/** 获取代纸信息列表 */
export function QueryPaperReplaceInfo(data: PaperReplace.PaperReplaceQueryParam) {
return request<PaperReplaceData>({
url: "/api/PaperReplace/QueryByPage",
method: "post",
data
})
}
@@ -0,0 +1,35 @@
import { request } from "@/utils/service"
import * as md from "@/dto/paperfeed/paperFeed"
/**
* 根据原纸卷标,查询原纸信息
* @param paperLable
* @returns
*/
export function QueryPaperInfo(paperLable: string) {
return request<QueryPaperBaseInfoResponseData>({
url: "api/PaperMesRfid/GetPaperByPaperLable?paperLable=" + paperLable,
method: "post"
})
}
/** 获取进纸记录列表 */
export function GetPaperFeedList(data: md.PaperFeedSearch) {
return request<PaperFeedResponseData>({
url: "/api/PaperFeed/GetPaperFeedList",
method: "post",
data
})
}
/**补录、编辑进纸信息 */
export function SaveFeedRecord(data: md.PaperFeedModel) {
return request<PaperFeedResponseData>({
url: "/api/PaperFeed/SaveFeedRecord",
method: "post",
data
})
}
export type QueryPaperBaseInfoResponseData = ApiResponseData<string>
export type PaperFeedResponseData = ApiResponseDataArray<md.PaperFeedModel>
@@ -0,0 +1,25 @@
import { request } from "@/utils/service"
import * as md from "@/dto/report/MonthReportDto"
/** */
export function GetMonthReport(data: md.MonthReportSearch) {
return request<MonthReportResponseData>({
url: "Report/MonthReport",
method: "post",
data
})
}
/** */
export function ExportMonthReport(data: md.MonthReportSearch) {
return request<any>({
url: "Report/ExportMonthReport",
method: "post",
responseType: "blob",
data
})
}
/** */
export type MonthReportResponseData = ApiResponseData<md.MonthReportDto>
@@ -0,0 +1,26 @@
import { request } from "@/utils/service"
import * as md from "@/dto/report/WetPaperReportDto"
/** */
export function GetWetPaperReport(data: md.WetPaperReportSearch) {
return request<WetPaperReportResponseData>({
url: "Report/WetPaperReport",
method: "post",
data
})
}
/** */
export function ExportWetPaperReport(data: md.WetPaperReportSearch) {
return request<any>({
url: "Report/ExportWetPaperReport",
method: "post",
responseType: "blob",
data
})
}
/** */
export type WetPaperReportResponseData = ApiResponseData<md.WetPaperReportDto>
@@ -0,0 +1,52 @@
import { PdaInParam } from "@/dto/simulativeDebugging/pdaInParam"
import { RcsTask } from "@/dto/simulativeDebugging/rcsTask"
import { UploadRcsTaskStatusParam } from "@/dto/simulativeDebugging/uploadRcsTaskStatusParam"
import { request } from "@/utils/service"
/** pda提交入库 */
export function StockInByPda(data: PdaInParam) {
return request<StockInByPdaResponseData>({
url: "/api/Wms/StockInByPda",
method: "post",
data
})
}
/** 获取ecs任务列表 */
export function GetRcsTaskLst() {
return request<GetRcsTaskLstResponseData>({
url: "/api/Test/GetRcsTaskLst",
method: "post",
})
}
/** 向wms上传任务状态 */
export function UploadRcsTaskStatus(data:UploadRcsTaskStatusParam) {
return request<UploadRcsTaskStatusResponseData>({
url: "/api/Wms/PostAgvExcutionStatus",
method: "post",
data
})
}
/** 修改rcs任务信息 */
export function UpdateRcsTask(data:RcsTask) {
return request<UpdateRcsTaskResponseData>({
url: "/api/Test/UpdateRcsTask",
method: "post",
data
})
}
export type StockInByPdaResponseData = ApiResponseData<boolean>
export type UpdateRcsTaskResponseData = ApiResponseData<boolean>
export type GetRcsTaskLstResponseData = ApiResponseData<RcsTask[]>
export type UploadRcsTaskStatusResponseData = ApiResponseData<string>
@@ -0,0 +1,17 @@
import { StockInAndOutStatisticInfo } from "@/dto/statistic/stockInAndOutStatisticInfo"
import { request } from "@/utils/service"
/** 获取当日出入库统计结果 */
export function GetStockInAndOutStatistic() {
return request<GetStockInAndOutStatisticResponseData>({
url: "/api/Statistic/GetStockInAndOutStatistic",
method: "post",
})
}
export type GetStockInAndOutStatisticResponseData = ApiResponseData<StockInAndOutStatisticInfo>
@@ -0,0 +1,11 @@
import * as stockInDetail from '@/dto/stockInDetail/stockInDetail'
import { request } from "@/utils/service"
export function GetStockinDetailToday(elvatorStockinNo:string) {
return request<GetStockinDetailTodayResponseData>({
url: "api/Warehouse/GetStockinDetailToday?elvatorStockinNo=" + elvatorStockinNo,
method: "get",
})
}
export type GetStockinDetailTodayResponseData = ApiResponseDataArray<stockInDetail.stockInDetail>
+13
View File
@@ -0,0 +1,13 @@
import {StockOutParam} from '@/dto/stockOut/stockOutParam'
import { request } from "@/utils/service"
export function StockOut(param: StockOutParam) {
return request<StockOutResponseData>({
url: "/api/Test/StockOut",
method: "post",
data:param
})
}
export type StockOutResponseData = ApiResponseData<boolean>
@@ -0,0 +1,13 @@
import { StorageRackInfo } from "@/dto/wareHouse/wareHouse"
import { request } from "@/utils/service"
export function GetStorageRackLst(floor: number) {
return request<GetStorageRackInfoResponseData>({
url: "/api/Warehouse/GetStorageRackLst?floor="+floor,
method: "post"
})
}
export type GetStorageRackInfoResponseData = ApiResponseData<StorageRackInfo[]>
@@ -0,0 +1,12 @@
import { request } from "@/utils/service"
import * as SupplierPaperStatistic from "../../dto/supplierPaperStatistic/SupplierPaperStatistic"
export type SupplierPaperStatisticData = ApiResponseDataArray<SupplierPaperStatistic.SupplierPaperStatisticInfo>
/** 获取供应商原纸统计信息 */
export function QuerySupplierPaperStatisticInfo(data: SupplierPaperStatistic.SupplierPaperStatisticInfoQueryParam) {
return request<SupplierPaperStatisticData>({
url: "/api/PaperOrderMap/GetSupplierPaperStatistic",
method: "post",
data
})
}
+37
View File
@@ -0,0 +1,37 @@
import { request } from "@/utils/service"
import type * as Table from "./types/table"
/** 增 */
export function createTableDataApi(data: Table.CreateTableRequestData) {
return request({
url: "table",
method: "post",
data
})
}
/** 删 */
export function deleteTableDataApi(id: string) {
return request({
url: `table/${id}`,
method: "delete"
})
}
/** 改 */
export function updateTableDataApi(data: Table.UpdateTableRequestData) {
return request({
url: "table",
method: "put",
data
})
}
/** 查 */
export function getTableDataApi(params: Table.GetTableRequestData) {
return request<Table.GetTableResponseData>({
url: "table",
method: "get",
params
})
}
+36
View File
@@ -0,0 +1,36 @@
export interface CreateTableRequestData {
username: string
password: string
}
export interface UpdateTableRequestData {
id: string
username: string
password?: string
}
export interface GetTableRequestData {
/** 当前页码 */
currentPage: number
/** 查询条数 */
size: number
/** 查询参数:用户名 */
username?: string
/** 查询参数:手机号 */
phone?: string
}
export interface GetTableData {
createTime: string
email: string
id: string
phone: string
roles: string
status: boolean
username: string
}
export type GetTableResponseData = ApiResponseData<{
list: GetTableData[]
total: number
}>
@@ -0,0 +1,36 @@
import { WareHouseAreaInfo,StorageRackInfo } from "@/dto/wareHouse/WareHouse"
import {MaterialQueryParam} from "@/dto/wareHouse/MaterialQueryParam"
import { request } from "@/utils/service"
/** 获取库区信息 */
export function GetWareHouseAreaInfo(floor: number) {
return request<GetWareHouseAreaInfoResponseData>({
url: "/api/Warehouse/GetWareHouseAreaInfo?floor="+floor,
method: "post"
})
}
//更新库位信息
export function UpdateWareHouse(warehouse: StorageRackInfo) {
return request<UpdateWareResponseData>({
url: "/api/Warehouse/Update",
method: "post",
data:warehouse
})
}
//查询物料信息
export function GetMaterialInfo(param: MaterialQueryParam) {
return request<GetMaterialInfoResponseData>({
url: "/api/Warehouse/GetMaterialInfo",
method: "post",
data:param
})
}
export type GetWareHouseAreaInfoResponseData = ApiResponseData<WareHouseAreaInfo[]>
export type UpdateWareResponseData = ApiResponseData<boolean>
export type GetMaterialInfoResponseData = ApiResponseData<StorageRackInfo[]>
Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+97
View File
@@ -0,0 +1,97 @@
// 颜色
$colors: (
"primary": #db9e3f,
"info-1": #4394e4,
"info": #4b67af,
"white": #ffffff,
"light": #f9f9f9,
"grey-1": #999999,
"grey": #666666,
"dark-1": #5f5f5f,
"dark": #222222,
"black-1": #171823,
"black": #000000,
);
// 字体大小
$base-font-size: 0.2rem;
$font-sizes: (
xxs: 0.1,
//8px
xs: 0.125,
//10px
sm: 0.2875,
//12px
md: 0.1625,
//13px
lg: 0.175,
//14px
xl: 0.2,
//16px
xxl: 0.225,
//18px
xxxl: 0.25 //20px,,,,
);
// 宽高
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
//flex
.d-flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
$flex-jc: (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around,
evenly: space-evenly,
);
$flex-ai: (
start: flex-start,
end: flex-end,
center: center,
stretch: stretch,
);
.flex-1 {
flex: 1;
}
//.mt-1 => margin top
//spacing
$spacing-types: (
m: margin,
p: padding,
);
$spacing-directions: (
t: top,
r: right,
b: bottom,
l: left,
);
$spacing-base-size: 0.2rem;
$spacing-sizes: (
0: 0,
1: 0.25,
2: 0.5,
3: 1,
4: 1.5,
5: 3,
);
+139
View File
@@ -0,0 +1,139 @@
#index {
color: #d3d6dd;
// background-color: black;
width: 100%;
height: 100vh;
// position: absolute;
transform-origin: left top;
overflow: hidden;
.bg {
width: 100%;
height: 100%;
padding: 0.2rem 0.2rem 0 0.2rem;
background-image:url("/img/pageBg.png") ;
background-size: cover;
background-position: center center;
}
.host-body {
.bg{
color: red($color: red);
}
.title {
position: relative;
width: 6.25rem;
text-align: center;
background-size: cover;
background-repeat: no-repeat;
.title-text {
font-size: 0.3rem;
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%);
}
.title-bototm {
position: absolute;
bottom: -0.375rem;
left: 50%;
transform: translate(-50%);
}
}
// 第二行
.aside-width {
width: 40%;
}
.react-r-s,
.react-l-s {
// background-color: #0f1325;
background-color: #1a5cd7;
}
// 平行四边形
.react-right {
&.react-l-s {
text-align: right;
width: 430px;
}
font-size: 18px;
width: 300px;
line-height: 50px;
text-align: center;
transform: skewX(-45deg);
.react-after {
position: absolute;
right: -25px;
top: 0;
height: 50px;
width: 50px;
// background-color: #0f1325;
background-color: #1a5cd7;
transform: skewX(45deg);
}
.text {
display: inline-block;
transform: skewX(45deg);
}
}
.react-left {
&.react-l-s {
width: 500px;
text-align: left;
}
font-size: 18px;
width: 300px;
height: 50px;
line-height: 50px;
text-align: center;
transform: skewX(45deg);
// background-color: #0f1325;
background-color: #1a5cd7;
.react-left {
position: absolute;
left: -25px;
top: 0;
height: 50px;
width: 50px;
// background-color: #0f1325;
background-color: #1a5cd7;
transform: skewX(-45deg);
}
.text {
display: inline-block;
transform: skewX(-45deg);
}
}
.body-box {
margin-top: 0.2rem;
display: flex;
flex-direction: column;
//下方区域的布局
.content-box {
display: grid;
grid-template-columns: 4fr 7fr 4fr;
}
// 底部数据
.bototm-box {
margin-top: 0.125rem;
display: grid;
grid-template-columns: repeat(2, 50%);
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
@import "./variables";
// 全局样式
* {
margin: 0;
padding: 0;
list-style-type: none;
box-sizing: border-box;
outline: none;
}
html {
margin: 0;
padding: 0;
}
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.2em;
background-color: #f1f1f1;
margin: 0;
padding: 0;
}
a {
color: #343440;
text-decoration: none;
}
.clearfix {
&::after {
content: "";
display: table;
height: 0;
line-height: 0;
visibility: hidden;
clear: both;
}
}
//浮动
.float-r {
float: right;
}
//浮动
.float-l {
float: left;
}
// 字体加粗
.fw-b {
font-weight: bold;
}
//文章一行显示,多余省略号显示
.title-item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// .bg-color-black {
// background-color: rgba(19, 25, 47, 0.6);
// }
.bg-color-blue {
background-color: #1a5cd7;
}
.colorBlack {
color: #272727 !important;
&:hover {
color: #272727 !important;
}
}
.colorGrass {
color: #33cea0;
&:hover {
color: #33cea0 !important;
}
}
.colorRed {
color: #ff5722;
&:hover {
color: #ff5722 !important;
}
}
.colorText {
color: #d3d6dd !important;
&:hover {
color: #d3d6dd !important;
}
}
.colorBlue {
color: #257dff !important;
&:hover {
color: #257dff !important;
}
}
//颜色
@each $colorkey, $color in $colors {
.text-#{$colorkey} {
color: $color;
}
.bg-#{$colorkey} {
background-color: $color;
}
}
//对齐
@each $var in (left, center, right) {
.text-#{$var} {
text-align: $var !important;
}
}
//flex
@each $key, $value in $flex-jc {
.jc-#{$key} {
justify-content: $value;
}
}
@each $key, $value in $flex-ai {
.ai-#{$key} {
align-items: $value;
}
}
//字体
@each $fontkey, $fontvalue in $font-sizes {
.fs-#{$fontkey} {
font-size: $fontvalue * $base-font-size;
}
}
//.mt-1 => margin top
//spacing
@each $typekey, $type in $spacing-types {
//.m-1
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}-#{$sizekey} {
#{$type}: $size * $spacing-base-size;
}
}
//.mx-1
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}x-#{$sizekey} {
#{$type}-left: $size * $spacing-base-size;
#{$type}-right: $size * $spacing-base-size;
}
.#{$typekey}y-#{$sizekey} {
#{$type}-top: $size * $spacing-base-size;
#{$type}-bottom: $size * $spacing-base-size;
}
}
//.mt-1
@each $directionkey, $direction in $spacing-directions {
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}#{$directionkey}-#{$sizekey} {
#{$type}-#{$direction}: $size * $spacing-base-size;
}
}
}
.#{$typekey} {
#{$type}: 0;
}
}
@@ -0,0 +1,58 @@
<script lang="ts" setup>
import { type ListItem } from "./data"
interface Props {
list: ListItem[]
}
const props = defineProps<Props>()
</script>
<template>
<el-empty v-if="props.list.length === 0" />
<el-card v-else v-for="(item, index) in props.list" :key="index" shadow="never" class="card-container">
<template #header>
<div class="card-header">
<div>
<span>
<span class="card-title">{{ item.title }}</span>
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
</span>
<div class="card-time">{{ item.datetime }}</div>
</div>
<div v-if="item.avatar" class="card-avatar">
<img :src="item.avatar" width="34" />
</div>
</div>
</template>
<div class="card-body">
{{ item.description ?? "No Data" }}
</div>
</el-card>
</template>
<style lang="scss" scoped>
.card-container {
margin-bottom: 10px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
font-weight: bold;
margin-right: 10px;
}
.card-time {
font-size: 12px;
color: grey;
}
.card-avatar {
display: flex;
align-items: center;
}
}
.card-body {
font-size: 12px;
}
}
</style>
+66
View File
@@ -0,0 +1,66 @@
export interface ListItem {
avatar?: string
title: string
datetime?: string
description?: string
status?: "" | "success" | "info" | "warning" | "danger"
extra?: string
}
export const notifyData: ListItem[] = [
{
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
title: "V3 Admin Vite 上线啦",
datetime: "一年前",
description:
"一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
},
{
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
title: "V3 Admin 上线啦",
datetime: "两年前",
description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia"
}
]
export const messageData: ListItem[] = [
{
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
title: "来自楚门的世界",
description: "如果再也不能见到你,祝你早安、午安和晚安",
datetime: "1998-06-05"
},
{
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
title: "来自大话西游",
description: "如果非要在这份爱上加上一个期限,我希望是一万年",
datetime: "1995-02-04"
},
{
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
title: "来自龙猫",
description: "心存善意,定能途遇天使",
datetime: "1988-04-16"
}
]
export const todoData: ListItem[] = [
{
title: "任务名称",
description: "这家伙很懒,什么都没留下",
extra: "未开始",
status: "info"
},
{
title: "任务名称",
description: "这家伙很懒,什么都没留下",
extra: "进行中",
status: ""
},
{
title: "任务名称",
description: "这家伙很懒,什么都没留下",
extra: "已超时",
status: "danger"
}
]
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import { ref, computed } from "vue"
import { ElMessage } from "element-plus"
import { Bell } from "@element-plus/icons-vue"
import NotifyList from "./NotifyList.vue"
import { type ListItem, notifyData, messageData, todoData } from "./data"
type TabName = "通知" | "消息" | "待办"
interface DataItem {
name: TabName
type: "primary" | "success" | "warning" | "danger" | "info"
list: ListItem[]
}
/** 角标当前值 */
const badgeValue = computed(() => {
return data.value.reduce((sum, item) => sum + item.list.length, 0)
})
/** 角标最大值 */
const badgeMax = 99
/** 面板宽度 */
const popoverWidth = 350
/** 当前 Tab */
const activeName = ref<TabName>("通知")
/** 所有数据 */
const data = ref<DataItem[]>([
// 通知数据
{
name: "通知",
type: "primary",
list: notifyData
},
// 消息数据
{
name: "消息",
type: "danger",
list: messageData
},
// 待办数据
{
name: "待办",
type: "warning",
list: todoData
}
])
const handleHistory = () => {
ElMessage.success(`跳转到${activeName.value}历史页面`)
}
</script>
<template>
<div class="notify">
<el-popover placement="bottom" :width="popoverWidth" trigger="click">
<template #reference>
<el-badge :value="badgeValue" :max="badgeMax" :hidden="badgeValue === 0">
<el-tooltip effect="dark" content="消息通知" placement="bottom">
<el-icon :size="20">
<Bell />
</el-icon>
</el-tooltip>
</el-badge>
</template>
<template #default>
<el-tabs v-model="activeName" class="demo-tabs" stretch>
<el-tab-pane v-for="(item, index) in data" :name="item.name" :key="index">
<template #label>
{{ item.name }}
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
</template>
<el-scrollbar height="400px">
<NotifyList :list="item.list" />
</el-scrollbar>
</el-tab-pane>
</el-tabs>
<div class="notify-history">
<el-button link @click="handleHistory">查看{{ activeName }}历史</el-button>
</div>
</template>
</el-popover>
</div>
</template>
<style lang="scss" scoped>
.notify {
margin-right: 10px;
color: var(--el-text-color-regular);
}
.notify-history {
text-align: center;
padding-top: 12px;
border-top: 1px solid var(--el-border-color);
}
</style>
@@ -0,0 +1,92 @@
<script lang="ts" setup>
import { computed, ref, watchEffect } from "vue"
import { ElMessage } from "element-plus"
import screenfull from "screenfull"
interface Props {
/** 全屏的元素,默认是 html */
element?: string
/** 打开全屏提示语 */
openTips?: string
/** 关闭全屏提示语 */
exitTips?: string
/** 是否只针对内容区 */
content?: boolean
}
const props = withDefaults(defineProps<Props>(), {
element: "html",
openTips: "全屏",
exitTips: "退出全屏",
content: false
})
//#region 全屏
const isFullscreen = ref<boolean>(false)
const fullscreenTips = computed(() => {
return isFullscreen.value ? props.exitTips : props.openTips
})
const fullscreenSvgName = computed(() => {
return isFullscreen.value ? "fullscreen-exit" : "fullscreen"
})
const handleFullscreenClick = () => {
const dom = document.querySelector(props.element) || undefined
screenfull.isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
}
const handleFullscreenChange = () => {
isFullscreen.value = screenfull.isFullscreen
}
watchEffect((onCleanup) => {
// 挂载组件时自动执行
screenfull.on("change", handleFullscreenChange)
// 卸载组件时自动执行
onCleanup(() => {
screenfull.isEnabled && screenfull.off("change", handleFullscreenChange)
})
})
//#endregion
//#region 内容区
const isContentLarge = ref<boolean>(false)
const contentLargeTips = computed(() => {
return isContentLarge.value ? "内容区复原" : "内容区放大"
})
const contentLargeSvgName = computed(() => {
return isContentLarge.value ? "fullscreen-exit" : "fullscreen"
})
const handleContentLargeClick = () => {
document.body.className = !isContentLarge.value ? "content-large" : ""
isContentLarge.value = !isContentLarge.value
}
//#endregion
</script>
<template>
<div>
<!-- 全屏 -->
<el-tooltip v-if="!content" effect="dark" :content="fullscreenTips" placement="bottom">
<SvgIcon :name="fullscreenSvgName" @click="handleFullscreenClick" />
</el-tooltip>
<!-- 内容区 -->
<el-dropdown v-else>
<SvgIcon :name="contentLargeSvgName" />
<template #dropdown>
<el-dropdown-menu>
<!-- 内容区放大 -->
<el-dropdown-item @click="handleContentLargeClick">{{ contentLargeTips }}</el-dropdown-item>
<!-- 内容区全屏 -->
<el-dropdown-item @click="handleFullscreenClick" :disabled="isFullscreen">内容区全屏</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<style lang="scss" scoped>
.svg-icon {
font-size: 20px;
&:focus {
outline: none;
}
}
</style>
@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { computed } from "vue"
import { useAppStore } from "@/store/modules/app"
import { DeviceEnum } from "@/constants/app-key"
interface Props {
total: number
}
const props = defineProps<Props>()
const appStore = useAppStore()
const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)
</script>
<template>
<div class="search-footer">
<template v-if="!isMobile">
<span class="search-footer-item">
<SvgIcon name="keyboard-enter" />
<span>确认</span>
</span>
<span class="search-footer-item">
<SvgIcon name="keyboard-up" />
<SvgIcon name="keyboard-down" />
<span>切换</span>
</span>
<span class="search-footer-item">
<SvgIcon name="keyboard-esc" />
<span>关闭</span>
</span>
</template>
<span class="search-footer-total">共 {{ props.total }} 项</span>
</div>
</template>
<style lang="scss" scoped>
.search-footer {
display: flex;
color: var(--el-text-color-secondary);
font-size: 14px;
&-item {
display: flex;
align-items: center;
margin-right: 12px;
.svg-icon {
margin-right: 5px;
padding: 2px;
font-size: 20px;
background-color: var(--el-fill-color);
}
}
&-total {
margin: 0 0 0 auto;
}
}
</style>
@@ -0,0 +1,219 @@
<script lang="ts" setup>
import { computed, ref, shallowRef } from "vue"
import { type RouteRecordName, type RouteRecordRaw, useRouter } from "vue-router"
import { useAppStore } from "@/store/modules/app"
import { usePermissionStore } from "@/store/modules/permission"
import SearchResult from "./SearchResult.vue"
import SearchFooter from "./SearchFooter.vue"
import { ElMessage, ElScrollbar } from "element-plus"
import { cloneDeep, debounce } from "lodash-es"
import { DeviceEnum } from "@/constants/app-key"
import { isExternal } from "@/utils/validate"
interface Props {
/** 控制 modal 显隐 */
modelValue: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
"update:modelValue": [boolean]
}>()
const appStore = useAppStore()
const router = useRouter()
const inputRef = ref<HTMLInputElement | null>(null)
const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
const searchResultRef = ref<InstanceType<typeof SearchResult> | null>(null)
const keyword = ref<string>("")
const resultList = shallowRef<RouteRecordRaw[]>([])
const activeRouteName = ref<RouteRecordName | undefined>(undefined)
/** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */
const isPressUpOrDown = ref<boolean>(false)
/** 控制搜索对话框宽度 */
const modalWidth = computed(() => (appStore.device === DeviceEnum.Mobile ? "80vw" : "40vw"))
/** 控制搜索对话框显隐 */
const modalVisible = computed({
get() {
return props.modelValue
},
set(value: boolean) {
emit("update:modelValue", value)
}
})
/** 树形菜单 */
const menusData = computed(() => cloneDeep(usePermissionStore().routes))
/** 搜索(防抖) */
const handleSearch = debounce(() => {
const flatMenusData = flatTree(menusData.value)
resultList.value = flatMenusData.filter((menu) =>
keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false
)
// 默认选中搜索结果的第一项
const length = resultList.value?.length
activeRouteName.value = length > 0 ? resultList.value[0].name : undefined
}, 500)
/** 将树形菜单扁平化为一维数组,用于菜单搜索 */
const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
arr.forEach((item) => {
result.push(item)
item.children && flatTree(item.children, result)
})
return result
}
/** 关闭搜索对话框 */
const handleClose = () => {
modalVisible.value = false
// 延时处理防止用户看到重置数据的操作
setTimeout(() => {
keyword.value = ""
resultList.value = []
}, 200)
}
/** 根据下标位置进行滚动 */
const scrollTo = (index: number) => {
if (!searchResultRef.value) return
const scrollTop = searchResultRef.value.getScrollTop(index)
// 手动控制 el-scrollbar 滚动条滚动,设置滚动条到顶部的距离
scrollbarRef.value?.setScrollTop(scrollTop)
}
/** 键盘上键 */
const handleUp = () => {
isPressUpOrDown.value = true
const { length } = resultList.value
if (length === 0) return
// 获取该 name 在菜单中第一次出现的位置
const index = resultList.value.findIndex((item) => item.name === activeRouteName.value)
// 如果已处在顶部
if (index === 0) {
const bottomName = resultList.value[length - 1].name
// 如果顶部和底部的 bottomName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的上键不能生效的问题)
if (activeRouteName.value === bottomName && length > 1) {
activeRouteName.value = resultList.value[length - 2].name
scrollTo(length - 2)
} else {
// 跳转到底部
activeRouteName.value = bottomName
scrollTo(length - 1)
}
} else {
activeRouteName.value = resultList.value[index - 1].name
scrollTo(index - 1)
}
}
/** 键盘下键 */
const handleDown = () => {
isPressUpOrDown.value = true
const { length } = resultList.value
if (length === 0) return
// 获取该 name 在菜单中最后一次出现的位置(可解决遇到连续两个相同 name 导致的下键不能生效的问题)
const index = resultList.value.map((item) => item.name).lastIndexOf(activeRouteName.value)
// 如果已处在底部
if (index === length - 1) {
const topName = resultList.value[0].name
// 如果底部和顶部的 topName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的下键不能生效的问题)
if (activeRouteName.value === topName && length > 1) {
activeRouteName.value = resultList.value[1].name
scrollTo(1)
} else {
// 跳转到顶部
activeRouteName.value = topName
scrollTo(0)
}
} else {
activeRouteName.value = resultList.value[index + 1].name
scrollTo(index + 1)
}
}
/** 键盘回车键 */
const handleEnter = () => {
const { length } = resultList.value
if (length === 0) return
const name = activeRouteName.value
const path = resultList.value.find((item) => item.name === name)?.path
if (path && isExternal(path)) {
window.open(path, "_blank", "noopener, noreferrer")
return
}
if (!name) {
ElMessage.warning("无法通过搜索进入该菜单,请为对应的路由设置唯一的 Name")
return
}
try {
router.push({ name })
} catch {
ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入")
return
}
handleClose()
}
/** 释放上键或下键 */
const handleReleaseUpOrDown = () => {
isPressUpOrDown.value = false
}
</script>
<template>
<el-dialog
v-model="modalVisible"
@opened="inputRef?.focus()"
@closed="inputRef?.blur()"
@keydown.up="handleUp"
@keydown.down="handleDown"
@keydown.enter="handleEnter"
@keyup.up.down="handleReleaseUpOrDown"
:before-close="handleClose"
:width="modalWidth"
top="5vh"
class="search-modal__private"
append-to-body
>
<el-input ref="inputRef" v-model="keyword" @input="handleSearch" placeholder="搜索菜单" size="large" clearable>
<template #prefix>
<SvgIcon name="search" />
</template>
</el-input>
<el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
<template v-else>
<p>搜索结果</p>
<el-scrollbar ref="scrollbarRef" max-height="40vh" always>
<SearchResult
ref="searchResultRef"
v-model="activeRouteName"
:list="resultList"
:isPressUpOrDown="isPressUpOrDown"
@click="handleEnter"
/>
</el-scrollbar>
</template>
<template #footer>
<SearchFooter :total="resultList.length" />
</template>
</el-dialog>
</template>
<style lang="scss">
.search-modal__private {
.svg-icon {
font-size: 18px;
}
.el-dialog__header {
display: none;
}
.el-dialog__footer {
border-top: 1px solid var(--el-border-color);
padding: var(--el-dialog-padding-primary);
}
}
</style>
@@ -0,0 +1,122 @@
<script lang="ts" setup>
import { computed, getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue"
import { type RouteRecordName, type RouteRecordRaw } from "vue-router"
interface Props {
modelValue: RouteRecordName | undefined
list: RouteRecordRaw[]
isPressUpOrDown: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
"update:modelValue": [RouteRecordName | undefined]
}>()
const instance = getCurrentInstance()
const scrollbarHeight = ref<number>(0)
/** 选中的菜单 */
const activeRouteName = computed({
get() {
return props.modelValue
},
set(value: RouteRecordName | undefined) {
emit("update:modelValue", value)
}
})
/** 菜单的样式 */
const itemStyle = (item: RouteRecordRaw) => {
const flag = item.name === activeRouteName.value
return {
background: flag ? "var(--el-color-primary)" : "",
color: flag ? "#fff" : ""
}
}
/** 鼠标移入 */
const handleMouseenter = (item: RouteRecordRaw) => {
// 如果上键或下键与 mouseenter 事件同时生效,则以上下键为准,不执行该函数的赋值逻辑
if (props.isPressUpOrDown) return
activeRouteName.value = item.name
}
/** 计算滚动可视区高度 */
const getScrollbarHeight = () => {
// el-scrollbar max-height="40vh"
scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1))
}
/** 根据下标计算到顶部的距离 */
const getScrollTop = (index: number) => {
const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[]
if (!currentInstance) return 0
const currentRef = currentInstance[0]
const scrollTop = currentRef.offsetTop + 128 // 128 = 两个 result-item (56 + 56 = 112)高度与上下 margin(8 + 8 = 16)大小之和
return scrollTop > scrollbarHeight.value ? scrollTop - scrollbarHeight.value : 0
}
/** 在组件挂载前添加窗口大小变化事件监听器 */
onBeforeMount(() => {
window.addEventListener("resize", getScrollbarHeight)
})
/** 在组件挂载时立即计算滚动可视区高度 */
onMounted(() => {
getScrollbarHeight()
})
/** 在组件卸载前移除窗口大小变化事件监听器 */
onBeforeUnmount(() => {
window.removeEventListener("resize", getScrollbarHeight)
})
defineExpose({ getScrollTop })
</script>
<template>
<!-- 外层 div 不能删除,是用来接收父组件 click 事件的 -->
<div>
<div
v-for="(item, index) in list"
:key="index"
:ref="`resultItemRef${index}`"
class="result-item"
:style="itemStyle(item)"
@mouseenter="handleMouseenter(item)"
>
<SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" />
<component v-else-if="item.meta?.elIcon" :is="item.meta.elIcon" class="el-icon" />
<span class="result-item-title">
{{ item.meta?.title }}
</span>
<SvgIcon v-if="activeRouteName && activeRouteName === item.name" name="keyboard-enter" />
</div>
</div>
</template>
<style lang="scss" scoped>
.result-item {
display: flex;
align-items: center;
height: 56px;
padding: 0 15px;
margin-top: 8px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
.svg-icon {
min-width: 1em;
font-size: 18px;
}
.el-icon {
width: 1em;
font-size: 18px;
}
&-title {
flex: 1;
margin-left: 12px;
}
}
</style>
@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { ref } from "vue"
import SearchModal from "./SearchModal.vue"
/** 控制 modal 显隐 */
const modalVisible = ref<boolean>(false)
/** 打开 modal */
const handleOpen = () => {
modalVisible.value = true
}
</script>
<template>
<div>
<el-tooltip effect="dark" content="搜索菜单" placement="bottom">
<SvgIcon name="search" @click="handleOpen" />
</el-tooltip>
<SearchModal v-model="modalVisible" />
</div>
</template>
<style lang="scss" scoped>
.svg-icon {
font-size: 20px;
&:focus {
outline: none;
}
}
</style>
@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { computed } from "vue"
interface Props {
prefix?: string
name: string
}
const props = withDefaults(defineProps<Props>(), {
prefix: "icon"
})
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
</script>
<template>
<svg class="svg-icon" aria-hidden="true">
<use :href="symbolId" />
</svg>
</template>
<style lang="scss" scoped>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
</style>
@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { useTheme } from "@/hooks/useTheme"
import { MagicStick } from "@element-plus/icons-vue"
const { themeList, activeThemeName, setTheme } = useTheme()
</script>
<template>
<el-dropdown trigger="click" @command="setTheme">
<div>
<el-tooltip effect="dark" content="主题模式" placement="bottom">
<el-icon :size="20">
<MagicStick />
</el-icon>
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(theme, index) in themeList"
:key="index"
:disabled="activeThemeName === theme.name"
:command="theme.name"
>
<span>{{ theme.title }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
+12
View File
@@ -0,0 +1,12 @@
export interface IGlobalConfig {
baseApiUrl: string
}
export const loadConfigFile = async () => {
const response = await fetch('/js/globalConfig.json');
if (!response.ok) {
throw new Error('Failed to load configuration file');
}
return response.json();
};
+48
View File
@@ -0,0 +1,48 @@
import { getConfigLayout } from "@/utils/cache/local-storage"
/** 项目配置类型 */
export interface LayoutSettings {
/** 是否显示 Settings Panel */
showSettings: boolean
/** 布局模式 */
layoutMode: "left" | "top" | "left-top"
/** 是否显示标签栏 */
showTagsView: boolean
/** 是否显示 Logo */
showLogo: boolean
/** 是否固定 Header */
fixedHeader: boolean
/** 是否显示消息通知 */
showNotify: boolean
/** 是否显示切换主题按钮 */
showThemeSwitch: boolean
/** 是否显示全屏按钮 */
showScreenfull: boolean
/** 是否显示搜索按钮 */
showSearchMenu: boolean
/** 是否缓存标签栏 */
cacheTagsView: boolean
/** 是否显示灰色模式 */
showGreyMode: boolean
/** 是否显示色弱模式 */
showColorWeakness: boolean
}
/** 默认配置 */
const defaultSettings: LayoutSettings = {
layoutMode: "left",
showSettings: true,
showTagsView: true,
fixedHeader: true,
showLogo: true,
showNotify: true,
showThemeSwitch: true,
showScreenfull: true,
showSearchMenu: true,
cacheTagsView: false,
showGreyMode: false,
showColorWeakness: false
}
/** 项目配置 */
export const layoutSettings: LayoutSettings = { ...defaultSettings, ...getConfigLayout() }
+28
View File
@@ -0,0 +1,28 @@
/** 动态路由配置 */
interface RouteSettings {
/**
* 是否开启动态路由功能?
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 async: false
*/
async: boolean
/** 当动态路由功能关闭时:
* 1. 应该将所有路由都写到常驻路由里面(表明所有登陆的用户能访问的页面都是一样的)
* 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
*/
defaultRoles: Array<string>
/**
* 是否开启三级及其以上路由缓存功能?
* 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
* 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
*/
thirdLevelRouteCache: boolean
}
const routeSettings: RouteSettings = {
async: false,
defaultRoles: ["DEFAULT_ROLE"],
thirdLevelRouteCache: false
}
export default routeSettings
+15
View File
@@ -0,0 +1,15 @@
import { type RouteLocationNormalized } from "vue-router"
/** 免登录白名单(匹配路由 path) */
const whiteListByPath: string[] = ["/login"]
/** 免登录白名单(匹配路由 name) */
const whiteListByName: string[] = []
/** 判断是否在白名单 */
const isWhiteList = (to: RouteLocationNormalized) => {
// path 和 name 任意一个匹配上即可
return whiteListByPath.indexOf(to.path) !== -1 || whiteListByName.indexOf(to.name as any) !== -1
}
export default isWhiteList
+13
View File
@@ -0,0 +1,13 @@
/** 设备类型 */
export enum DeviceEnum {
Mobile,
Desktop
}
/** 侧边栏打开状态常量 */
export const SIDEBAR_OPENED = "opened"
/** 侧边栏关闭状态常量 */
export const SIDEBAR_CLOSED = "closed"
export type SidebarOpened = typeof SIDEBAR_OPENED
export type SidebarClosed = typeof SIDEBAR_CLOSED
+15
View File
@@ -0,0 +1,15 @@
const SYSTEM_NAME = "acs-office"
/** 缓存数据时用到的 Key */
class CacheKey {
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key`
static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key`
static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key`
static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key`
static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key`
static readonly PRODUCTION_CONFIG = `${SYSTEM_NAME}-cached-production-config-key`
static readonly USER_NAME = `${SYSTEM_NAME}-cached-username-key`
}
export default CacheKey
+7
View File
@@ -0,0 +1,7 @@
import { type App } from "vue"
import { permission } from "./permission"
/** 挂载自定义指令 */
export function loadDirectives(app: App) {
app.directive("permission", permission)
}
@@ -0,0 +1,17 @@
import { type Directive } from "vue"
import { useUserStoreHook } from "@/store/modules/user"
/** 权限指令,和权限判断函数 checkPermission 功能类似 */
export const permission: Directive = {
mounted(el, binding) {
const { value: permissionRoles } = binding
const { roles } = useUserStoreHook()
if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
const hasPermission = roles.some((role) => permissionRoles.includes(role))
// hasPermission || (el.style.display = "none") // 隐藏
hasPermission || el.parentNode?.removeChild(el) // 销毁
} else {
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
}
+126
View File
@@ -0,0 +1,126 @@
export interface AgvTask{
/**
* 唯一标识 (GUID)
*/
id: string;
/**
* ERP 任务 ID (GUID)
*/
erpTaskId: string;
/**
* 合格证号
*/
barCode: string;
/**
* 订单号
*/
orderNo: string;
/**
* 用户名称
*/
customName: string;
/**
* 物料名称
*/
materiaName: string;
/**
* 物料类型
*/
materiaType: string;
/**
* 单个托盘上面的物料数量
*/
materiaNum: number;
/**
* 批次号(作业单号 + 日期 + 班次)
*/
materiaBatch: string;
/**
* 物料编号
*/
materiaCode: string;
/**
* 联动线出口,机械手设备唯一号
*/
equipmentNo: string;
/**
* 工单状态
*/
taskStatus: number;
/**
* 任务类型
*/
taskType: number;
/**
* 优先级
*/
priority: number;
/**
* 楼层
*/
floor: number;
/**
* 起点(库位编号)
*/
startPositionCode: string;
/**
* 终点(库位编号)
*/
endPositionCode: string;
/**
* 码头位置
*/
transport: number;
/**
* 任务是否锁定(true 表示锁定,false 表示未锁定)
*/
isLock: boolean;
/**
* ERP 数据上传状态
*/
erpUploadStatus: number;
/**
* ERP 返回错误信息
*/
erpReturnMsg: string;
/**
* 重试次数
*/
retryNum: number;
/**
* 分组 ID (GUID)
*/
groupId: string;
/**
* 创建时间(可选)
*/
creationTime?: Date;
/**
* 修改时间(可选)
*/
lastModifiedTime?: Date;
}
@@ -0,0 +1,28 @@
export class ProductionConfig {
web!: number
isDoubleLevel!: boolean
maxLevel!: number
position?: string
mainKnife?: string
maxSpeed?: number
cycleDayTime?: number
paperCodeWordMaxLen?: number
maxSecNum?: number
maxLineNum?: number
rcvOrderFilePath?: string
historyFilePath?: string
recycleFilePath?: string
showInchWeb?: boolean
continuousStop?: number
minSecWidth?: number
minCutlength?: number
localIpAdderss?: string
trimHighLightMinValue?: number
lineCode?: string
enableSpeed?: boolean
stkSecWidth?: number
threeBestSpd?: number
fiveBestSpd?: number
sevenBestSpd?: number
minBestSpd?: number
}
+139
View File
@@ -0,0 +1,139 @@
export interface ErpTask{
/**
* 唯一标识
*/
Id: string; // GUID 转换为 string
/**
* 库位编号(起点)
*/
StorageRackNo: string;
/**
* 订单号
*/
OrderNo: string;
/**
* 生产退库单号
*/
ProduceReturnCode: string;
/**
* 销售单号
*/
SaleNo: string;
/**
* 销售退库单号
*/
SaleReturnCode: string;
/**
* 合格证号
*/
BarCode: string;
/**
* 用户名称
*/
CustomName: string;
/**
* 物料名称
*/
MateriaName: string;
/**
* 物料类型
*/
MateriaType: string;
/**
* 批次号的生成规则为:作业单号+日期+班次
*/
MateriaBatch: string;
/**
* 物料编号
*/
MateriaCode: string;
/**
* 联动线出口,机械手设备唯一号
*/
EquipmentNo: string;
/**
* erp下发命令中出入库总数量
*/
ErpOrderNum: number;
/**
* 任务状态
*/
TaskStatus: number;
/**
* 任务类型
*/
TaskType: number;
/**
* 楼层(入库,尾托回库指定楼层使用)
*/
Floor: number;
/**
* 码头位置 1:北区 2:南区 3:人工 4:退货到车间 99:其它
*/
Transport: number;
/**
* 优先级 1:入库 10:出库 20:托盘入库 30:托盘出库 40:移库 99:其它
*/
Priority: number;
/**
* 锁定 false:未锁定 true:锁定
*/
IsLock: boolean;
/**
* 已上传给Erp的数量
*/
AlreadyUploadNum: number;
/**
* Erp出库撤单数量
*/
ErpCancelNum: number;
/**
* 回调Erp失败/成功 1:成功 0:失败
*/
IsCallBackErpEnd: boolean;
/**
* Erp回调Wms接口地址
*/
ErpCallWmsInterface: string;
/**
* Wms回调Erp接口地址
*/
WmsCallErpInterface: string;
//任务信息(生成失败提示等)
TaskMsg:string,
/**
* 创建时间
*/
CreationTime?: Date; // 可空类型
/**
* 修改时间
*/
LastModifiedTime?: Date; // 可空类型
}
@@ -0,0 +1,5 @@
export interface ErpTaskQueryParam {
StartTime: Date,
EndTime: Date,
TaskType: number
}
@@ -0,0 +1,19 @@
export interface OperationLog {
OperationSource: number,
OperationType: number,
UserId: string,
OldObject: string,
NewObject: string,
CreationTime: Date,
LastModifiedTime: Date
}
export interface OperationLogQueryParam {
StartTime?: Date,
EndTime?: Date,
UserId: string,
OperationType: number,
OperationSource: number,
PageIndex: number,
PageSize: number
}
@@ -0,0 +1,25 @@
export interface PaperBaseInfo {
Barcode: string // 条码
StockItem: string // 物料号
StockDescription: string // 规格型号说明
Location: string //库区
LocSub: string //库位
StockMark: string // 残卷标志
Paper: string // 材质
PaperWidth: string // 幅宽
GPerM2: string // 克重
ActualQty: string // 重量(公斤)
Diameter: string // 直径(毫米)
StockMeter: string // 米数(米)
Maker: string // 制造商
SuppID: string // 供应商编号
SuppShortName: string //供应商简称
ReceiveNote: string // 入库单号
Remark: string // 备注
ReservedCustID: string // 来料客户编号
Status: string //状态
CanUse: string // 可用状态
StockChecking: string //盘点状态
ObjID: string //该纸卷库存的唯一身份标记
InWeight:string //回库重量
}
@@ -0,0 +1,11 @@
export interface PaperCostInfo {
Date: string,
PaperStockOutWeightTotal: number,
PaperStockInWeightTotal: number,
PaperCostTotal: number
}
export interface PaperCostInfoQueryParam{
startTime:string,
endTime:string
}
@@ -0,0 +1,6 @@
export interface PaperUsingRateInfo {
Date: string,
Supplier: string,
PaperCode: string,
UsingRate: string
}
@@ -0,0 +1,23 @@
export interface PaperWastageStatisticInfo
{
Date:string,
Group:string,
HC:MachinePaperInfo,
SF1W:MachinePaperInfo,
SF1X:MachinePaperInfo,
SF2W:MachinePaperInfo,
SF2X:MachinePaperInfo
}
export interface MachinePaperInfo
{
Plan:number,
Used:number,
Wastage:number
}
export interface PaperWastageStatisticQueryParam{
startTime: string;
endTime: string;
}
@@ -0,0 +1,131 @@
import { UUID } from "crypto"
//订单用纸信息
export interface PaperOrderMapInfo {
[key: string]: string | number | undefined | null | void|UUID|Date
Id: UUID; // 在TypeScript中,通常使用string类型代替C#中的Guid,除非有特定的库支持Guid类型
/// <summary>
/// 卷标
/// </summary>
PaperLabel: string;
/// <summary>
/// 纸卷上架时间
/// </summary>
UpTime: Date;
/// <summary>
/// 纸卷下架时间
/// </summary>
DownTime: Date;
/// <summary>
/// 纸卷使用长度
/// </summary>
UsedLength: number;
/// <summary>
/// 幅宽
/// </summary>
Breadth: number;
/// <summary>
/// 纸卷材质
/// </summary>
PaperMaterial: string;
/// <summary>
/// 机台
/// </summary>
MachineNum: string;
/// <summary>
/// 楞型
/// </summary>
Flute: string;
/// <summary>
/// 订单号
/// </summary>
OrderId: string;
/// <summary>
/// 订单号
/// </summary>
OrderNo: string;
/// <summary>
/// 客户名称
/// </summary>
CustomName: string;
/// <summary>
/// 订单楞型
/// </summary>
OrderFlutes: string;
/// <summary>
/// 订单每片长度
/// </summary>
OrderLength: number;
/// <summary>
/// 订单每片宽度
/// </summary>
OrderWidth: number;
/// <summary>
/// 计划生产片数
/// </summary>
OrderPlanqty: number;
/// <summary>
/// 实际生产片数
/// </summary>
OrderProdqty: number;
/// <summary>
/// 订单开始生产时间
/// </summary>
OrderStartTime: Date;
/// <summary>
/// 创建时间
/// </summary>
CreationTime?: Date; // 使用?表示这个属性是可选的,即可以为null或undefined
/// <summary>
/// 修改时间
/// </summary>
LastModifiedTime?: Date; // 同样,这个属性也是可选的
}
//订单用纸信息查询参数
export interface PaperOrderMapQueryParam{
/// <summary>
/// 开始时间
/// </summary>
StartTime?: Date;
/// <summary>
/// 结束时间
/// </summary>
EndTime?: Date;
/// <summary>
/// 订单号
/// </summary>
OrderNo: string;
/// <summary>
/// 订单号集合
/// </summary>
OrderNoList: Array<string>;
/// <summary>
/// 卷标
/// </summary>
PaperLabel: string;
/// <summary>
/// 纸质
/// </summary>
PaperCode: string;
/// <summary>
/// 门幅
/// </summary>
PaperWidth: string;
/// <summary>
/// 机台
/// </summary>
MachineNum: string;
/// <summary>
/// 订单号
/// </summary>
OrderId: string;
/// <summary>
/// 页码
/// </summary>
PageIndex: number,
/// <summary>
/// 每页显示的数据条数
/// </summary>
PageSize: number,
}

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