feat(odf): Add marker pole management and audit logging for v1.2.0
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Add marker pole CRUD functionality with database tables (odf_marker_poles, odf_marker_pole_images) - Implement marker pole API endpoints and service layer with data isolation by department - Add UniApp pages for marker pole list, detail, and creation workflows - Add Vue management backend pages for marker pole and audit log management - Implement audit logging system via ActionFilter to track all business entity modifications - Extend search API to include marker poles in results alongside cables and faults - Add OdfAuditLogsController and service for querying audit trail data - Update optical box detail page with left-right frame color scheme (green-orange) - Add cable type page as entry point for marker poles and fault lists - Create database migration scripts for v1.2.0 schema and permissions - Add DeptDataScopeHelper for department-based data access control - Update MCP settings to disable SQL Server connection and fix formatting - Add marker pole service integration in UniApp with COS image upload support
This commit is contained in:
parent
2fee350fec
commit
827d7a4367
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"api-sqlserver": {
|
||||
"api-sqlserver": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"C:/mcp/mssql-mcp-server/index.js"
|
||||
|
|
@ -30,7 +30,8 @@
|
|||
},
|
||||
"autoApprove": [
|
||||
"execute_sql"
|
||||
]
|
||||
],
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.kiro/specs/odf-v120-marker-pole/.config.kiro
Normal file
1
.kiro/specs/odf-v120-marker-pole/.config.kiro
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"specId": "9a943afb-7e20-40a1-98c9-071c4599edc6", "workflowType": "requirements-first", "specType": "feature"}
|
||||
564
.kiro/specs/odf-v120-marker-pole/design.md
Normal file
564
.kiro/specs/odf-v120-marker-pole/design.md
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
# 技术设计文档 — ODF v1.2.0 标石杆号牌及管理后台优化
|
||||
|
||||
## 概述
|
||||
|
||||
本设计文档覆盖 ODF v1.2.0 版本的全部功能模块,基于现有技术栈(UniApp 前端 + .NET 后端 + Vue 管理后台 + SQL Server 数据库)进行扩展。主要包含:
|
||||
|
||||
1. **标石/杆号牌 CRUD**:新增数据库表 `odf_marker_poles`、`odf_marker_pole_images`,后端 Controller/Service/Model,UniApp 前端 4 个新页面(光缆类型页、列表页、详情页、新增页),管理后台 Vue 页面
|
||||
2. **搜索扩展**:在现有干线搜索 API 和搜索结果页中增加标石/杆号牌类型
|
||||
3. **光交箱 UI 优化**:修改光交箱详情页左右框体配色(左绿右橙)
|
||||
4. **审计日志(修改统计)**:新增 `odf_audit_logs` 表,通过 .NET ActionFilter 自动记录增删改操作
|
||||
5. **组织架构权限**:基于 `DeptId` 的数据隔离、机房权限修复、分公司管理员角色
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 前端
|
||||
A[odf-uniapp<br/>UniApp/Vue3] -->|HTTP REST| C
|
||||
B[ZR.Vue<br/>管理后台] -->|HTTP REST| C
|
||||
end
|
||||
subgraph 后端
|
||||
C[ZR.Admin.WebApi<br/>.NET Controllers] --> D[ZR.Service<br/>业务逻辑层]
|
||||
D --> E[SqlSugar ORM]
|
||||
C --> F[ActionFilter<br/>审计日志拦截]
|
||||
F --> E
|
||||
end
|
||||
subgraph 存储
|
||||
E --> G[(SQL Server)]
|
||||
A -->|直传| H[腾讯云 COS<br/>图片存储]
|
||||
end
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
- **标石/杆号牌新增流程**:拍照 → 水印叠加 → COS 直传 → 获取 COS URL → 提交表单(含 URL)→ 后端入库
|
||||
- **审计日志流程**:Controller Action 执行 → AuditLogFilter 拦截 → 记录操作前后数据 → 写入 `odf_audit_logs`
|
||||
- **数据隔离流程**:请求到达 → 从 JWT Token 提取 DeptId → Service 层查询 `sys_dept` 获取本级及下级部门 ID 列表 → 拼接 WHERE 条件过滤
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 后端组件
|
||||
|
||||
#### 1. 标石/杆号牌模块
|
||||
|
||||
| 组件 | 路径 | 职责 |
|
||||
|------|------|------|
|
||||
| `OdfMarkerPolesController` | `Controllers/Business/OdfMarkerPolesController.cs` | REST API 入口 |
|
||||
| `OdfMarkerPolesService` | `Service/Business/OdfMarkerPolesService.cs` | 业务逻辑 |
|
||||
| `IOdfMarkerPolesService` | `Service/Business/IBusinessService/IOdfMarkerPolesService.cs` | 服务接口 |
|
||||
| `OdfMarkerPoles` | `Model/Business/OdfMarkerPoles.cs` | 实体模型 |
|
||||
| `OdfMarkerPoleImages` | `Model/Business/OdfMarkerPoleImages.cs` | 图片实体 |
|
||||
| `OdfMarkerPolesDto` | `Model/Business/Dto/OdfMarkerPolesDto.cs` | 查询/新增/编辑 DTO |
|
||||
|
||||
#### 2. 审计日志模块
|
||||
|
||||
| 组件 | 路径 | 职责 |
|
||||
|------|------|------|
|
||||
| `OdfAuditLogsController` | `Controllers/Business/OdfAuditLogsController.cs` | 审计日志查询 API |
|
||||
| `OdfAuditLogsService` | `Service/Business/OdfAuditLogsService.cs` | 审计日志业务逻辑 |
|
||||
| `OdfAuditLogs` | `Model/Business/OdfAuditLogs.cs` | 审计日志实体 |
|
||||
| `OdfAuditLogFilter` | `Filters/OdfAuditLogFilter.cs` | ActionFilter,自动拦截记录 |
|
||||
|
||||
#### 3. 数据隔离组件
|
||||
|
||||
| 组件 | 路径 | 职责 |
|
||||
|------|------|------|
|
||||
| `DeptDataScopeHelper` | `Service/Business/DeptDataScopeHelper.cs` | 根据 DeptId 查询本级及下级部门 ID 列表 |
|
||||
|
||||
### API 接口设计
|
||||
|
||||
#### 标石/杆号牌 API
|
||||
|
||||
```
|
||||
GET /business/OdfMarkerPoles/list?cableId={id}&pageNum=1&pageSize=10
|
||||
权限: odfmarkerpoles:list
|
||||
返回: PagedInfo<OdfMarkerPoleListDto>
|
||||
|
||||
GET /business/OdfMarkerPoles/{id}
|
||||
权限: odfmarkerpoles:query
|
||||
返回: OdfMarkerPoleDetailDto(含图片列表)
|
||||
|
||||
POST /business/OdfMarkerPoles/add
|
||||
权限: odfmarkerpoles:add
|
||||
Body: OdfMarkerPoleAddDto
|
||||
返回: int (新记录ID)
|
||||
|
||||
PUT /business/OdfMarkerPoles/edit
|
||||
权限: odfmarkerpoles:edit
|
||||
Body: OdfMarkerPoleEditDto
|
||||
返回: int (影响行数)
|
||||
|
||||
DELETE /business/OdfMarkerPoles/delete/{ids}
|
||||
权限: odfmarkerpoles:delete
|
||||
返回: int (删除行数)
|
||||
```
|
||||
|
||||
#### 搜索 API 扩展
|
||||
|
||||
```
|
||||
GET /business/OdfCables/search?deptId={id}&keyword={kw}
|
||||
现有接口,返回结构新增 markerPoles 字段:
|
||||
{ cables: [...], faults: [...], markerPoles: [...] }
|
||||
```
|
||||
|
||||
#### 审计日志 API
|
||||
|
||||
```
|
||||
GET /business/OdfAuditLogs/list?pageNum=1&pageSize=10&beginTime=&endTime=&operationType=
|
||||
权限: odfauditlogs:list
|
||||
返回: PagedInfo<OdfAuditLogDto>
|
||||
```
|
||||
|
||||
### 前端组件(UniApp)
|
||||
|
||||
| 页面 | 路由 | 职责 |
|
||||
|------|------|------|
|
||||
| 光缆类型页 | `pages/cable-type/index` | 展示【标石、杆号牌】和【故障列表】入口 |
|
||||
| 标石杆号牌列表页 | `pages/marker-pole-list/index` | 展示某光缆下标石/杆号牌列表 |
|
||||
| 标石杆号牌详情页 | `pages/marker-pole-detail/index` | 展示详情,支持导航 |
|
||||
| 新增标石杆号牌页 | `pages/marker-pole-add/index` | 拍照+水印+表单提交 |
|
||||
|
||||
服务层新增 `services/markerPole.js`,封装标石/杆号牌相关 API 调用。
|
||||
|
||||
### 管理后台组件(ZR.Vue)
|
||||
|
||||
| 组件 | 路径 | 职责 |
|
||||
|------|------|------|
|
||||
| `OdfMarkerPoles.vue` | `views/business/OdfMarkerPoles.vue` | 标石/杆号牌 CRUD 管理页 |
|
||||
| `OdfAuditLogs.vue` | `views/business/OdfAuditLogs.vue` | 修改统计查看页 |
|
||||
| `odfmarkerpoles.js` | `api/business/odfmarkerpoles.js` | 标石/杆号牌 API 封装 |
|
||||
| `odfauditlogs.js` | `api/business/odfauditlogs.js` | 审计日志 API 封装 |
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 数据库表设计
|
||||
|
||||
#### 1. `odf_marker_poles` — 标石/杆号牌表
|
||||
|
||||
```sql
|
||||
CREATE TABLE odf_marker_poles (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY, -- 主键
|
||||
CableId INT NOT NULL, -- 关联光缆ID (odf_cables.Id)
|
||||
Name NVARCHAR(200) NOT NULL, -- 标石/杆号牌名称
|
||||
RecordTime DATETIME NOT NULL, -- 记录时间(拍照时间)
|
||||
Personnel NVARCHAR(100) NULL, -- 责任人
|
||||
Latitude DECIMAL(10,7) DEFAULT 0, -- 纬度
|
||||
Longitude DECIMAL(10,7) DEFAULT 0, -- 经度
|
||||
ActualMileage NVARCHAR(100) NULL, -- 实际里程
|
||||
DeptId BIGINT NOT NULL, -- 所属公司/部门ID
|
||||
DeptName NVARCHAR(200) NULL, -- 部门名称(冗余)
|
||||
UserId BIGINT NULL, -- 提交人用户ID
|
||||
CreatedAt DATETIME DEFAULT GETDATE(), -- 创建时间
|
||||
UpdatedAt DATETIME DEFAULT GETDATE() -- 更新时间
|
||||
);
|
||||
|
||||
CREATE INDEX IX_odf_marker_poles_CableId ON odf_marker_poles(CableId);
|
||||
CREATE INDEX IX_odf_marker_poles_DeptId ON odf_marker_poles(DeptId);
|
||||
```
|
||||
|
||||
#### 2. `odf_marker_pole_images` — 标石/杆号牌图片表
|
||||
|
||||
```sql
|
||||
CREATE TABLE odf_marker_pole_images (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
MarkerPoleId INT NOT NULL, -- 关联标石ID (odf_marker_poles.Id)
|
||||
ImageUrl NVARCHAR(500) NOT NULL, -- COS 图片 URL
|
||||
CreatedAt DATETIME DEFAULT GETDATE()
|
||||
);
|
||||
|
||||
CREATE INDEX IX_odf_marker_pole_images_MarkerPoleId ON odf_marker_pole_images(MarkerPoleId);
|
||||
```
|
||||
|
||||
#### 3. `odf_audit_logs` — 审计日志表
|
||||
|
||||
```sql
|
||||
CREATE TABLE odf_audit_logs (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
TableName NVARCHAR(100) NOT NULL, -- 操作的表名
|
||||
RecordId INT NULL, -- 操作的记录ID
|
||||
OperationType NVARCHAR(20) NOT NULL, -- 操作类型: INSERT/UPDATE/DELETE
|
||||
OperatorId BIGINT NOT NULL, -- 操作人用户ID
|
||||
OperatorName NVARCHAR(100) NULL, -- 操作人用户名
|
||||
SourceClient NVARCHAR(20) NOT NULL, -- 操作来源: App/Admin
|
||||
OldData NVARCHAR(MAX) NULL, -- 修改前数据 (JSON)
|
||||
NewData NVARCHAR(MAX) NULL, -- 修改后数据 (JSON)
|
||||
OperationTime DATETIME DEFAULT GETDATE(), -- 操作时间
|
||||
DeptId BIGINT NULL, -- 操作人所属部门
|
||||
Remark NVARCHAR(500) NULL -- 备注
|
||||
);
|
||||
|
||||
CREATE INDEX IX_odf_audit_logs_OperationTime ON odf_audit_logs(OperationTime);
|
||||
CREATE INDEX IX_odf_audit_logs_TableName ON odf_audit_logs(TableName);
|
||||
CREATE INDEX IX_odf_audit_logs_OperationType ON odf_audit_logs(OperationType);
|
||||
```
|
||||
|
||||
### 实体模型
|
||||
|
||||
#### OdfMarkerPoles.cs
|
||||
|
||||
```csharp
|
||||
[SugarTable("odf_marker_poles")]
|
||||
public class OdfMarkerPoles
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
public int CableId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTime RecordTime { get; set; }
|
||||
public string? Personnel { get; set; }
|
||||
public decimal Latitude { get; set; }
|
||||
public decimal Longitude { get; set; }
|
||||
public string? ActualMileage { get; set; }
|
||||
public long DeptId { get; set; }
|
||||
public string? DeptName { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### OdfMarkerPoleImages.cs
|
||||
|
||||
```csharp
|
||||
[SugarTable("odf_marker_pole_images")]
|
||||
public class OdfMarkerPoleImages
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
public int MarkerPoleId { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### OdfAuditLogs.cs
|
||||
|
||||
```csharp
|
||||
[SugarTable("odf_audit_logs")]
|
||||
public class OdfAuditLogs
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
public string TableName { get; set; }
|
||||
public int? RecordId { get; set; }
|
||||
public string OperationType { get; set; }
|
||||
public long OperatorId { get; set; }
|
||||
public string? OperatorName { get; set; }
|
||||
public string SourceClient { get; set; }
|
||||
[SugarColumn(ColumnDataType = "nvarchar(max)")]
|
||||
public string? OldData { get; set; }
|
||||
[SugarColumn(ColumnDataType = "nvarchar(max)")]
|
||||
public string? NewData { get; set; }
|
||||
public DateTime? OperationTime { get; set; }
|
||||
public long? DeptId { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### DTO 设计
|
||||
|
||||
#### OdfMarkerPolesDto.cs
|
||||
|
||||
```csharp
|
||||
// 查询 DTO
|
||||
public class OdfMarkerPolesQueryDto : PagerInfo
|
||||
{
|
||||
public int? CableId { get; set; }
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
// 新增 DTO
|
||||
public class OdfMarkerPoleAddDto
|
||||
{
|
||||
public int CableId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string RecordTime { get; set; }
|
||||
public string? Personnel { get; set; }
|
||||
public decimal Latitude { get; set; }
|
||||
public decimal Longitude { get; set; }
|
||||
public string? ActualMileage { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
public string[] ImageUrls { get; set; }
|
||||
}
|
||||
|
||||
// 编辑 DTO
|
||||
public class OdfMarkerPoleEditDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? Personnel { get; set; }
|
||||
public decimal Latitude { get; set; }
|
||||
public decimal Longitude { get; set; }
|
||||
public string? ActualMileage { get; set; }
|
||||
public int CableId { get; set; }
|
||||
}
|
||||
|
||||
// 审计日志查询 DTO
|
||||
public class OdfAuditLogsQueryDto : PagerInfo
|
||||
{
|
||||
public DateTime? BeginTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public string? OperationType { get; set; }
|
||||
public string? TableName { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 页面路由设计(UniApp pages.json 新增)
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "pages/cable-type/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-list/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-detail/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-add/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
}
|
||||
```
|
||||
|
||||
### 页面导航流程
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[光缆列表页<br/>cable/index] -->|点击光缆| B[光缆类型页<br/>cable-type/index]
|
||||
B -->|标石杆号牌| C[标石列表页<br/>marker-pole-list/index]
|
||||
B -->|故障列表| D[故障列表页<br/>fault-list/index]
|
||||
C -->|点击记录| E[标石详情页<br/>marker-pole-detail/index]
|
||||
C -->|新增| F[新增标石页<br/>marker-pole-add/index]
|
||||
E -->|导航按钮| G[外部导航APP/地图网页]
|
||||
```
|
||||
|
||||
### 权限与数据隔离方案
|
||||
|
||||
#### 数据隔离实现
|
||||
|
||||
`DeptDataScopeHelper` 工具类提供统一的部门范围查询:
|
||||
|
||||
```csharp
|
||||
public static class DeptDataScopeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的部门ID列表(本级 + 所有下级)
|
||||
/// </summary>
|
||||
public static List<long> GetVisibleDeptIds(ISqlSugarClient db, long deptId)
|
||||
{
|
||||
var allDepts = db.Queryable<SysDept>().ToList();
|
||||
var result = new List<long> { deptId };
|
||||
CollectChildDeptIds(allDepts, deptId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void CollectChildDeptIds(List<SysDept> all, long parentId, List<long> result)
|
||||
{
|
||||
var children = all.Where(d => d.ParentId == parentId).ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
result.Add(child.DeptId);
|
||||
CollectChildDeptIds(all, child.DeptId, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 Service 层查询时统一应用:
|
||||
|
||||
```csharp
|
||||
var deptIds = DeptDataScopeHelper.GetVisibleDeptIds(Context, currentDeptId);
|
||||
predicate = predicate.And(it => deptIds.Contains(it.DeptId));
|
||||
```
|
||||
|
||||
需要应用数据隔离的模块:
|
||||
- 标石/杆号牌列表查询
|
||||
- 干线故障列表查询(现有,需补充 DeptId 过滤)
|
||||
- 光缆列表查询
|
||||
|
||||
#### 机房权限修复
|
||||
|
||||
现有机房模块的权限控制需与干线模块对齐:
|
||||
- 在 `OdfPortsController` 的编辑接口上增加 `odfports:edit` 权限校验
|
||||
- 前端 UniApp 在端口编辑弹窗中根据用户权限(从 store 中读取)控制编辑按钮的显示/隐藏
|
||||
- 权限值通过登录接口返回,存储在 `store.permissions` 中
|
||||
|
||||
#### 分公司管理员角色
|
||||
|
||||
- 在 `sys_role` 表中新增「分公司管理员」角色记录
|
||||
- 角色权限包含:查看本公司及下属公司数据、管理本公司及下级公司人员账号
|
||||
- 后端在创建用户时校验:分公司管理员只能创建 DeptId 在其可见范围内的账号
|
||||
- 管理后台登录时校验:分公司账号必须拥有「分公司管理员」角色才允许登录
|
||||
|
||||
### 光交箱 UI 配色方案
|
||||
|
||||
修改 `odf-uniapp/pages/optical-box-detail/index.vue` 中的样式:
|
||||
|
||||
| 区域 | 当前颜色 | 新颜色 |
|
||||
|------|---------|--------|
|
||||
| 左侧框体(配线端子)背景 | `#FFC0CB` (粉色) | `#C8E6C9` (浅绿) |
|
||||
| 左侧行背景 | `#FFB6C1` | `#A5D6A7` |
|
||||
| 右侧框体(局线端子)背景 | `#E0F7FA` (浅蓝) | `#FFE0B2` (浅橙) |
|
||||
| 右侧行背景 | `#B3E5FC` | `#FFCC80` |
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性是在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### 属性 1:标石记录列表渲染包含必要字段
|
||||
|
||||
*对于任意*标石/杆号牌记录,当在列表页(包括标石列表页和搜索结果页)渲染时,渲染输出应包含名称、时间、责任人、所属光缆名称。
|
||||
|
||||
**验证需求: 2.1, 5.3**
|
||||
|
||||
### 属性 2:标石详情页展示完整信息
|
||||
|
||||
*对于任意*标石/杆号牌记录,当在详情页渲染时,渲染输出应包含照片、名称、时间、责任人、经纬度、所属公司、实际里程。
|
||||
|
||||
**验证需求: 3.1**
|
||||
|
||||
### 属性 3:标石 CRUD round-trip
|
||||
|
||||
*对于任意*有效的标石/杆号牌数据,创建后通过 API 查询应返回与提交数据一致的记录;编辑后查询应返回修改后的值。
|
||||
|
||||
**验证需求: 4.8, 6.4**
|
||||
|
||||
### 属性 4:标石删除后不可查
|
||||
|
||||
*对于任意*已存在的标石/杆号牌记录,执行删除操作后,通过 API 查询该记录应返回空或不存在。
|
||||
|
||||
**验证需求: 6.5**
|
||||
|
||||
### 属性 5:新增标石自动填充所属公司
|
||||
|
||||
*对于任意*用户,新增标石/杆号牌时,后端应自动将该记录的 DeptId 和 DeptName 设置为当前用户的所属公司信息。
|
||||
|
||||
**验证需求: 4.4**
|
||||
|
||||
### 属性 6:搜索结果包含匹配的标石记录
|
||||
|
||||
*对于任意*关键词和标石/杆号牌数据集,当标石名称包含该关键词时,搜索 API 返回的 markerPoles 列表应包含该标石记录。
|
||||
|
||||
**验证需求: 5.1**
|
||||
|
||||
### 属性 7:管理后台标石列表展示完整字段
|
||||
|
||||
*对于任意*标石/杆号牌记录,管理后台列表 API 返回的数据应包含名称、时间、责任人、经纬度、所属公司、所属光缆、实际里程。
|
||||
|
||||
**验证需求: 6.2**
|
||||
|
||||
### 属性 8:业务操作产生完整审计日志
|
||||
|
||||
*对于任意*对 ODF/干线/标石相关数据的增删改操作,审计日志表中应存在对应记录,且该记录包含操作人、操作时间、操作类型、操作来源端;当操作类型为修改时,应同时包含修改前数据(OldData)和修改后数据(NewData)。
|
||||
|
||||
**验证需求: 8.2, 8.3, 8.4**
|
||||
|
||||
### 属性 9:审计日志筛选正确性
|
||||
|
||||
*对于任意*时间范围和操作类型筛选条件,审计日志查询 API 返回的所有记录应满足:操作时间在指定范围内,且操作类型匹配筛选条件。
|
||||
|
||||
**验证需求: 8.5, 8.6**
|
||||
|
||||
### 属性 10:数据隔离 — 查询结果仅包含可见部门数据
|
||||
|
||||
*对于任意*用户和任意数据查询(光缆、故障、标石),返回的所有记录的 DeptId 应属于该用户可见的部门 ID 集合(本级 + 所有下级部门)。
|
||||
|
||||
**验证需求: 9.1, 9.2, 9.3, 9.4**
|
||||
|
||||
### 属性 11:机房操作权限与用户角色匹配
|
||||
|
||||
*对于任意*用户和机房操作请求,当用户权限为「仅查看」时,修改操作应被拒绝;当用户权限为「修改和查看」时,查看和修改操作应被允许。
|
||||
|
||||
**验证需求: 10.1, 10.2**
|
||||
|
||||
### 属性 12:分公司管理员创建账号范围限制
|
||||
|
||||
*对于任意*分公司管理员创建用户账号操作,新账号的 DeptId 必须在该管理员可见的部门范围内(本公司及下级公司),否则操作应被拒绝。
|
||||
|
||||
**验证需求: 11.3, 11.5**
|
||||
|
||||
### 属性 13:非分公司管理员禁止登录管理后台
|
||||
|
||||
*对于任意*分公司级别的账号,如果该账号不具有「分公司管理员」角色,则登录管理后台的请求应被拒绝。
|
||||
|
||||
**验证需求: 11.4**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 前端错误处理(UniApp)
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| 未拍照即提交 | `uni.showToast` 提示「请至少拍摄一张照片」,阻止提交 |
|
||||
| 名称为空即提交 | `uni.showToast` 提示「请输入名称」,阻止提交 |
|
||||
| 网络请求失败 | `uni.showToast` 提示错误信息,保留用户已填写数据 |
|
||||
| GPS 定位失败 | 提示用户检查定位权限,不阻止表单提交(经纬度可为 0) |
|
||||
| COS 图片上传失败 | 提示上传失败,允许重试 |
|
||||
| 导航经纬度为空 | 隐藏导航按钮,不展示导航入口 |
|
||||
|
||||
### 后端错误处理(.NET)
|
||||
|
||||
| 场景 | HTTP 状态码 | 处理方式 |
|
||||
|------|------------|---------|
|
||||
| CableId 不存在 | 400 | 抛出 `CustomException("光缆不存在")` |
|
||||
| 标石记录不存在 | 400 | 抛出 `CustomException("标石记录不存在")` |
|
||||
| 无图片 URL | 400 | 抛出 `CustomException("请至少上传一张图片")` |
|
||||
| 无权限访问 | 403 | 框架 `ActionPermissionFilter` 自动拦截 |
|
||||
| 数据隔离越权 | 403 | Service 层校验 DeptId 范围,不在范围内返回空数据 |
|
||||
| 分公司管理员越权创建账号 | 400 | 抛出 `CustomException("无权为该公司创建账号")` |
|
||||
| 审计日志写入失败 | - | 记录错误日志,不影响主业务流程(异步/try-catch) |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 双重测试方法
|
||||
|
||||
本项目采用单元测试 + 属性测试的双重测试策略:
|
||||
|
||||
- **单元测试**:验证具体示例、边界条件和错误处理
|
||||
- **属性测试**:验证跨所有输入的通用属性
|
||||
|
||||
### 属性测试配置
|
||||
|
||||
- **测试库**:后端使用 [FsCheck](https://fscheck.github.io/FsCheck/) (.NET 属性测试库);前端使用 [fast-check](https://github.com/dubzzz/fast-check) (JavaScript 属性测试库)
|
||||
- **每个属性测试最少运行 100 次迭代**
|
||||
- **每个属性测试必须通过注释引用设计文档中的属性编号**
|
||||
- **标签格式**:`Feature: odf-v120-marker-pole, Property {number}: {property_text}`
|
||||
- **每个正确性属性由单个属性测试实现**
|
||||
|
||||
### 单元测试覆盖
|
||||
|
||||
| 测试类别 | 覆盖内容 |
|
||||
|---------|---------|
|
||||
| API 示例测试 | 标石 CRUD 接口的正常流程验证 |
|
||||
| 边界条件 | 空列表状态、经纬度为零、未拍照提交、上传失败 |
|
||||
| 权限测试 | 仅查看用户无法修改、非管理员无法登录后台 |
|
||||
| 数据隔离 | 跨公司数据不可见 |
|
||||
| 搜索测试 | 关键词匹配、无结果场景 |
|
||||
| UI 样式 | 光交箱左绿右橙配色验证 |
|
||||
|
||||
### 属性测试覆盖
|
||||
|
||||
| 属性编号 | 测试内容 | 测试库 |
|
||||
|---------|---------|--------|
|
||||
| 属性 1 | 标石列表渲染字段完整性 | fast-check |
|
||||
| 属性 2 | 标石详情渲染字段完整性 | fast-check |
|
||||
| 属性 3 | 标石 CRUD round-trip | FsCheck |
|
||||
| 属性 4 | 标石删除后不可查 | FsCheck |
|
||||
| 属性 5 | 新增标石自动填充公司 | FsCheck |
|
||||
| 属性 6 | 搜索结果包含匹配标石 | FsCheck |
|
||||
| 属性 7 | 管理后台列表字段完整性 | FsCheck |
|
||||
| 属性 8 | 审计日志完整性 | FsCheck |
|
||||
| 属性 9 | 审计日志筛选正确性 | FsCheck |
|
||||
| 属性 10 | 数据隔离正确性 | FsCheck |
|
||||
| 属性 11 | 机房权限匹配 | FsCheck |
|
||||
| 属性 12 | 创建账号范围限制 | FsCheck |
|
||||
| 属性 13 | 非管理员登录拒绝 | FsCheck |
|
||||
174
.kiro/specs/odf-v120-marker-pole/requirements.md
Normal file
174
.kiro/specs/odf-v120-marker-pole/requirements.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# 需求文档 — ODF v1.2.0 标石杆号牌及管理后台优化
|
||||
|
||||
## 简介
|
||||
|
||||
ODF(光缆资源管理)v1.2.0 版本在现有 odf-uniapp(UniApp/Vue)前端和 .NET 后端基础上进行功能扩展。本次更新包含三大模块:新增标石/杆号牌完整 CRUD 功能(含拍照水印、GPS 定位、导航)、光交箱机房详情页 UI 优化、管理后台修改统计与组织架构权限优化。H5 与 APP 同步更新。
|
||||
|
||||
## 需求来源
|
||||
|
||||
#[[file:docs/1.2.0/1.2.0.md]]
|
||||
|
||||
## 术语表
|
||||
|
||||
- **App**:odf-uniapp 前端应用,基于 UniApp + Vue 构建,运行于安卓和 H5 平台
|
||||
- **管理后台**:基于 ZR.Vue(Vue.js)构建的 Web 管理系统,用于数据配置和管理
|
||||
- **后端**:基于 .NET 的 ZR.Admin.WebApi 服务端,提供 RESTful API
|
||||
- **光缆列表页**:展示某公司下所有光缆的页面(现有 pages/cable/index)
|
||||
- **光缆类型页**:点击光缆后进入的中间页面,展示【标石、杆号牌】和【故障列表】两个入口按钮
|
||||
- **标石杆号牌列表页**:展示某光缆下所有标石/杆号牌记录的页面
|
||||
- **标石杆号牌详情页**:展示单条标石/杆号牌完整信息的页面
|
||||
- **新增标石杆号牌页**:用于创建新标石/杆号牌记录的表单页面
|
||||
- **水印照片**:拍摄后在照片上叠加坐标、时间、责任人、所属光缆等信息文字的照片
|
||||
- **导航点**:标石/杆号牌的 GPS 经纬度坐标,用于地图导航
|
||||
- **实际里程**:标石/杆号牌在光缆上的实际里程数值
|
||||
- **光交箱详情页**:光交箱类型机架的详情页面(现有 pages/optical-box-detail/index)
|
||||
- **修改统计**:管理后台记录所有增删改操作的审计功能
|
||||
- **组织架构权限**:基于公司层级的数据隔离和访问控制机制
|
||||
- **分公司管理员**:分公司级别的管理角色,可管理本公司及下属公司人员账号
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:光缆类型页入口
|
||||
|
||||
**用户故事:** 作为用户,我希望点击光缆后看到功能分类入口,以便分别进入标石杆号牌管理或故障管理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户在光缆列表页点击任意光缆,THE App SHALL 跳转至光缆类型页
|
||||
2. THE 光缆类型页 SHALL 展示【标石、杆号牌】和【故障列表】两个入口按钮
|
||||
3. WHEN 用户点击【标石、杆号牌】按钮,THE App SHALL 跳转至该光缆的标石杆号牌列表页
|
||||
4. WHEN 用户点击【故障列表】按钮,THE App SHALL 跳转至该光缆的故障列表页(现有页面)
|
||||
5. THE 光缆类型页 SHALL 在导航栏显示当前光缆名称
|
||||
|
||||
### 需求 2:标石杆号牌列表页
|
||||
|
||||
**用户故事:** 作为用户,我希望查看某光缆下的所有标石/杆号牌记录,以便了解标石分布情况并新增记录。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 标石杆号牌列表页 SHALL 展示标石/杆号牌记录列表,每条记录显示名称、时间、责任人、导航点、实际里程
|
||||
2. WHEN 用户点击任意标石/杆号牌记录,THE App SHALL 跳转至该记录的标石杆号牌详情页
|
||||
3. THE 标石杆号牌列表页 SHALL 提供【新增】按钮
|
||||
4. WHEN 用户点击【新增】按钮,THE App SHALL 跳转至新增标石杆号牌页
|
||||
5. WHILE 该光缆下无标石/杆号牌记录,THE 标石杆号牌列表页 SHALL 展示空状态提示文字
|
||||
|
||||
### 需求 3:标石杆号牌详情页
|
||||
|
||||
**用户故事:** 作为用户,我希望查看标石/杆号牌的完整信息并导航至该位置,以便进行现场巡检。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 标石杆号牌详情页 SHALL 展示以下信息:照片、名称、时间、责任人、导航点(经纬度)、所属公司、实际里程
|
||||
2. WHEN 用户点击照片,THE App SHALL 以全屏模式放大展示该照片
|
||||
3. THE 标石杆号牌详情页 SHALL 提供【导航】按钮
|
||||
4. WHEN 用户点击【导航】按钮,THE App SHALL 根据导航点经纬度拉起导航应用
|
||||
5. IF 导航点经纬度为空或为零,THEN THE 标石杆号牌详情页 SHALL 隐藏【导航】按钮
|
||||
|
||||
### 需求 4:新增标石杆号牌页
|
||||
|
||||
**用户故事:** 作为用户,我希望在现场拍照记录标石/杆号牌信息并提交,以便及时上报标石数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 新增标石杆号牌页 SHALL 提供拍照功能,仅支持相机拍摄(不支持从相册选择)
|
||||
2. WHEN 用户拍摄照片,THE App SHALL 在照片上叠加水印信息,水印内容包含:当前坐标、拍摄时间、责任人、所属光缆名称
|
||||
3. WHEN 用户拍摄照片完成,THE App SHALL 自动获取拍摄时间并填充至时间字段
|
||||
4. THE 新增标石杆号牌页 SHALL 自动填充所属公司(取当前用户所属公司)
|
||||
5. THE 新增标石杆号牌页 SHALL 提供以下手动输入字段:名称、责任人、实际里程
|
||||
6. THE 新增标石杆号牌页 SHALL 提供【获取经纬度】按钮
|
||||
7. WHEN 用户点击【获取经纬度】按钮,THE App SHALL 调用设备定位功能获取当前经纬度,并在页面上显示经纬度数值
|
||||
8. WHEN 用户点击【提交】按钮,THE App SHALL 将所有数据(照片、名称、时间、责任人、经纬度、所属公司、所属光缆、实际里程)上传至后端
|
||||
9. WHEN 数据上传成功,THE App SHALL 弹出系统提示「提交成功」并自动返回标石杆号牌列表页
|
||||
10. IF 数据上传失败,THEN THE App SHALL 弹出错误提示信息,保留用户已填写的数据
|
||||
11. IF 用户未拍摄照片即点击提交,THEN THE App SHALL 提示用户至少拍摄一张照片
|
||||
|
||||
### 需求 5:搜索结果页支持标石杆号牌类型
|
||||
|
||||
**用户故事:** 作为用户,我希望搜索结果中能展示标石/杆号牌类型,以便在搜索时快速定位标石信息。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户在光缆列表页搜索关键词,THE App SHALL 在搜索范围中包含标石/杆号牌记录
|
||||
2. THE 搜索结果页 SHALL 新增「标石、杆号牌」分类区域,展示匹配的标石/杆号牌记录
|
||||
3. WHEN 搜索结果中包含匹配的标石/杆号牌记录,THE 搜索结果页 SHALL 在「标石、杆号牌」分类下展示名称、时间、责任人、所属光缆
|
||||
4. WHEN 用户点击搜索结果中的标石/杆号牌项,THE App SHALL 跳转至该记录的标石杆号牌详情页
|
||||
|
||||
### 需求 6:管理后台标石杆号牌增删改
|
||||
|
||||
**用户故事:** 作为管理员,我希望在管理后台对标石/杆号牌数据进行增删改操作,以便维护标石数据的准确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 管理后台 SHALL 提供标石/杆号牌管理页面,展示标石/杆号牌数据列表
|
||||
2. THE 管理后台标石杆号牌列表 SHALL 展示以下字段:名称、时间、责任人、经纬度、所属公司、所属光缆、实际里程
|
||||
3. THE 管理后台 SHALL 提供新增标石/杆号牌功能,支持填写名称、责任人、经纬度、所属光缆、实际里程
|
||||
4. THE 管理后台 SHALL 提供编辑标石/杆号牌功能,支持修改已有记录的所有字段
|
||||
5. THE 管理后台 SHALL 提供删除标石/杆号牌功能,支持删除单条记录
|
||||
6. WHEN 管理员执行删除操作,THE 管理后台 SHALL 弹出确认对话框,确认后执行删除
|
||||
|
||||
### 需求 7:光交箱机房详情页 UI 优化
|
||||
|
||||
**用户故事:** 作为用户,我希望光交箱详情页的左右两侧用不同颜色区分,以便快速识别光交箱端子和 ODF 端子。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 光交箱详情页 SHALL 将左侧框体背景色设置为绿色
|
||||
2. THE 光交箱详情页 SHALL 将右侧框体背景色设置为橙色
|
||||
3. THE 光交箱详情页 SHALL 仅对光交箱类型机架应用此配色方案,ODF 类型机架保持现有样式不变
|
||||
|
||||
### 需求 8:管理后台修改统计功能
|
||||
|
||||
**用户故事:** 作为管理员,我希望查看所有数据修改记录,以便追溯数据变更历史和责任人。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 管理后台 SHALL 提供修改统计页面,展示所有增删改操作记录
|
||||
2. WHEN 前端 App 或管理后台对 ODF、干线相关内容执行新增、修改、删除操作,THE 后端 SHALL 记录该操作的审计日志
|
||||
3. THE 修改统计记录 SHALL 包含以下信息:修改人、修改时间、具体修改内容、操作来源端(App 端或管理后台端)
|
||||
4. WHEN 操作类型为修改,THE 修改统计记录 SHALL 保存修改前和修改后的数据,支持对比查看
|
||||
5. THE 管理后台修改统计页面 SHALL 支持按时间范围筛选查看记录
|
||||
6. THE 管理后台修改统计页面 SHALL 支持按操作类型(新增、修改、删除)筛选查看记录
|
||||
|
||||
### 需求 9:组织架构权限 — 公司数据隔离
|
||||
|
||||
**用户故事:** 作为管理员,我希望每个账号只能看到所属公司的数据,以便实现数据安全隔离。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 后端 SHALL 根据当前登录账号的所属公司过滤返回数据,每个账号仅能查看所属公司的信息
|
||||
2. WHILE 当前账号为上级公司账号,THE 后端 SHALL 允许该账号查看本公司及所有下级公司的信息
|
||||
3. THE 后端 SHALL 对干线故障数据执行公司隔离,管理后台仅展示当前账号有权限查看的公司故障数据
|
||||
4. THE 后端 SHALL 对标石/杆号牌数据执行公司隔离,遵循与干线故障相同的公司层级权限规则
|
||||
|
||||
### 需求 10:组织架构权限 — 机房权限修复
|
||||
|
||||
**用户故事:** 作为管理员,我希望机房内容的查看/修改权限与干线权限保持一致,以便权限控制准确生效。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHILE 当前账号权限为「仅查看」,THE App SHALL 禁止该账号对机房内容执行任何修改操作
|
||||
2. WHILE 当前账号权限为「修改和查看」,THE App SHALL 允许该账号对机房内容执行查看和修改操作
|
||||
3. THE App 机房权限控制 SHALL 与干线版块的「仅查看」「修改和查看」权限逻辑保持一致
|
||||
|
||||
### 需求 11:组织架构权限 — 分公司管理员角色
|
||||
|
||||
**用户故事:** 作为分公司管理员,我希望管理本公司及下属公司的人员账号,以便实现分级管理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 管理后台 SHALL 新增「分公司管理员」角色
|
||||
2. THE 分公司管理员 SHALL 能查看本公司及下属公司的所有信息
|
||||
3. THE 分公司管理员 SHALL 能新增所属分公司和下级公司的人员账号
|
||||
4. WHILE 分公司账号角色不是「分公司管理员」,THE 管理后台 SHALL 禁止该账号登录管理后台
|
||||
5. THE 后端 SHALL 在分公司管理员创建账号时,限制新账号的所属公司范围为本公司及下级公司
|
||||
|
||||
### 需求 12:平台兼容性
|
||||
|
||||
**用户故事:** 作为用户,我希望在安卓和 H5 平台上使用相同的标石杆号牌功能,以便在不同设备上获得一致的体验。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE App SHALL 在安卓平台和 H5 平台同步提供标石杆号牌的所有功能
|
||||
2. WHEN App 在新增标石杆号牌页使用拍照功能,THE App SHALL 在安卓平台调用原生相机 API,在 H5 平台调用浏览器媒体 API
|
||||
3. WHEN App 在新增标石杆号牌页使用定位功能,THE App SHALL 在安卓平台调用原生定位 API,在 H5 平台调用浏览器 Geolocation API
|
||||
4. WHEN App 在标石杆号牌详情页调用导航功能,THE App SHALL 在安卓平台弹出已安装的导航 APP 列表,在 H5 平台打开地图网页进行导航
|
||||
219
.kiro/specs/odf-v120-marker-pole/tasks.md
Normal file
219
.kiro/specs/odf-v120-marker-pole/tasks.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# 实现计划:ODF v1.2.0 标石杆号牌及管理后台优化
|
||||
|
||||
## 概述
|
||||
|
||||
基于需求文档和设计文档,按照数据库 → 后端模型 → 后端服务/接口 → 前端 UniApp → 管理后台 Vue 的顺序实现。涵盖标石/杆号牌 CRUD、搜索扩展、光交箱 UI 优化、审计日志、组织架构权限五大模块。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 数据库迁移脚本
|
||||
- [x] 1.1 创建标石/杆号牌相关表
|
||||
- 创建 `sql/v1.2.0/` 目录
|
||||
- 编写 `01_create_odf_marker_poles.sql`:创建 `odf_marker_poles` 表及索引(CableId, DeptId)
|
||||
- 编写 `02_create_odf_marker_pole_images.sql`:创建 `odf_marker_pole_images` 表及索引(MarkerPoleId)
|
||||
- _需求: 2.1, 4.8, 6.1_
|
||||
- [x] 1.2 创建审计日志表
|
||||
- 编写 `03_create_odf_audit_logs.sql`:创建 `odf_audit_logs` 表及索引(OperationTime, TableName, OperationType)
|
||||
- _需求: 8.1, 8.2_
|
||||
- [x] 1.3 创建菜单和权限数据脚本
|
||||
- 编写 `04_insert_marker_pole_menus_permissions.sql`:插入标石/杆号牌管理菜单、审计日志菜单及对应按钮权限(odfmarkerpoles:list/query/add/edit/delete, odfauditlogs:list)
|
||||
- 编写 `05_insert_branch_admin_role.sql`:插入「分公司管理员」角色记录到 `sys_role`,并配置角色权限
|
||||
- _需求: 6.1, 8.1, 11.1_
|
||||
|
||||
- [x] 2. 后端模型层(ZR.Model)
|
||||
- [x] 2.1 创建标石/杆号牌实体模型
|
||||
- 创建 `server/ZR.Model/Business/OdfMarkerPoles.cs`:定义 `OdfMarkerPoles` 实体,映射 `odf_marker_poles` 表
|
||||
- 创建 `server/ZR.Model/Business/OdfMarkerPoleImages.cs`:定义 `OdfMarkerPoleImages` 实体,映射 `odf_marker_pole_images` 表
|
||||
- _需求: 2.1, 3.1, 4.8_
|
||||
- [x] 2.2 创建标石/杆号牌 DTO
|
||||
- 创建 `server/ZR.Model/Business/Dto/OdfMarkerPolesDto.cs`:定义 `OdfMarkerPolesQueryDto`、`OdfMarkerPoleAddDto`、`OdfMarkerPoleEditDto`、`OdfMarkerPoleListDto`、`OdfMarkerPoleDetailDto`
|
||||
- _需求: 2.1, 3.1, 4.8, 6.2, 6.3, 6.4_
|
||||
- [x] 2.3 创建审计日志实体和 DTO
|
||||
- 创建 `server/ZR.Model/Business/OdfAuditLogs.cs`:定义 `OdfAuditLogs` 实体,映射 `odf_audit_logs` 表
|
||||
- 创建 `server/ZR.Model/Business/Dto/OdfAuditLogsDto.cs`:定义 `OdfAuditLogsQueryDto`
|
||||
- _需求: 8.2, 8.3_
|
||||
|
||||
- [x] 3. 后端服务层与 API — 标石/杆号牌模块
|
||||
- [x] 3.1 创建数据隔离工具类
|
||||
- 创建 `server/ZR.Service/Business/DeptDataScopeHelper.cs`:实现 `GetVisibleDeptIds` 方法,根据 DeptId 递归查询本级及所有下级部门 ID 列表
|
||||
- _需求: 9.1, 9.2_
|
||||
- [x] 3.2 创建标石/杆号牌服务接口和实现
|
||||
- 创建 `server/ZR.Service/Business/IBusinessService/IOdfMarkerPolesService.cs`:定义 GetList、GetDetail、Add、Update、Delete 接口方法
|
||||
- 创建 `server/ZR.Service/Business/OdfMarkerPolesService.cs`:实现标石/杆号牌 CRUD 逻辑,查询时应用 DeptDataScopeHelper 进行数据隔离,新增时自动填充 DeptId/DeptName
|
||||
- _需求: 2.1, 3.1, 4.4, 4.8, 6.3, 6.4, 6.5, 9.4_
|
||||
- [x] 3.3 创建标石/杆号牌图片服务
|
||||
- 创建 `server/ZR.Service/Business/IBusinessService/IOdfMarkerPoleImagesService.cs`
|
||||
- 创建 `server/ZR.Service/Business/OdfMarkerPoleImagesService.cs`:实现图片批量插入、按 MarkerPoleId 查询、按 MarkerPoleId 删除
|
||||
- _需求: 3.1, 4.1, 4.8_
|
||||
- [x] 3.4 创建标石/杆号牌 Controller
|
||||
- 创建 `server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs`:实现 list/query/add/edit/delete 五个接口,配置 `ActionPermissionFilter` 权限校验
|
||||
- 新增时校验 CableId 存在性和 ImageUrls 非空
|
||||
- _需求: 2.1, 3.1, 4.8, 6.3, 6.4, 6.5_
|
||||
- [ ]* 3.5 编写标石/杆号牌 CRUD 属性测试
|
||||
- **属性 3: 标石 CRUD round-trip**
|
||||
- **属性 4: 标石删除后不可查**
|
||||
- **属性 5: 新增标石自动填充所属公司**
|
||||
- **验证需求: 4.4, 4.8, 6.4, 6.5**
|
||||
|
||||
- [x] 4. 后端服务层与 API — 搜索扩展
|
||||
- [x] 4.1 扩展搜索接口支持标石/杆号牌
|
||||
- 修改 `server/ZR.Service/Business/OdfCablesService.cs` 的 `Search` 方法:在搜索结果中新增 `markerPoles` 字段,按关键词匹配标石名称
|
||||
- 修改 `server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs` 对应接口返回结构
|
||||
- _需求: 5.1, 5.2_
|
||||
- [ ]* 4.2 编写搜索属性测试
|
||||
- **属性 6: 搜索结果包含匹配的标石记录**
|
||||
- **验证需求: 5.1**
|
||||
|
||||
- [x] 5. 后端服务层与 API — 审计日志模块
|
||||
- [x] 5.1 创建审计日志服务
|
||||
- 创建 `server/ZR.Service/Business/IBusinessService/IOdfAuditLogsService.cs`
|
||||
- 创建 `server/ZR.Service/Business/OdfAuditLogsService.cs`:实现分页查询(支持时间范围、操作类型筛选)和写入方法
|
||||
- _需求: 8.1, 8.5, 8.6_
|
||||
- [x] 5.2 创建审计日志 ActionFilter
|
||||
- 创建 `server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs`:实现 `IAsyncActionFilter`,拦截标石/ODF/干线相关 Controller 的增删改操作
|
||||
- 在 `OnActionExecutionAsync` 中记录操作前数据(OldData),在操作后记录新数据(NewData)
|
||||
- 通过 `SourceClient` 区分 App 端和管理后台端(从请求 Header 或路由判断)
|
||||
- 审计日志写入失败时 try-catch 不影响主业务
|
||||
- _需求: 8.2, 8.3, 8.4_
|
||||
- [x] 5.3 创建审计日志 Controller
|
||||
- 创建 `server/ZR.Admin.WebApi/Controllers/Business/OdfAuditLogsController.cs`:实现 list 查询接口,配置 `odfauditlogs:list` 权限
|
||||
- _需求: 8.1, 8.5_
|
||||
- [x] 5.4 在现有 Controller 上应用审计日志 Filter
|
||||
- 在 `OdfMarkerPolesController`、`OdfCablesController`、`OdfCableFaultsController`、`OdfPortsController`、`OdfRacksController` 的增删改 Action 上添加 `[ServiceFilter(typeof(OdfAuditLogFilter))]`
|
||||
- _需求: 8.2_
|
||||
- [ ]* 5.5 编写审计日志属性测试
|
||||
- **属性 8: 业务操作产生完整审计日志**
|
||||
- **属性 9: 审计日志筛选正确性**
|
||||
- **验证需求: 8.2, 8.3, 8.4, 8.5, 8.6**
|
||||
|
||||
- [x] 6. 后端 — 数据隔离与权限修复
|
||||
- [x] 6.1 应用数据隔离到现有模块
|
||||
- 修改 `server/ZR.Service/Business/OdfCableFaultsService.cs`:在查询方法中应用 `DeptDataScopeHelper` 过滤 DeptId
|
||||
- 修改 `server/ZR.Service/Business/OdfCablesService.cs`:在查询方法中应用 `DeptDataScopeHelper` 过滤 DeptId
|
||||
- _需求: 9.1, 9.2, 9.3_
|
||||
- [x] 6.2 修复机房权限控制
|
||||
- 修改 `server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs`:在编辑接口上增加 `odfports:edit` 权限校验
|
||||
- _需求: 10.1, 10.2, 10.3_
|
||||
- [x] 6.3 实现分公司管理员登录限制
|
||||
- 修改后端登录逻辑:分公司级别账号登录管理后台时,校验是否拥有「分公司管理员」角色,无该角色则拒绝登录
|
||||
- _需求: 11.4_
|
||||
- [x] 6.4 实现分公司管理员创建账号范围限制
|
||||
- 修改后端用户创建逻辑:分公司管理员创建用户时,校验新账号 DeptId 是否在管理员可见部门范围内
|
||||
- _需求: 11.3, 11.5_
|
||||
- [ ]* 6.5 编写数据隔离与权限属性测试
|
||||
- **属性 10: 数据隔离 — 查询结果仅包含可见部门数据**
|
||||
- **属性 11: 机房操作权限与用户角色匹配**
|
||||
- **属性 12: 分公司管理员创建账号范围限制**
|
||||
- **属性 13: 非分公司管理员禁止登录管理后台**
|
||||
- **验证需求: 9.1, 9.2, 9.3, 9.4, 10.1, 10.2, 11.3, 11.4, 11.5**
|
||||
|
||||
- [x] 7. 检查点 — 后端完成验证
|
||||
- 确保所有后端代码编译通过,所有测试通过,如有问题请向用户确认。
|
||||
|
||||
- [x] 8. UniApp 前端 — 标石/杆号牌服务层
|
||||
- [x] 8.1 创建标石/杆号牌 API 服务
|
||||
- 创建 `odf-uniapp/services/markerPole.js`:封装 getMarkerPoleList、getMarkerPoleDetail、addMarkerPole、searchMarkerPoles 等 API 调用方法
|
||||
- _需求: 2.1, 3.1, 4.8, 5.1_
|
||||
|
||||
- [x] 9. UniApp 前端 — 光缆类型页
|
||||
- [x] 9.1 创建光缆类型页
|
||||
- 创建 `odf-uniapp/pages/cable-type/index.vue`:展示【标石、杆号牌】和【故障列表】两个入口按钮
|
||||
- 导航栏显示当前光缆名称
|
||||
- 点击按钮分别跳转至标石列表页和故障列表页,传递 cableId 和 cableName 参数
|
||||
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5_
|
||||
- [x] 9.2 修改光缆列表页跳转逻辑
|
||||
- 修改 `odf-uniapp/pages/cable/index.vue`:点击光缆时跳转至光缆类型页(而非直接跳转故障列表页)
|
||||
- _需求: 1.1_
|
||||
- [x] 9.3 注册新页面路由
|
||||
- 修改 `odf-uniapp/pages.json`:添加 cable-type、marker-pole-list、marker-pole-detail、marker-pole-add 四个页面路由配置
|
||||
- _需求: 1.1, 2.1, 3.1, 4.1_
|
||||
|
||||
- [x] 10. UniApp 前端 — 标石/杆号牌列表页
|
||||
- [x] 10.1 创建标石/杆号牌列表页
|
||||
- 创建 `odf-uniapp/pages/marker-pole-list/index.vue`:展示标石/杆号牌记录列表,每条显示名称、时间、责任人、导航点、实际里程
|
||||
- 点击记录跳转详情页,点击【新增】按钮跳转新增页
|
||||
- 无数据时展示空状态提示
|
||||
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5_
|
||||
- [ ]* 10.2 编写标石列表渲染属性测试
|
||||
- **属性 1: 标石记录列表渲染包含必要字段**
|
||||
- **验证需求: 2.1, 5.3**
|
||||
|
||||
- [x] 11. UniApp 前端 — 标石/杆号牌详情页
|
||||
- [x] 11.1 创建标石/杆号牌详情页
|
||||
- 创建 `odf-uniapp/pages/marker-pole-detail/index.vue`:展示照片、名称、时间、责任人、导航点(经纬度)、所属公司、实际里程
|
||||
- 照片支持点击全屏预览(uni.previewImage)
|
||||
- 提供【导航】按钮,经纬度为空或为零时隐藏
|
||||
- 点击导航按钮:安卓端弹出导航 APP 列表,H5 端打开地图网页
|
||||
- _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 12.4_
|
||||
- [ ]* 11.2 编写标石详情渲染属性测试
|
||||
- **属性 2: 标石详情页展示完整信息**
|
||||
- **验证需求: 3.1**
|
||||
|
||||
- [x] 12. UniApp 前端 — 新增标石/杆号牌页
|
||||
- [x] 12.1 创建新增标石/杆号牌页
|
||||
- 创建 `odf-uniapp/pages/marker-pole-add/index.vue`
|
||||
- 拍照功能:调用 uni.chooseImage(sourceType: ['camera']),拍摄后叠加水印(坐标、时间、责任人、所属光缆)
|
||||
- 自动填充拍摄时间和所属公司
|
||||
- 手动输入字段:名称、责任人、实际里程
|
||||
- 【获取经纬度】按钮:调用 uni.getLocation 获取 GPS 坐标
|
||||
- 提交前校验:名称非空、至少一张照片
|
||||
- 照片上传至腾讯云 COS,获取 URL 后提交表单
|
||||
- 提交成功后提示并返回列表页
|
||||
- _需求: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 12.1, 12.2, 12.3_
|
||||
|
||||
- [x] 13. UniApp 前端 — 搜索扩展
|
||||
- [x] 13.1 扩展搜索结果页支持标石/杆号牌
|
||||
- 修改 `odf-uniapp/pages/trunk-search/index.vue`(或对应搜索结果页):新增「标石、杆号牌」分类区域
|
||||
- 展示匹配的标石记录(名称、时间、责任人、所属光缆)
|
||||
- 点击标石记录跳转至详情页
|
||||
- _需求: 5.1, 5.2, 5.3, 5.4_
|
||||
- [x] 13.2 修改搜索服务调用
|
||||
- 修改 `odf-uniapp/services/search.js`:适配搜索 API 返回的 markerPoles 新字段
|
||||
- _需求: 5.1_
|
||||
|
||||
- [x] 14. UniApp 前端 — 光交箱 UI 优化与机房权限修复
|
||||
- [x] 14.1 修改光交箱详情页配色
|
||||
- 修改 `odf-uniapp/pages/optical-box-detail/index.vue`:左侧框体背景改为浅绿(#C8E6C9 / #A5D6A7),右侧框体背景改为浅橙(#FFE0B2 / #FFCC80)
|
||||
- 仅光交箱类型应用新配色,ODF 类型保持不变
|
||||
- _需求: 7.1, 7.2, 7.3_
|
||||
- [x] 14.2 修复机房端口编辑权限控制
|
||||
- 修改 `odf-uniapp/components/port-edit-dialog.vue`(或对应端口编辑组件):根据 store 中用户权限控制编辑按钮显示/隐藏
|
||||
- 「仅查看」权限用户隐藏编辑入口,「修改和查看」权限用户显示编辑入口
|
||||
- _需求: 10.1, 10.2, 10.3_
|
||||
|
||||
- [x] 15. 检查点 — UniApp 前端完成验证
|
||||
- 确保所有 UniApp 页面编译通过,页面导航流程正确,如有问题请向用户确认。
|
||||
|
||||
- [x] 16. 管理后台 Vue — 标石/杆号牌管理页
|
||||
- [x] 16.1 创建标石/杆号牌 API 封装
|
||||
- 创建 `server/ZR.Vue/src/api/business/odfmarkerpoles.js`:封装 list、get、add、update、del 接口调用
|
||||
- _需求: 6.1_
|
||||
- [x] 16.2 创建标石/杆号牌管理页面
|
||||
- 创建 `server/ZR.Vue/src/views/business/OdfMarkerPoles.vue`:实现数据列表(名称、时间、责任人、经纬度、所属公司、所属光缆、实际里程)、新增对话框、编辑对话框、删除确认
|
||||
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6_
|
||||
- [ ]* 16.3 编写管理后台标石列表属性测试
|
||||
- **属性 7: 管理后台标石列表展示完整字段**
|
||||
- **验证需求: 6.2**
|
||||
|
||||
- [x] 17. 管理后台 Vue — 审计日志页
|
||||
- [x] 17.1 创建审计日志 API 封装
|
||||
- 创建 `server/ZR.Vue/src/api/business/odfauditlogs.js`:封装 list 查询接口
|
||||
- _需求: 8.1_
|
||||
- [x] 17.2 创建审计日志管理页面
|
||||
- 创建 `server/ZR.Vue/src/views/business/OdfAuditLogs.vue`:实现审计日志列表(操作人、时间、操作类型、操作来源、表名)、时间范围筛选、操作类型筛选、修改前后数据对比展示
|
||||
- _需求: 8.1, 8.3, 8.4, 8.5, 8.6_
|
||||
|
||||
- [x] 18. 管理后台 Vue — 分公司管理员登录限制
|
||||
- [x] 18.1 管理后台登录页增加分公司管理员校验
|
||||
- 修改管理后台登录流程:登录成功后检查返回的角色信息,分公司账号若无「分公司管理员」角色则提示无权登录并阻止进入
|
||||
- _需求: 11.4_
|
||||
|
||||
- [x] 19. 最终检查点 — 全部完成验证
|
||||
- 确保所有后端编译通过、前端编译通过、管理后台编译通过,所有测试通过,如有问题请向用户确认。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选测试任务,可跳过以加快 MVP 进度
|
||||
- 每个任务引用了具体的需求编号,确保需求全覆盖
|
||||
- 检查点任务用于阶段性验证,确保增量开发的正确性
|
||||
- 属性测试验证设计文档中定义的正确性属性
|
||||
94
docs/1.2.0/1.2.0.md
Normal file
94
docs/1.2.0/1.2.0.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# odf(光缆资源管理)v1.2.0需求文档
|
||||
|
||||
# 需求大纲
|
||||
|
||||
|
||||
1. H5、APP同步更新。
|
||||
2. 新增标石、杆号牌相关功能。
|
||||
3. 优化后台相关功能。
|
||||
|
||||
# 标石、杆号牌入口
|
||||
|
||||

|
||||
|
||||
|
||||
1. 干线,光缆列表,点击任意光缆,跳转进"光缆类型页"。
|
||||
2. 展示【标石、杆号牌】【故障列表】入口按钮。
|
||||
3. 点击【标石、杆号牌】按钮,跳转至"标石、杆号牌列表页"
|
||||
4. 点击【故障列表】按钮,跳转至原故障列表页
|
||||
|
||||
# 标石、杆号牌列表页
|
||||
|
||||

|
||||
|
||||
|
||||
1. 展示名称、时间、责任人、导航点、实际里程。
|
||||
2. 点击任一区域,跳转至对应"标石、杆号牌详情页"。
|
||||
3. 点击【新增】按钮,跳转至"新增标石、杆号牌页"。
|
||||
|
||||
# 标石、杆号牌详情页
|
||||
|
||||

|
||||
|
||||
|
||||
1. 展示照片、名称、时间、责任人、导航点、所属公司、实际里程。
|
||||
2. 点击【导航】按钮,根据导航点经纬度,拉起导航。
|
||||
|
||||
# 新增标石、杆号牌页
|
||||
|
||||

|
||||
|
||||
|
||||
1. 点击【拍照】,呼出相机。
|
||||
2. 照片增加水印信息:坐标、时间、责任人、所属光缆。
|
||||
3. 输入"标石、杆号牌"名称、责任人、实际里程。
|
||||
4. 时间,拍照上传后自动填写。
|
||||
5. 所属公司,自动填写。
|
||||
6. 点击【获取经纬度】按钮,获取当前经纬度。
|
||||
7. 点击【提交】按钮,提交信息,自动返回上一级页面。
|
||||
8. 管理后台支持增删改。
|
||||
|
||||
# 搜索结果页
|
||||
|
||||

|
||||
|
||||
|
||||
1. 搜索结果页,可展示"标石、杆号牌"搜索结果类型。
|
||||
|
||||
# 光交箱机房详情页
|
||||
|
||||
|
||||
1. 修改光交箱详情页,左右两框的颜色。
|
||||
2. 左边为绿色,右边为橙色,参考如下图。
|
||||
|
||||

|
||||
|
||||
# 管理后台优化
|
||||
|
||||
## 修改统计
|
||||
|
||||
|
||||
1. 后台新增修改统计功能。
|
||||
2. 前端或管理后台新增、修改、删除ODF、干线内容时,可在管理后台查看记录。
|
||||
|
||||
|
||||
1. 可查看修改前后的对比数据。
|
||||
2. 记录包含修改人、时间、具体修改内容、从哪个端修改。
|
||||
3. 可根据时间查看。
|
||||
|
||||
## 组织架构权限
|
||||
|
||||
|
||||
1. 每个账号,只能看到自己所属公司的信息。
|
||||
2. 上级公司账号,可查看本公司、所属下级公司的信息。
|
||||
|
||||
|
||||
1. 干线故障,现在后台能看见所有的信息,没有进行分公司隔离,需进行公司隔离。
|
||||
3. 前端查看"机房"内容时,该账号的权限为"仅查看""修改和查看"时,权限不正确,应同步修改干线时的"仅查看""修改和查看"账号权限。
|
||||
|
||||
|
||||
1. 现在"仅查看"权限,能对"机房"内容修改,应只能查看。
|
||||
4. 分公司新增"分公司管理员"权限,能查看本公司及下属公司信息、能新增所属分公司、下级公司人员账号。
|
||||
|
||||
|
||||
1. 分公司中,只有分公司管理员账号允许登录管理后台。
|
||||
BIN
docs/1.2.0/attachments/290c3dda-14b9-4a8c-8233-0994b3488cfc.png
Normal file
BIN
docs/1.2.0/attachments/290c3dda-14b9-4a8c-8233-0994b3488cfc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/1.2.0/attachments/55220979-83e5-4ff3-949a-6cbf5e9053f5.png
Normal file
BIN
docs/1.2.0/attachments/55220979-83e5-4ff3-949a-6cbf5e9053f5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/1.2.0/attachments/895d5b87-4235-4a0d-a6a4-a0ce8cc82d18.png
Normal file
BIN
docs/1.2.0/attachments/895d5b87-4235-4a0d-a6a4-a0ce8cc82d18.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
docs/1.2.0/attachments/9c560e3d-6da7-4811-85ec-ffb19b639663.png
Normal file
BIN
docs/1.2.0/attachments/9c560e3d-6da7-4811-85ec-ffb19b639663.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/1.2.0/attachments/efa7641d-c66b-4702-b1b7-5f5d1cb78f4b.jpg
Normal file
BIN
docs/1.2.0/attachments/efa7641d-c66b-4702-b1b7-5f5d1cb78f4b.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
BIN
docs/1.2.0/attachments/f13df494-322c-4c04-9ee9-693fa4cba2cf.png
Normal file
BIN
docs/1.2.0/attachments/f13df494-322c-4c04-9ee9-693fa4cba2cf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
|
|
@ -24,9 +24,9 @@
|
|||
<view class="remarks-row">
|
||||
<view class="textarea-wrap">
|
||||
<textarea class="remarks-input" v-model="form.remarks" :maxlength="-1" placeholder="请输入备注说明"
|
||||
:disabled="!store.isPermission" auto-height />
|
||||
:disabled="!canEdit" auto-height />
|
||||
</view>
|
||||
<view v-if="store.isPermission" class="add-note-btn" @click="showAddNote = true">
|
||||
<view v-if="canEdit" class="add-note-btn" @click="showAddNote = true">
|
||||
<text class="add-note-text">添加备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<view class="section">
|
||||
<text class="section-title">光衰信息</text>
|
||||
<input class="form-input" v-model="form.opticalAttenuation" placeholder="请输入光衰信息"
|
||||
:disabled="!store.isPermission" />
|
||||
:disabled="!canEdit" />
|
||||
</view>
|
||||
|
||||
<!-- 历史障碍记录 -->
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<view class="fault-item" v-for="(item, index) in form.historyFault" :key="index">
|
||||
<view class="fault-row">
|
||||
<picker mode="date" :value="item.faultTime ? item.faultTime.substring(0, 10) : ''"
|
||||
:disabled="!store.isPermission" @change="onFaultDateChange($event, index)">
|
||||
:disabled="!canEdit" @change="onFaultDateChange($event, index)">
|
||||
<view class="date-picker">
|
||||
<text :class="item.faultTime ? 'date-text' : 'date-placeholder'">
|
||||
{{ item.faultTime || '选择日期' }}
|
||||
|
|
@ -54,14 +54,14 @@
|
|||
</view>
|
||||
</picker>
|
||||
<input class="fault-reason-input" v-model="item.faultReason" placeholder="故障原因"
|
||||
:disabled="!store.isPermission" />
|
||||
<view v-if="store.isPermission" class="delete-btn" @click="removeFault(index)">
|
||||
:disabled="!canEdit" />
|
||||
<view v-if="canEdit" class="delete-btn" @click="removeFault(index)">
|
||||
<text class="delete-btn-text">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="store.isPermission" class="add-record-link" @click="addFault">
|
||||
<view v-if="canEdit" class="add-record-link" @click="addFault">
|
||||
<text class="add-record-text">添加新记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -70,11 +70,11 @@
|
|||
<view class="section">
|
||||
<text class="section-title">光缆段信息</text>
|
||||
<input class="form-input" v-model="form.opticalCableOffRemarks" placeholder="请输入光缆段信息"
|
||||
:disabled="!store.isPermission" />
|
||||
:disabled="!canEdit" />
|
||||
</view>
|
||||
|
||||
<!-- 权限控制区域 -->
|
||||
<view v-if="store.isPermission" class="section">
|
||||
<view v-if="canEdit" class="section">
|
||||
<text class="section-title">改变状态</text>
|
||||
<view class="status-toggle-row">
|
||||
<view class="toggle-btn toggle-green" :class="{ 'toggle-active': form.status === 1 }"
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
<!-- 底部按钮(固定在弹窗底部) -->
|
||||
<view v-if="!loading" class="btn-row">
|
||||
<template v-if="store.isPermission">
|
||||
<template v-if="canEdit">
|
||||
<view class="btn btn-cancel" @click="onClose">
|
||||
<text class="btn-text">取消</text>
|
||||
</view>
|
||||
|
|
@ -117,6 +117,7 @@
|
|||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch
|
||||
} from 'vue'
|
||||
import {
|
||||
|
|
@ -126,6 +127,8 @@
|
|||
import store from '@/store'
|
||||
import addNoteDialog from '@/components/add-note-dialog.vue'
|
||||
|
||||
const canEdit = computed(() => store.hasPermi('odfports:edit'))
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"name" : "绥时录",
|
||||
"appid" : "__UNI__45FFD83",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.4",
|
||||
"versionCode" : 104,
|
||||
"versionName" : "1.0.5",
|
||||
"versionCode" : 105,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,34 @@
|
|||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/cable-type/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-list/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-detail/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/marker-pole-add/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
|
|
|||
141
odf-uniapp/pages/cable-type/index.vue
Normal file
141
odf-uniapp/pages/cable-type/index.vue
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<view class="cable-type-page">
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">{{ cableName }}</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 小标题 -->
|
||||
<text class="section-title">请选择功能</text>
|
||||
|
||||
<!-- 入口按钮 -->
|
||||
<view class="entry-list">
|
||||
<view class="entry-card" @click="goMarkerPoleList">
|
||||
<text class="entry-name">标石、杆号牌</text>
|
||||
</view>
|
||||
<view class="entry-card" @click="goFaultList">
|
||||
<text class="entry-name">故障列表</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const cableId = ref('')
|
||||
const cableName = ref('')
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goMarkerPoleList() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/marker-pole-list/index?cableId=' + cableId.value + '&cableName=' + encodeURIComponent(cableName.value)
|
||||
})
|
||||
}
|
||||
|
||||
function goFaultList() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/fault-list/index?cableId=' + cableId.value + '&cableName=' + encodeURIComponent(cableName.value)
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.cableId) {
|
||||
cableId.value = options.cableId
|
||||
}
|
||||
if (options.cableName) {
|
||||
cableName.value = decodeURIComponent(options.cableName)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cable-type-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
max-width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 16rpx 24rpx 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.entry-list {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 40rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8E8E8;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.entry-name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -84,7 +84,7 @@ function handleSearch() {
|
|||
|
||||
function goFaultList(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/fault-list/index?cableId=' + item.id + '&cableName=' + encodeURIComponent(item.cableName)
|
||||
url: '/pages/cable-type/index?cableId=' + item.id + '&cableName=' + encodeURIComponent(item.cableName)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
561
odf-uniapp/pages/marker-pole-add/index.vue
Normal file
561
odf-uniapp/pages/marker-pole-add/index.vue
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
<template>
|
||||
<view class="marker-pole-add-page">
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">新增标石/杆号牌</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍照区域 -->
|
||||
<view class="photo-area">
|
||||
<scroll-view class="photo-scroll" scroll-x>
|
||||
<view class="photo-list">
|
||||
<view class="photo-add-btn" @click="takePhoto">
|
||||
<text class="plus-icon">+</text>
|
||||
<text class="add-text">点击拍摄</text>
|
||||
</view>
|
||||
<image
|
||||
class="photo-thumb"
|
||||
v-for="(photo, index) in photoList"
|
||||
:key="index"
|
||||
:src="photo"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-area">
|
||||
<view class="form-group">
|
||||
<text class="form-label">名称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.name"
|
||||
placeholder="请输入名称"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">拍摄时间</text>
|
||||
<view class="form-display">
|
||||
<text class="display-text">{{ form.recordTime || '拍摄第一张照片后自动填充' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">责任人</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.personnel"
|
||||
placeholder="请输入"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">实际里程</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.actualMileage"
|
||||
placeholder="请输入"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">所属光缆</text>
|
||||
<view class="form-display">
|
||||
<text class="display-text">{{ form.cableName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">所属公司</text>
|
||||
<view class="form-display">
|
||||
<text class="display-text">{{ form.companyName || '提交时自动填充' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">地点</text>
|
||||
<view class="location-btn" @click="getLocation">
|
||||
<text class="location-btn-text">获取经纬度</text>
|
||||
</view>
|
||||
<text class="location-text">当前经度:{{ form.longitude }} 当前纬度:{{ form.latitude }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定提交按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="submit-btn" :class="{ 'submit-btn-disabled': submitting }" @click.stop="handleSubmit">
|
||||
<text class="submit-btn-text">{{ submitting ? '提交中...' : '提交' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 离屏 canvas 用于水印绘制 -->
|
||||
<canvas
|
||||
canvas-id="watermarkCanvas"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
width: canvasW + 'px',
|
||||
height: canvasH + 'px',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1
|
||||
}"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, getCurrentInstance, nextTick } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import store from '@/store'
|
||||
import { addMarkerPole } from '@/services/markerPole'
|
||||
import { getPresignUrls, uploadToCos } from '@/services/cos'
|
||||
import { addWatermark } from '@/utils/watermark'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const photoList = ref([])
|
||||
const cableId = ref('')
|
||||
const submitting = ref(false)
|
||||
const canvasW = ref(100)
|
||||
const canvasH = ref(100)
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
recordTime: '',
|
||||
personnel: '',
|
||||
actualMileage: '',
|
||||
cableName: '',
|
||||
companyName: store.userName || '',
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function formatNow() {
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
const h = String(now.getHours()).padStart(2, '0')
|
||||
const min = String(now.getMinutes()).padStart(2, '0')
|
||||
return `${y}/${m}/${d} ${h}:${min}`
|
||||
}
|
||||
|
||||
function takePhoto() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
success(res) {
|
||||
const tempPath = res.tempFilePaths[0]
|
||||
photoList.value.push(tempPath)
|
||||
// 第一张照片自动填充拍摄时间
|
||||
if (photoList.value.length === 1) {
|
||||
form.recordTime = formatNow()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getLocation() {
|
||||
// #ifdef H5
|
||||
uni.showLoading({ title: '定位中...', mask: true })
|
||||
if (window.AMap) {
|
||||
AMap.plugin('AMap.Geolocation', () => {
|
||||
const geolocation = new AMap.Geolocation({
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000
|
||||
})
|
||||
geolocation.getCurrentPosition((status, result) => {
|
||||
uni.hideLoading()
|
||||
if (status === 'complete' && result.position) {
|
||||
form.longitude = result.position.lng
|
||||
form.latitude = result.position.lat
|
||||
uni.showToast({ title: '获取成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '获取位置失败,请检查浏览器定位权限', icon: 'none' })
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '地图SDK加载失败', icon: 'none' })
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
doGetLocation()
|
||||
// #endif
|
||||
}
|
||||
|
||||
// #ifndef H5
|
||||
function doGetLocation() {
|
||||
uni.showLoading({ title: '定位中...', mask: true })
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
isHighAccuracy: true,
|
||||
highAccuracyExpireTime: 10000,
|
||||
success(res) {
|
||||
form.latitude = res.latitude
|
||||
form.longitude = res.longitude
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取成功', icon: 'success' })
|
||||
},
|
||||
fail(err) {
|
||||
fallbackPlusLocation(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function fallbackPlusLocation(originalErr) {
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus !== 'undefined' && plus.geolocation) {
|
||||
plus.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
form.latitude = pos.coords.latitude
|
||||
form.longitude = pos.coords.longitude
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取成功', icon: 'success' })
|
||||
},
|
||||
(plusErr) => {
|
||||
uni.hideLoading()
|
||||
handleLocationError(originalErr)
|
||||
},
|
||||
{ provider: 'system', coordsType: 'gcj02', timeout: 15000 }
|
||||
)
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
handleLocationError(originalErr)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.hideLoading()
|
||||
handleLocationError(originalErr)
|
||||
// #endif
|
||||
}
|
||||
|
||||
function handleLocationError(err) {
|
||||
const errMsg = (err.errMsg || '').toLowerCase()
|
||||
if (errMsg.includes('deny') || errMsg.includes('auth') || errMsg.includes('permission')) {
|
||||
uni.showModal({
|
||||
title: '定位权限未开启',
|
||||
content: '请在系统设置中允许本应用使用定位服务',
|
||||
confirmText: '去设置',
|
||||
success(modalRes) {
|
||||
if (modalRes.confirm) {
|
||||
uni.openSetting && uni.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '获取位置失败,请检查GPS是否开启', icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.name.trim()) {
|
||||
uni.showToast({ title: '请输入名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (photoList.value.length === 0) {
|
||||
uni.showToast({ title: '请至少拍摄一张照片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!cableId.value) {
|
||||
uni.showToast({ title: '所属光缆信息缺失,无法提交', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '处理图片中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 水印处理
|
||||
const watermarkLines = [
|
||||
`经度:${form.longitude} 纬度:${form.latitude}`,
|
||||
`${form.recordTime} ${form.personnel}`,
|
||||
`所属光缆:${form.cableName}`
|
||||
]
|
||||
const watermarkedPhotos = []
|
||||
for (let idx = 0; idx < photoList.value.length; idx++) {
|
||||
const photo = photoList.value[idx]
|
||||
try {
|
||||
const result = await addWatermark(photo, watermarkLines, {
|
||||
canvasId: 'watermarkCanvas',
|
||||
proxy: instance.proxy,
|
||||
setSize(w, h) {
|
||||
canvasW.value = w
|
||||
canvasH.value = h
|
||||
}
|
||||
})
|
||||
watermarkedPhotos.push(result)
|
||||
await nextTick()
|
||||
} catch (err) {
|
||||
console.warn(`[marker-pole-add] 第${idx + 1}张水印失败, 使用原图`, err)
|
||||
watermarkedPhotos.push(photo)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取 COS 预签名 URL
|
||||
uni.showLoading({ title: '准备上传...', mask: true })
|
||||
const presignList = await getPresignUrls(watermarkedPhotos.length, '.jpg')
|
||||
|
||||
// 3. 逐张直传到 COS
|
||||
const imageUrls = []
|
||||
for (let i = 0; i < watermarkedPhotos.length; i++) {
|
||||
uni.showLoading({ title: `上传图片 ${i + 1}/${watermarkedPhotos.length}`, mask: true })
|
||||
await uploadToCos(presignList[i].presignUrl, watermarkedPhotos[i])
|
||||
imageUrls.push(presignList[i].accessUrl)
|
||||
}
|
||||
|
||||
// 4. 提交表单
|
||||
uni.showLoading({ title: '提交中...', mask: true })
|
||||
const res = await addMarkerPole({
|
||||
cableId: Number(cableId.value),
|
||||
name: form.name,
|
||||
recordTime: form.recordTime,
|
||||
personnel: form.personnel,
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
actualMileage: form.actualMileage,
|
||||
imageUrls
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
// #ifdef H5
|
||||
uni.redirectTo({
|
||||
url: '/pages/marker-pole-list/index?cableId=' + cableId.value + '&cableName=' + encodeURIComponent(form.cableName)
|
||||
})
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.navigateBack()
|
||||
// #endif
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err.message || '网络异常,请重试', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.cableId) {
|
||||
cableId.value = options.cableId
|
||||
}
|
||||
if (options.cableName) {
|
||||
form.cableName = decodeURIComponent(options.cableName)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marker-pole-add-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.photo-area {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.photo-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.photo-list {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.photo-add-btn {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: #fff;
|
||||
border: 2rpx dashed #CCCCCC;
|
||||
border-radius: 12rpx;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
font-size: 48rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.photo-thumb {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-area {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8E8E8;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-display {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8E8E8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.location-btn {
|
||||
background: #1A73EC;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.location-btn-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-top: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #1A73EC;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.submit-btn-disabled {
|
||||
background: #93bdf5;
|
||||
}
|
||||
</style>
|
||||
316
odf-uniapp/pages/marker-pole-detail/index.vue
Normal file
316
odf-uniapp/pages/marker-pole-detail/index.vue
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
<template>
|
||||
<view class="marker-pole-detail-page">
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">标石/杆号牌详情</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片区域 -->
|
||||
<view class="image-area" v-if="imageList.length > 0">
|
||||
<scroll-view class="image-scroll" scroll-x>
|
||||
<view class="image-grid">
|
||||
<view
|
||||
class="image-wrapper"
|
||||
v-for="(img, index) in imageList"
|
||||
:key="img"
|
||||
@click="previewImage(index)"
|
||||
>
|
||||
<view class="image-placeholder" v-if="imageStatus[index] !== 'loaded'">
|
||||
<text class="placeholder-text">{{ imageStatus[index] === 'error' ? '加载失败' : '加载中...' }}</text>
|
||||
</view>
|
||||
<image
|
||||
class="image-item"
|
||||
:class="{ 'image-hidden': imageStatus[index] !== 'loaded' }"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
@load="onImageLoad(index)"
|
||||
@error="onImageError(index)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 信息展示区域 -->
|
||||
<view class="info-area">
|
||||
<view class="info-row">
|
||||
<text class="info-label">名称</text>
|
||||
<text class="info-value">{{ detail.name }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">时间</text>
|
||||
<text class="info-value">{{ detail.recordTime }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">责任人</text>
|
||||
<text class="info-value">{{ detail.personnel || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">导航点</text>
|
||||
<text class="info-value">{{ formatCoord(detail.longitude, detail.latitude) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">所属公司</text>
|
||||
<text class="info-value">{{ detail.deptName || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row last-row">
|
||||
<text class="info-label">实际里程</text>
|
||||
<text class="info-value">{{ detail.actualMileage || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航按钮 -->
|
||||
<view class="bottom-bar" v-if="hasLocation">
|
||||
<view class="navigate-btn" @click="handleNavigate">
|
||||
<text class="navigate-btn-text">导航</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getMarkerPoleDetail } from '@/services/markerPole'
|
||||
import { BASE_URL } from '@/services/api'
|
||||
import { openNavigation } from '@/utils/navigation'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const recordId = ref('')
|
||||
const imageList = ref([])
|
||||
const imageStatus = reactive({})
|
||||
|
||||
const detail = reactive({
|
||||
name: '',
|
||||
recordTime: '',
|
||||
personnel: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
deptName: '',
|
||||
actualMileage: ''
|
||||
})
|
||||
|
||||
const hasLocation = computed(() => {
|
||||
return detail.latitude && detail.longitude &&
|
||||
Number(detail.latitude) !== 0 && Number(detail.longitude) !== 0
|
||||
})
|
||||
|
||||
function formatCoord(lng, lat) {
|
||||
if ((!lng || Number(lng) === 0) && (!lat || Number(lat) === 0)) return '-'
|
||||
return `${lng}, ${lat}`
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
try {
|
||||
const res = await getMarkerPoleDetail(recordId.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
const d = res.data
|
||||
detail.name = d.name || ''
|
||||
detail.recordTime = d.recordTime || ''
|
||||
detail.personnel = d.personnel || ''
|
||||
detail.latitude = d.latitude || 0
|
||||
detail.longitude = d.longitude || 0
|
||||
detail.deptName = d.deptName || ''
|
||||
detail.actualMileage = d.actualMileage || ''
|
||||
|
||||
imageList.value = (d.images || []).map((img, i) => {
|
||||
const url = img.url || img.imageUrl || ''
|
||||
imageStatus[i] = 'loading'
|
||||
return url.startsWith('http') ? url : BASE_URL + url
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function onImageLoad(index) {
|
||||
imageStatus[index] = 'loaded'
|
||||
}
|
||||
|
||||
function onImageError(index) {
|
||||
imageStatus[index] = 'error'
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function previewImage(index) {
|
||||
uni.previewImage({
|
||||
urls: imageList.value,
|
||||
current: imageList.value[index]
|
||||
})
|
||||
}
|
||||
|
||||
function handleNavigate() {
|
||||
openNavigation(detail.latitude, detail.longitude, detail.name || '标石/杆号牌')
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.id) {
|
||||
recordId.value = options.id
|
||||
}
|
||||
loadDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marker-pole-detail-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.image-area {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.image-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: inline-flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #E8E8E8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.info-area {
|
||||
background-color: #fff;
|
||||
margin: 0 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-row.last-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
width: 180rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navigate-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #1A73EC;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navigate-btn-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
265
odf-uniapp/pages/marker-pole-list/index.vue
Normal file
265
odf-uniapp/pages/marker-pole-list/index.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<view class="marker-pole-list-page">
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">标石/杆号牌</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 小标题 -->
|
||||
<text class="section-title">标石/杆号牌列表</text>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view class="marker-pole-list" v-if="list.length > 0">
|
||||
<view
|
||||
class="marker-pole-card"
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="card-row">
|
||||
<text class="card-label">名称:</text>
|
||||
<text class="card-value">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">时间:</text>
|
||||
<text class="card-value">{{ item.recordTime }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">责任人:</text>
|
||||
<text class="card-value">{{ item.personnel || '-' }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">导航点:</text>
|
||||
<text class="card-value">{{ formatCoord(item.longitude, item.latitude) }}</text>
|
||||
</view>
|
||||
<view class="card-row last-row">
|
||||
<text class="card-label">实际里程:</text>
|
||||
<text class="card-value">{{ item.actualMileage || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && list.length === 0">
|
||||
<text class="empty-text">暂无标石/杆号牌记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="add-btn" @click="goAdd">
|
||||
<text class="add-btn-text">新增</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getMarkerPoleList } from '@/services/markerPole'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const list = ref([])
|
||||
const cableId = ref('')
|
||||
const cableName = ref('')
|
||||
const pageNum = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalPage = ref(1)
|
||||
const loading = ref(false)
|
||||
|
||||
function formatCoord(lng, lat) {
|
||||
if ((!lng || lng === 0) && (!lat || lat === 0)) return '-'
|
||||
return `${lng}, ${lat}`
|
||||
}
|
||||
|
||||
async function loadList(isLoadMore = false) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await getMarkerPoleList(cableId.value, pageNum.value, pageSize.value)
|
||||
if (res.code === 200) {
|
||||
const data = res.data || {}
|
||||
const items = data.result || []
|
||||
if (isLoadMore) {
|
||||
list.value = [...list.value, ...items]
|
||||
} else {
|
||||
list.value = items
|
||||
}
|
||||
totalPage.value = data.totalPage || 1
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
uni.navigateTo({ url: '/pages/marker-pole-detail/index?id=' + item.id })
|
||||
}
|
||||
|
||||
function goAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/marker-pole-add/index?cableId=' + cableId.value + '&cableName=' + encodeURIComponent(cableName.value)
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.cableId) {
|
||||
cableId.value = options.cableId
|
||||
}
|
||||
if (options.cableName) {
|
||||
cableName.value = decodeURIComponent(options.cableName)
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
pageNum.value = 1
|
||||
loadList()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (pageNum.value < totalPage.value) {
|
||||
pageNum.value++
|
||||
loadList(true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marker-pole-list-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 16rpx 24rpx 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.marker-pole-list {
|
||||
padding: 0 0 24rpx;
|
||||
}
|
||||
|
||||
.marker-pole-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8E8E8;
|
||||
padding: 24rpx;
|
||||
margin: 0 24rpx 20rpx;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.card-row.last-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #1A73EC;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-btn-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -202,10 +202,10 @@ onLoad((options) => {
|
|||
.port-tips { font-size: 20rpx; color: #fff; text-align: center; }
|
||||
.port-name { font-size: 20rpx; color: #333; margin-top: 6rpx; text-align: center; }
|
||||
.optical-box-wrapper { display: inline-flex; flex-direction: row; min-width: 100%; }
|
||||
.optical-left-col { background-color: #FFC0CB; padding: 16rpx; border-radius: 8rpx 0 0 8rpx; }
|
||||
.optical-left-col .optical-port-row { background-color: #FFB6C1; border-radius: 8rpx; padding: 12rpx; margin-bottom: 12rpx; }
|
||||
.optical-right-col { background-color: #E0F7FA; padding: 16rpx; border-radius: 0 8rpx 8rpx 0; }
|
||||
.optical-right-col .optical-port-row { background-color: #B3E5FC; border-radius: 8rpx; padding: 12rpx; margin-bottom: 12rpx; }
|
||||
.optical-left-col { background-color: #C8E6C9; padding: 16rpx; border-radius: 8rpx 0 0 8rpx; }
|
||||
.optical-left-col .optical-port-row { background-color: #A5D6A7; border-radius: 8rpx; padding: 12rpx; margin-bottom: 12rpx; }
|
||||
.optical-right-col { background-color: #FFE0B2; padding: 16rpx; border-radius: 0 8rpx 8rpx 0; }
|
||||
.optical-right-col .optical-port-row { background-color: #FFCC80; border-radius: 8rpx; padding: 12rpx; margin-bottom: 12rpx; }
|
||||
.optical-divider { width: 4rpx; background-color: #999; flex-shrink: 0; }
|
||||
.optical-col-header { padding: 0 0 12rpx; text-align: center; }
|
||||
.optical-col-title { font-size: 26rpx; font-weight: 600; color: #333; }
|
||||
|
|
|
|||
|
|
@ -58,8 +58,36 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标石、杆号牌分类 -->
|
||||
<view class="section" v-if="markerPoles.length > 0">
|
||||
<text class="section-title">标石、杆号牌</text>
|
||||
<view
|
||||
class="fault-card"
|
||||
v-for="item in markerPoles"
|
||||
:key="item.id"
|
||||
@click="goMarkerPoleDetail(item)"
|
||||
>
|
||||
<view class="fault-row">
|
||||
<text class="fault-label">名称:</text>
|
||||
<text class="fault-value">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="fault-row">
|
||||
<text class="fault-label">时间:</text>
|
||||
<text class="fault-value">{{ item.recordTime }}</text>
|
||||
</view>
|
||||
<view class="fault-row">
|
||||
<text class="fault-label">责任人:</text>
|
||||
<text class="fault-value">{{ item.personnel }}</text>
|
||||
</view>
|
||||
<view class="fault-row last-row">
|
||||
<text class="fault-label">所属光缆:</text>
|
||||
<text class="fault-value">{{ item.cableName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无结果 -->
|
||||
<view class="no-result" v-if="cables.length === 0 && faults.length === 0">
|
||||
<view class="no-result" v-if="cables.length === 0 && faults.length === 0 && markerPoles.length === 0">
|
||||
<text class="no-result-text">暂无搜索结果</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -71,10 +99,12 @@
|
|||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { searchCablesAndFaults } from '@/services/trunk'
|
||||
import { extractMarkerPoles } from '@/services/search'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const cables = ref([])
|
||||
const faults = ref([])
|
||||
const markerPoles = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
async function doSearch(deptId, keyword) {
|
||||
|
|
@ -84,6 +114,7 @@ async function doSearch(deptId, keyword) {
|
|||
if (res.code === 200 && res.data) {
|
||||
cables.value = res.data.cables || []
|
||||
faults.value = res.data.faults || []
|
||||
markerPoles.value = extractMarkerPoles(res.data)
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '搜索失败', icon: 'none' })
|
||||
|
|
@ -106,6 +137,10 @@ function goFaultDetail(item) {
|
|||
uni.navigateTo({ url: '/pages/fault-detail/index?faultId=' + item.id })
|
||||
}
|
||||
|
||||
function goMarkerPoleDetail(item) {
|
||||
uni.navigateTo({ url: '/pages/marker-pole-detail/index?id=' + item.id })
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
const deptId = options.deptId || ''
|
||||
const keyword = decodeURIComponent(options.keyword || '')
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
import store from '@/store'
|
||||
|
||||
// export const BASE_URL = 'http://49.233.115.141:11082'
|
||||
// export const BASE_URL = 'https://ssl.api.suigongxj.top'
|
||||
export const BASE_URL = 'https://ssl.api.suigongxj.top'
|
||||
// const BASE_URL = 'http://115.190.188.216:2861'
|
||||
export const BASE_URL = 'https://api.wux.shhmkjgs.cn'
|
||||
// export const BASE_URL = 'https://api.wux.shhmkjgs.cn'
|
||||
export const APP_VERSION = '1.0.5'
|
||||
// export const BASE_URL = 'http://127.0.0.1:8888'
|
||||
const TIMEOUT = 20000
|
||||
|
|
|
|||
27
odf-uniapp/services/markerPole.js
Normal file
27
odf-uniapp/services/markerPole.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { get, post } from './api'
|
||||
|
||||
/**
|
||||
* 获取标石/杆号牌列表
|
||||
* @param {number} cableId - 光缆ID
|
||||
* @param {number} pageNum - 页码
|
||||
* @param {number} pageSize - 每页条数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const getMarkerPoleList = (cableId, pageNum, pageSize) =>
|
||||
get('/business/OdfMarkerPoles/list', { cableId, pageNum, pageSize })
|
||||
|
||||
/**
|
||||
* 获取标石/杆号牌详情
|
||||
* @param {number} id - 标石记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const getMarkerPoleDetail = (id) =>
|
||||
get(`/business/OdfMarkerPoles/${id}`)
|
||||
|
||||
/**
|
||||
* 新增标石/杆号牌(JSON 提交,图片已上传至 COS)
|
||||
* @param {object} data - 标石信息,含 imageUrls 数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const addMarkerPole = (data) =>
|
||||
post('/business/OdfMarkerPoles/add', data, { timeout: 120000 })
|
||||
|
|
@ -5,3 +5,12 @@ export const searchPorts = (key, pageNum, pageSize, roomId) => {
|
|||
if (roomId) params.roomId = roomId
|
||||
return get('/business/OdfPorts/search2', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从搜索 API 响应中提取 markerPoles 字段
|
||||
* @param {object} data - 搜索 API 返回的 data 对象
|
||||
* @returns {Array} 标石/杆号牌记录列表
|
||||
*/
|
||||
export const extractMarkerPoles = (data) => {
|
||||
return data?.markerPoles || []
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
namespace ZR.Admin.WebApi.Controllers.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志管理
|
||||
/// </summary>
|
||||
[Route("business/OdfAuditLogs")]
|
||||
public class OdfAuditLogsController : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志接口
|
||||
/// </summary>
|
||||
private readonly IOdfAuditLogsService _OdfAuditLogsService;
|
||||
|
||||
public OdfAuditLogsController(IOdfAuditLogsService OdfAuditLogsService)
|
||||
{
|
||||
_OdfAuditLogsService = OdfAuditLogsService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审计日志列表分页查询
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("list")]
|
||||
[ActionPermissionFilter(Permission = "odfauditlogs:list")]
|
||||
public IActionResult GetList([FromQuery] OdfAuditLogsQueryDto parm)
|
||||
{
|
||||
var response = _OdfAuditLogsService.GetList(parm);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiniExcelLibs;
|
||||
using ZR.Admin.WebApi.Filters;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
|
|
@ -31,7 +32,8 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[ActionPermissionFilter(Permission = "odfcablefaults:list")]
|
||||
public IActionResult GetList([FromQuery] OdfCableFaultsQueryDto parm)
|
||||
{
|
||||
var response = _OdfCableFaultsService.GetList(parm);
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var response = _OdfCableFaultsService.GetList(parm, deptId);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("add")]
|
||||
[ActionPermissionFilter(Permission = "odfcablefaults:add")]
|
||||
[Log(Title = "干线故障", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> Add([FromBody] OdfCableFaultAddDto dto)
|
||||
{
|
||||
dto.UserId = HttpContext.GetUId();
|
||||
|
|
@ -68,6 +71,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("incrementFaultCount/{id}")]
|
||||
[ActionPermissionFilter(Permission = "odfcablefaults:edit")]
|
||||
[Log(Title = "干线故障", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult IncrementFaultCount(int id)
|
||||
{
|
||||
var response = _OdfCableFaultsService.IncrementFaultCount(id);
|
||||
|
|
@ -80,6 +84,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("updateMileageCorrection/{id}")]
|
||||
[ActionPermissionFilter(Permission = "odfcablefaults:edit")]
|
||||
[Log(Title = "干线故障", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult UpdateMileageCorrection(int id, [FromBody] MileageCorrectionDto dto)
|
||||
{
|
||||
var response = _OdfCableFaultsService.UpdateMileageCorrection(id, dto.MileageCorrection);
|
||||
|
|
@ -93,6 +98,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("delete/{ids}")]
|
||||
[ActionPermissionFilter(Permission = "odfcablefaults:delete")]
|
||||
[Log(Title = "干线故障", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Delete(string ids)
|
||||
{
|
||||
var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
|
|
@ -122,7 +128,8 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[ActionPermissionFilter(Permission = "odfcablefaults:export")]
|
||||
public IActionResult Export([FromQuery] OdfCableFaultsQueryDto parm)
|
||||
{
|
||||
var list = _OdfCableFaultsService.ExportList(parm);
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var list = _OdfCableFaultsService.ExportList(parm, deptId);
|
||||
if (list == null || list.Count <= 0)
|
||||
{
|
||||
return ToResponse(ResultCode.FAIL, "没有要导出的数据");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using ZR.Admin.WebApi.Filters;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
|
@ -31,12 +32,13 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[ActionPermissionFilter(Permission = "odfcables:list")]
|
||||
public IActionResult GetList([FromQuery] OdfCablesQueryDto parm)
|
||||
{
|
||||
var response = _OdfCablesService.GetList(parm);
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var response = _OdfCablesService.GetList(parm, deptId);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索光缆和故障(限定公司范围)
|
||||
/// 搜索光缆、故障和标石/杆号牌(限定公司范围)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("search")]
|
||||
|
|
@ -54,6 +56,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost]
|
||||
[ActionPermissionFilter(Permission = "odfcables:add")]
|
||||
[Log(Title = "光缆管理", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Add([FromBody] OdfCables parm)
|
||||
{
|
||||
parm.CreatedAt = DateTime.Now;
|
||||
|
|
@ -69,6 +72,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPut]
|
||||
[ActionPermissionFilter(Permission = "odfcables:edit")]
|
||||
[Log(Title = "光缆管理", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Update([FromBody] OdfCables parm)
|
||||
{
|
||||
parm.UpdatedAt = DateTime.Now;
|
||||
|
|
@ -96,6 +100,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("delete/{id}")]
|
||||
[ActionPermissionFilter(Permission = "odfcables:delete")]
|
||||
[Log(Title = "光缆管理", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Delete(int id)
|
||||
{
|
||||
var response = _OdfCablesService.Delete(id);
|
||||
|
|
@ -111,7 +116,8 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[ActionPermissionFilter(Permission = "odfcables:export")]
|
||||
public IActionResult Export([FromQuery] OdfCablesQueryDto parm)
|
||||
{
|
||||
var list = _OdfCablesService.ExportList(parm);
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var list = _OdfCablesService.ExportList(parm, deptId);
|
||||
if (list == null || list.Result == null || list.Result.Count <= 0)
|
||||
{
|
||||
return ToResponse(ResultCode.FAIL, "没有要导出的数据");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using ZR.Admin.WebApi.Filters;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
//创建时间:2025-01-01
|
||||
namespace ZR.Admin.WebApi.Controllers.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌管理
|
||||
/// </summary>
|
||||
[Route("business/OdfMarkerPoles")]
|
||||
public class OdfMarkerPolesController : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌接口
|
||||
/// </summary>
|
||||
private readonly IOdfMarkerPolesService _OdfMarkerPolesService;
|
||||
|
||||
public OdfMarkerPolesController(IOdfMarkerPolesService OdfMarkerPolesService)
|
||||
{
|
||||
_OdfMarkerPolesService = OdfMarkerPolesService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌列表分页查询
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("list")]
|
||||
[ActionPermissionFilter(Permission = "odfmarkerpoles:list")]
|
||||
public IActionResult GetList([FromQuery] OdfMarkerPolesQueryDto parm)
|
||||
{
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var response = _OdfMarkerPolesService.GetList(parm, deptId);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌详情(含图片)
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{id}")]
|
||||
[ActionPermissionFilter(Permission = "odfmarkerpoles:query")]
|
||||
public IActionResult GetDetail(int id)
|
||||
{
|
||||
var response = _OdfMarkerPolesService.GetDetail(id);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增标石/杆号牌(图片已上传至COS,提交COS URL)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add")]
|
||||
[ActionPermissionFilter(Permission = "odfmarkerpoles:add")]
|
||||
[Log(Title = "标石/杆号牌", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Add([FromBody] OdfMarkerPoleAddDto dto)
|
||||
{
|
||||
dto.UserId = HttpContext.GetUId();
|
||||
var deptId = HttpContext.GetDeptId();
|
||||
var response = _OdfMarkerPolesService.Add(dto, deptId);
|
||||
return SUCCESS(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑标石/杆号牌
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPut("edit")]
|
||||
[ActionPermissionFilter(Permission = "odfmarkerpoles:edit")]
|
||||
[Log(Title = "标石/杆号牌", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Update([FromBody] OdfMarkerPoleEditDto dto)
|
||||
{
|
||||
var response = _OdfMarkerPolesService.Update(dto);
|
||||
return ToResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除标石/杆号牌(支持单个/批量)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("delete/{ids}")]
|
||||
[ActionPermissionFilter(Permission = "odfmarkerpoles:delete")]
|
||||
[Log(Title = "标石/杆号牌", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult Delete(string ids)
|
||||
{
|
||||
var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => int.TryParse(s.Trim(), out var v) ? v : 0)
|
||||
.Where(v => v > 0)
|
||||
.ToList();
|
||||
|
||||
if (idList.Count == 0)
|
||||
{
|
||||
return ToResponse(ResultCode.FAIL, "请选择要删除的数据");
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
foreach (var id in idList)
|
||||
{
|
||||
total += _OdfMarkerPolesService.Delete(id);
|
||||
}
|
||||
return ToResponse(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ZR.Admin.WebApi.Filters;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Model.System.Dto;
|
||||
|
|
@ -361,6 +362,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpGet("status/{Id}/{status}")]
|
||||
[ActionPermissionFilter(Permission = "odfports:edit")]
|
||||
[Log(Title = "更新端口状态", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult GetOdfPortsStatus(int Id, int status)
|
||||
{
|
||||
var response = _OdfPortsService.GetInfo(Id);
|
||||
|
|
@ -388,6 +390,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost]
|
||||
[ActionPermissionFilter(Permission = "odfports:add")]
|
||||
[Log(Title = "端口", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> AddOdfPorts([FromBody] OdfPortsDto parm)
|
||||
{
|
||||
var modal = parm.Adapt<OdfPorts>().ToCreate(HttpContext);
|
||||
|
|
@ -419,6 +422,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPut]
|
||||
[ActionPermissionFilter(Permission = "odfports:edit")]
|
||||
[Log(Title = "端口", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> UpdateOdfPorts([FromBody] OdfPortsDto parm)
|
||||
{
|
||||
var modal = parm.Adapt<OdfPorts>().ToUpdate(HttpContext);
|
||||
|
|
@ -454,6 +458,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("save")]
|
||||
[ActionPermissionFilter(Permission = "odfports:edit")]
|
||||
[Log(Title = "APP修改端口", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> SaveMOdfPorts([FromBody] OdfPortsMMDto parm)
|
||||
{
|
||||
var port = _OdfPortsService.GetById(parm.Id);
|
||||
|
|
@ -562,6 +567,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpGet("empty/{Id}")]
|
||||
[ActionPermissionFilter(Permission = "odfports:edit")]
|
||||
[Log(Title = "清空数据", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult GetOdfPortsEmpty(int Id)
|
||||
{
|
||||
var response = _OdfPortsService.GetInfo(Id);
|
||||
|
|
@ -582,6 +588,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("delete/{ids}")]
|
||||
[ActionPermissionFilter(Permission = "odfports:delete")]
|
||||
[Log(Title = "端口", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult DeleteOdfPorts([FromRoute] string ids)
|
||||
{
|
||||
var idArr = Tools.SplitAndConvert<int>(ids);
|
||||
|
|
@ -1016,6 +1023,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("deleteAll")]
|
||||
[ActionPermissionFilter(Permission = "odfports:delete")]
|
||||
[Log(Title = "删除端口数据", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> DeleteAll([FromBody] OdfRoomsTreeDto treeDto)
|
||||
{
|
||||
if (treeDto.Level == 2)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using ZR.Admin.WebApi.Filters;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
|
@ -75,6 +76,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost]
|
||||
[ActionPermissionFilter(Permission = "odfracks:add")]
|
||||
[Log(Title = "机架列表", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public IActionResult AddOdfRacks([FromBody] OdfRacksDto parm)
|
||||
{
|
||||
var modal = parm.Adapt<OdfRacks>().ToCreate(HttpContext);
|
||||
|
|
@ -100,6 +102,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("expert")]
|
||||
[ActionPermissionFilter(Permission = "odfracks:add")]
|
||||
[Log(Title = "机架列表", BusinessType = BusinessType.INSERT)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> AddOdfRacksExpert([FromBody] OdfRacksExpertDto parm)
|
||||
{
|
||||
var modal = parm.Adapt<OdfRacks>().ToCreate(HttpContext);
|
||||
|
|
@ -278,6 +281,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPut]
|
||||
[ActionPermissionFilter(Permission = "odfracks:edit")]
|
||||
[Log(Title = "机架列表", BusinessType = BusinessType.UPDATE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> UpdateOdfRacks([FromBody] OdfRacksDto parm)
|
||||
{
|
||||
var modal = parm.Adapt<OdfRacks>().ToUpdate(HttpContext);
|
||||
|
|
@ -311,6 +315,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
[HttpPost("delete/{ids}")]
|
||||
[ActionPermissionFilter(Permission = "odfracks:delete")]
|
||||
[Log(Title = "机架列表", BusinessType = BusinessType.DELETE)]
|
||||
[ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
public async Task<IActionResult> DeleteOdfRacks([FromRoute] string ids)
|
||||
{
|
||||
var idArr = Tools.SplitAndConvert<int>(ids);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
private readonly ISysConfigService sysConfigService;
|
||||
private readonly ISysRoleService roleService;
|
||||
private readonly ISmsCodeLogService smsCodeLogService;
|
||||
private readonly ISysDeptService deptService;
|
||||
|
||||
public SysLoginController(
|
||||
ISysMenuService sysMenuService,
|
||||
|
|
@ -32,6 +33,7 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
ISysConfigService configService,
|
||||
ISysRoleService sysRoleService,
|
||||
ISmsCodeLogService smsCodeLogService,
|
||||
ISysDeptService deptService,
|
||||
ICaptcha captcha)
|
||||
{
|
||||
SecurityCodeHelper = captcha;
|
||||
|
|
@ -41,6 +43,7 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
this.permissionService = permissionService;
|
||||
this.sysConfigService = configService;
|
||||
this.smsCodeLogService = smsCodeLogService;
|
||||
this.deptService = deptService;
|
||||
roleService = sysRoleService;
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +71,23 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
var user = sysLoginService.Login(loginBody, new SysLogininfor() { LoginLocation = location });
|
||||
|
||||
List<SysRole> roles = roleService.SelectUserRoleListByUserId(user.UserId);
|
||||
|
||||
// 分公司账号登录管理后台限制:非超级管理员的分公司账号必须拥有「分公司管理员」角色
|
||||
if (!user.IsAdmin && !roles.Any(r => r.RoleKey == GlobalConstant.AdminRole))
|
||||
{
|
||||
var dept = deptService.GetFirst(d => d.DeptId == user.DeptId);
|
||||
if (dept != null)
|
||||
{
|
||||
// 判断是否为分公司级别:Ancestors 中包含两级以上(如 "0,100" 表示分公司)
|
||||
var ancestors = dept.Ancestors?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
|
||||
bool isBranchCompany = ancestors.Length >= 2;
|
||||
if (isBranchCompany && !roles.Any(r => r.RoleKey == "branch_admin"))
|
||||
{
|
||||
throw new CustomException(ResultCode.LOGIN_ERROR, "该账号无权登录管理后台,请联系管理员分配「分公司管理员」角色", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//权限集合 eg *:*:*,system:user:list
|
||||
List<string> permissions = permissionService.GetMenuPermission(new SysUserDto() { UserId = user.UserId });
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using SqlSugar;
|
|||
using ZR.Model;
|
||||
using ZR.Model.System;
|
||||
using ZR.Model.System.Dto;
|
||||
using ZR.Service.Business;
|
||||
|
||||
using static SKIT.FlurlHttpClient.Wechat.Api.Models.CgibinUserInfoBatchGetRequest.Types;
|
||||
|
||||
|
|
@ -23,17 +24,20 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
private readonly ISysRoleService RoleService;
|
||||
private readonly ISysPostService PostService;
|
||||
private readonly ISysUserPostService UserPostService;
|
||||
private readonly ISqlSugarClient _db;
|
||||
|
||||
public SysUserController(
|
||||
ISysUserService userService,
|
||||
ISysRoleService roleService,
|
||||
ISysPostService postService,
|
||||
ISysUserPostService userPostService)
|
||||
ISysUserPostService userPostService,
|
||||
ISqlSugarClient db)
|
||||
{
|
||||
UserService = userService;
|
||||
RoleService = roleService;
|
||||
PostService = postService;
|
||||
UserPostService = userPostService;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -91,6 +95,18 @@ namespace ZR.Admin.WebApi.Controllers.System
|
|||
var user = parm.Adapt<SysUser>().ToCreate(HttpContext);
|
||||
user.DeptId = parm.DeptId;
|
||||
if (user == null) { return ToResponse(ApiResult.Error(101, "请求参数错误")); }
|
||||
|
||||
// 分公司管理员创建账号范围限制:非超级管理员需校验新账号 DeptId 在可见部门范围内
|
||||
if (!HttpContext.IsAdmin())
|
||||
{
|
||||
var currentDeptId = HttpContext.GetDeptId();
|
||||
var visibleDeptIds = DeptDataScopeHelper.GetVisibleDeptIds(_db, currentDeptId);
|
||||
if (!visibleDeptIds.Contains(user.DeptId))
|
||||
{
|
||||
throw new CustomException("无权为该公司创建账号");
|
||||
}
|
||||
}
|
||||
|
||||
if (UserConstants.NOT_UNIQUE.Equals(UserService.CheckUserNameUnique(user.UserName)))
|
||||
{
|
||||
return ToResponse(ApiResult.Error($"新增用户 '{user.UserName}'失败,登录账号已存在"));
|
||||
|
|
|
|||
336
server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs
Normal file
336
server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
using Infrastructure;
|
||||
using Infrastructure.Attribute;
|
||||
using Infrastructure.Enums;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using SqlSugar;
|
||||
using System.Text.Json;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
namespace ZR.Admin.WebApi.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// ODF 审计日志 ActionFilter
|
||||
/// 拦截标石/ODF/干线相关 Controller 的增删改操作,自动记录审计日志。
|
||||
/// 使用方式:在 Action 上添加 [ServiceFilter(typeof(OdfAuditLogFilter))]
|
||||
/// </summary>
|
||||
public class OdfAuditLogFilter : IAsyncActionFilter
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly IOdfAuditLogsService _auditLogsService;
|
||||
|
||||
/// <summary>
|
||||
/// 控制器名称到数据库表名的映射
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> ControllerTableMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "OdfMarkerPoles", "odf_marker_poles" },
|
||||
{ "OdfCables", "odf_cables" },
|
||||
{ "OdfCableFaults", "odf_cable_faults" },
|
||||
{ "OdfPorts", "odf_ports" },
|
||||
{ "OdfRacks", "odf_racks" },
|
||||
{ "OdfFrames", "odf_frames" },
|
||||
{ "OdfRooms", "odf_rooms" },
|
||||
};
|
||||
|
||||
public OdfAuditLogFilter(IOdfAuditLogsService auditLogsService)
|
||||
{
|
||||
_auditLogsService = auditLogsService;
|
||||
}
|
||||
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
string oldData = null;
|
||||
string operationType = null;
|
||||
string tableName = null;
|
||||
int? recordId = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 从 [Log] 特性获取 BusinessType
|
||||
var businessType = GetBusinessType(context);
|
||||
operationType = businessType switch
|
||||
{
|
||||
BusinessType.INSERT => "INSERT",
|
||||
BusinessType.UPDATE => "UPDATE",
|
||||
BusinessType.DELETE => "DELETE",
|
||||
_ => null
|
||||
};
|
||||
|
||||
// 仅拦截增删改操作
|
||||
if (operationType == null)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取表名
|
||||
tableName = GetTableName(context);
|
||||
|
||||
// 获取记录 ID(从路由参数或请求体)
|
||||
recordId = GetRecordId(context);
|
||||
|
||||
// 操作前:对于 UPDATE/DELETE,尝试读取现有数据作为 OldData
|
||||
if ((operationType == "UPDATE" || operationType == "DELETE") && recordId.HasValue && recordId.Value > 0)
|
||||
{
|
||||
oldData = await GetExistingDataJson(tableName, recordId.Value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "审计日志:操作前记录数据失败");
|
||||
}
|
||||
|
||||
// 执行实际的 Action
|
||||
var resultContext = await next();
|
||||
|
||||
// 操作后:记录审计日志
|
||||
try
|
||||
{
|
||||
// 如果 Action 执行出错,不记录审计日志
|
||||
if (resultContext.Exception != null && !resultContext.ExceptionHandled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (operationType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取新数据
|
||||
string newData = null;
|
||||
if (operationType == "INSERT" || operationType == "UPDATE")
|
||||
{
|
||||
newData = GetNewDataFromActionArguments(context);
|
||||
}
|
||||
|
||||
// 对于 INSERT,尝试从返回结果获取新记录 ID
|
||||
if (operationType == "INSERT" && (!recordId.HasValue || recordId.Value <= 0))
|
||||
{
|
||||
recordId = GetRecordIdFromResult(resultContext);
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
var operatorId = context.HttpContext.GetUId();
|
||||
var operatorName = context.HttpContext.GetName();
|
||||
var deptId = context.HttpContext.GetDeptId();
|
||||
|
||||
// 判断操作来源
|
||||
var sourceClient = GetSourceClient(context.HttpContext);
|
||||
|
||||
var auditLog = new OdfAuditLogs
|
||||
{
|
||||
TableName = tableName ?? "unknown",
|
||||
RecordId = recordId,
|
||||
OperationType = operationType,
|
||||
OperatorId = operatorId,
|
||||
OperatorName = operatorName,
|
||||
SourceClient = sourceClient,
|
||||
OldData = oldData,
|
||||
NewData = newData,
|
||||
OperationTime = DateTime.Now,
|
||||
DeptId = deptId > 0 ? deptId : null,
|
||||
};
|
||||
|
||||
_auditLogsService.AddLog(auditLog);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 审计日志写入失败不影响主业务
|
||||
logger.Error(ex, "审计日志:写入审计日志失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 [Log] 特性获取 BusinessType
|
||||
/// </summary>
|
||||
private static BusinessType? GetBusinessType(ActionExecutingContext context)
|
||||
{
|
||||
if (context.ActionDescriptor is not ControllerActionDescriptor descriptor)
|
||||
return null;
|
||||
|
||||
var logAttr = descriptor.MethodInfo
|
||||
.GetCustomAttributes(inherit: true)
|
||||
.OfType<LogAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
return logAttr?.BusinessType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从控制器名称映射到数据库表名
|
||||
/// </summary>
|
||||
private static string GetTableName(ActionExecutingContext context)
|
||||
{
|
||||
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
|
||||
{
|
||||
var controllerName = descriptor.ControllerName;
|
||||
if (ControllerTableMap.TryGetValue(controllerName, out var table))
|
||||
{
|
||||
return table;
|
||||
}
|
||||
// 回退:使用控制器名称
|
||||
return controllerName;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从路由参数或请求体中提取记录 ID
|
||||
/// </summary>
|
||||
private static int? GetRecordId(ActionExecutingContext context)
|
||||
{
|
||||
// 尝试从路由参数获取 id
|
||||
if (context.ActionArguments.TryGetValue("id", out var idObj))
|
||||
{
|
||||
if (idObj is int intId) return intId;
|
||||
if (int.TryParse(idObj?.ToString(), out var parsed)) return parsed;
|
||||
}
|
||||
|
||||
// 尝试从路由参数获取 ids(批量删除场景,取第一个)
|
||||
if (context.ActionArguments.TryGetValue("ids", out var idsObj) && idsObj is string idsStr)
|
||||
{
|
||||
var first = idsStr.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||
if (int.TryParse(first?.Trim(), out var firstId)) return firstId;
|
||||
}
|
||||
|
||||
// 尝试从请求体 DTO 中获取 Id 属性
|
||||
foreach (var arg in context.ActionArguments.Values)
|
||||
{
|
||||
if (arg == null) continue;
|
||||
var idProp = arg.GetType().GetProperty("Id");
|
||||
if (idProp != null && idProp.PropertyType == typeof(int))
|
||||
{
|
||||
var val = (int)(idProp.GetValue(arg) ?? 0);
|
||||
if (val > 0) return val;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Action 参数中获取新数据(请求体 DTO 序列化为 JSON)
|
||||
/// </summary>
|
||||
private static string GetNewDataFromActionArguments(ActionExecutingContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 查找请求体中的 DTO 对象(排除简单类型)
|
||||
foreach (var arg in context.ActionArguments)
|
||||
{
|
||||
if (arg.Value == null) continue;
|
||||
var type = arg.Value.GetType();
|
||||
if (type.IsClass && type != typeof(string) && !type.IsPrimitive)
|
||||
{
|
||||
return JsonSerializer.Serialize(arg.Value, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn(ex, "审计日志:序列化新数据失败");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Action 返回结果中尝试获取新记录 ID
|
||||
/// </summary>
|
||||
private static int? GetRecordIdFromResult(ActionExecutedContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.Result is ObjectResult objResult && objResult.Value != null)
|
||||
{
|
||||
// 尝试从返回对象中获取 data 或 result 属性
|
||||
var resultType = objResult.Value.GetType();
|
||||
var dataProp = resultType.GetProperty("Data") ?? resultType.GetProperty("data");
|
||||
if (dataProp != null)
|
||||
{
|
||||
var dataVal = dataProp.GetValue(objResult.Value);
|
||||
if (dataVal is int intVal && intVal > 0) return intVal;
|
||||
if (int.TryParse(dataVal?.ToString(), out var parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断操作来源:App 端或管理后台端
|
||||
/// 优先从自定义 Header "X-Source-Client" 判断,
|
||||
/// 其次从 User-Agent 判断(包含 uni-app 或移动端标识则为 App)
|
||||
/// </summary>
|
||||
private static string GetSourceClient(HttpContext httpContext)
|
||||
{
|
||||
// 优先检查自定义 Header
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Source-Client", out var sourceHeader))
|
||||
{
|
||||
var val = sourceHeader.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
return val.Equals("App", StringComparison.OrdinalIgnoreCase) ? "App" : "Admin";
|
||||
}
|
||||
}
|
||||
|
||||
// 从 User-Agent 判断
|
||||
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
|
||||
if (!string.IsNullOrEmpty(userAgent))
|
||||
{
|
||||
if (userAgent.Contains("uni-app", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("Android", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("iPhone", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("iPad", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "App";
|
||||
}
|
||||
}
|
||||
|
||||
// 默认为管理后台
|
||||
return "Admin";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询现有记录数据并序列化为 JSON(用于 OldData)
|
||||
/// 通过 SqlSugar 直接查询对应表
|
||||
/// </summary>
|
||||
private async Task<string> GetExistingDataJson(string tableName, int recordId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = App.GetService<ISqlSugarClient>();
|
||||
if (db == null) return null;
|
||||
|
||||
var data = await db.Queryable<dynamic>()
|
||||
.AS(tableName)
|
||||
.Where("Id = @id", new { id = recordId })
|
||||
.FirstAsync();
|
||||
|
||||
if (data == null) return null;
|
||||
|
||||
return JsonSerializer.Serialize(data, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn(ex, $"审计日志:查询表 {tableName} 记录 {recordId} 的旧数据失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,8 @@ builder.Services.AddJwt();
|
|||
builder.Services.AddSingleton(new AppSettings(builder.Configuration));
|
||||
//app服务注册
|
||||
builder.Services.AddAppService();
|
||||
// 注册 ODF 审计日志 Filter(ServiceFilter 需要 DI 注入)
|
||||
builder.Services.AddScoped<ZR.Admin.WebApi.Filters.OdfAuditLogFilter>();
|
||||
var wxMp = builder.Configuration.GetSection("WxMp");
|
||||
var mp_appid = wxMp.GetSection("AppID").Value ?? "";//默认值
|
||||
var mp_appSecret = wxMp.GetSection("AppSecret").Value ?? "";//默认值
|
||||
|
|
|
|||
28
server/ZR.Model/Business/Dto/OdfAuditLogsDto.cs
Normal file
28
server/ZR.Model/Business/Dto/OdfAuditLogsDto.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace ZR.Model.Business.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志查询对象
|
||||
/// </summary>
|
||||
public class OdfAuditLogsQueryDto : PagerInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询时间范围 - 开始
|
||||
/// </summary>
|
||||
public DateTime? BeginTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询时间范围 - 结束
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作类型筛选: INSERT/UPDATE/DELETE
|
||||
/// </summary>
|
||||
public string? OperationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表名筛选
|
||||
/// </summary>
|
||||
public string? TableName { get; set; }
|
||||
}
|
||||
}
|
||||
121
server/ZR.Model/Business/Dto/OdfMarkerPolesDto.cs
Normal file
121
server/ZR.Model/Business/Dto/OdfMarkerPolesDto.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
|
||||
namespace ZR.Model.Business.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌查询对象
|
||||
/// </summary>
|
||||
public class OdfMarkerPolesQueryDto : PagerInfo
|
||||
{
|
||||
public int? CableId { get; set; }
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌新增对象
|
||||
/// </summary>
|
||||
public class OdfMarkerPoleAddDto
|
||||
{
|
||||
public int CableId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string RecordTime { get; set; }
|
||||
|
||||
public string? Personnel { get; set; }
|
||||
|
||||
public decimal Latitude { get; set; }
|
||||
|
||||
public decimal Longitude { get; set; }
|
||||
|
||||
public string? ActualMileage { get; set; }
|
||||
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// COS 图片访问 URL 列表(前端直传 COS 后传入)
|
||||
/// </summary>
|
||||
public string[] ImageUrls { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌编辑对象
|
||||
/// </summary>
|
||||
public class OdfMarkerPoleEditDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string? Personnel { get; set; }
|
||||
|
||||
public decimal Latitude { get; set; }
|
||||
|
||||
public decimal Longitude { get; set; }
|
||||
|
||||
public string? ActualMileage { get; set; }
|
||||
|
||||
public int CableId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌列表返回对象
|
||||
/// </summary>
|
||||
public class OdfMarkerPoleListDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CableId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public DateTime RecordTime { get; set; }
|
||||
|
||||
public string? Personnel { get; set; }
|
||||
|
||||
public decimal Latitude { get; set; }
|
||||
|
||||
public decimal Longitude { get; set; }
|
||||
|
||||
public string? ActualMileage { get; set; }
|
||||
|
||||
public string? DeptName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌详情返回对象
|
||||
/// </summary>
|
||||
public class OdfMarkerPoleDetailDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CableId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public DateTime RecordTime { get; set; }
|
||||
|
||||
public string? Personnel { get; set; }
|
||||
|
||||
public decimal Latitude { get; set; }
|
||||
|
||||
public decimal Longitude { get; set; }
|
||||
|
||||
public string? ActualMileage { get; set; }
|
||||
|
||||
public long DeptId { get; set; }
|
||||
|
||||
public string? DeptName { get; set; }
|
||||
|
||||
public long? UserId { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片 URL 列表
|
||||
/// </summary>
|
||||
public List<string> ImageUrls { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
72
server/ZR.Model/Business/OdfAuditLogs.cs
Normal file
72
server/ZR.Model/Business/OdfAuditLogs.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
namespace ZR.Model.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志
|
||||
/// </summary>
|
||||
[SugarTable("odf_audit_logs")]
|
||||
public class OdfAuditLogs
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作的表名
|
||||
/// </summary>
|
||||
public string TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作的记录ID
|
||||
/// </summary>
|
||||
public int? RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作类型: INSERT/UPDATE/DELETE
|
||||
/// </summary>
|
||||
public string OperationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人用户ID
|
||||
/// </summary>
|
||||
public long OperatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人用户名
|
||||
/// </summary>
|
||||
public string? OperatorName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作来源: App/Admin
|
||||
/// </summary>
|
||||
public string SourceClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改前数据 (JSON)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "nvarchar(max)")]
|
||||
public string? OldData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改后数据 (JSON)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "nvarchar(max)")]
|
||||
public string? NewData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作时间
|
||||
/// </summary>
|
||||
public DateTime? OperationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人所属部门
|
||||
/// </summary>
|
||||
public long? DeptId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
}
|
||||
30
server/ZR.Model/Business/OdfMarkerPoleImages.cs
Normal file
30
server/ZR.Model/Business/OdfMarkerPoleImages.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
namespace ZR.Model.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌图片
|
||||
/// </summary>
|
||||
[SugarTable("odf_marker_pole_images")]
|
||||
public class OdfMarkerPoleImages
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联标石ID
|
||||
/// </summary>
|
||||
public int MarkerPoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片访问URL
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
75
server/ZR.Model/Business/OdfMarkerPoles.cs
Normal file
75
server/ZR.Model/Business/OdfMarkerPoles.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
namespace ZR.Model.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌
|
||||
/// </summary>
|
||||
[SugarTable("odf_marker_poles")]
|
||||
public class OdfMarkerPoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联光缆ID
|
||||
/// </summary>
|
||||
public int CableId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标石/杆号牌名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录时间(拍照时间)
|
||||
/// </summary>
|
||||
public DateTime RecordTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 责任人
|
||||
/// </summary>
|
||||
public string? Personnel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 纬度
|
||||
/// </summary>
|
||||
public decimal Latitude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 经度
|
||||
/// </summary>
|
||||
public decimal Longitude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实际里程
|
||||
/// </summary>
|
||||
public string? ActualMileage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属公司/部门ID
|
||||
/// </summary>
|
||||
public long DeptId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 部门名称(冗余)
|
||||
/// </summary>
|
||||
public string? DeptName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交人用户ID
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
39
server/ZR.Service/Business/DeptDataScopeHelper.cs
Normal file
39
server/ZR.Service/Business/DeptDataScopeHelper.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using SqlSugar;
|
||||
using ZR.Model.System;
|
||||
|
||||
namespace ZR.Service.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 部门数据隔离工具类
|
||||
/// 根据 DeptId 递归查询本级及所有下级部门 ID 列表
|
||||
/// </summary>
|
||||
public static class DeptDataScopeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的部门ID列表(本级 + 所有下级)
|
||||
/// </summary>
|
||||
/// <param name="db">SqlSugar 客户端实例</param>
|
||||
/// <param name="deptId">当前用户所属部门ID</param>
|
||||
/// <returns>可见部门ID列表</returns>
|
||||
public static List<long> GetVisibleDeptIds(ISqlSugarClient db, long deptId)
|
||||
{
|
||||
var allDepts = db.Queryable<SysDept>().ToList();
|
||||
var result = new List<long> { deptId };
|
||||
CollectChildDeptIds(allDepts, deptId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归收集所有下级部门ID
|
||||
/// </summary>
|
||||
private static void CollectChildDeptIds(List<SysDept> all, long parentId, List<long> result)
|
||||
{
|
||||
var children = all.Where(d => d.ParentId == parentId).ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
result.Add(child.DeptId);
|
||||
CollectChildDeptIds(all, child.DeptId, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using ZR.Model;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
|
||||
namespace ZR.Service.Business.IBusinessService
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志service接口
|
||||
/// </summary>
|
||||
public interface IOdfAuditLogsService : IBaseService<OdfAuditLogs>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询审计日志列表,支持时间范围、操作类型、表名筛选
|
||||
/// </summary>
|
||||
PagedInfo<OdfAuditLogs> GetList(OdfAuditLogsQueryDto parm);
|
||||
|
||||
/// <summary>
|
||||
/// 写入审计日志记录
|
||||
/// </summary>
|
||||
int AddLog(OdfAuditLogs log);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,9 @@ namespace ZR.Service.Business.IBusinessService
|
|||
public interface IOdfCableFaultsService : IBaseService<OdfCableFaults>
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序
|
||||
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序,应用部门数据隔离
|
||||
/// </summary>
|
||||
PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm);
|
||||
PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm, long deptId);
|
||||
|
||||
/// <summary>
|
||||
/// 查询故障详情,联查光缆名称和图片列表
|
||||
|
|
@ -32,7 +32,7 @@ namespace ZR.Service.Business.IBusinessService
|
|||
/// <summary>
|
||||
/// 导出故障列表
|
||||
/// </summary>
|
||||
List<OdfCableFaultExportDto> ExportList(OdfCableFaultsQueryDto parm);
|
||||
List<OdfCableFaultExportDto> ExportList(OdfCableFaultsQueryDto parm, long deptId);
|
||||
|
||||
/// <summary>
|
||||
/// 批量导入故障
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ namespace ZR.Service.Business.IBusinessService
|
|||
public interface IOdfCablesService : IBaseService<OdfCables>
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询)
|
||||
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询),应用部门数据隔离
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <param name="deptId">当前用户所属部门ID,用于数据隔离</param>
|
||||
/// <returns></returns>
|
||||
PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm);
|
||||
PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm, long deptId);
|
||||
|
||||
/// <summary>
|
||||
/// 在指定公司范围内搜索光缆和故障
|
||||
|
|
@ -56,7 +57,8 @@ namespace ZR.Service.Business.IBusinessService
|
|||
/// 导出光缆列表
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <param name="deptId">当前用户所属部门ID,用于数据隔离</param>
|
||||
/// <returns></returns>
|
||||
PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm);
|
||||
PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm, long deptId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using ZR.Model.Business;
|
||||
|
||||
namespace ZR.Service.Business.IBusinessService
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌图片service接口
|
||||
/// </summary>
|
||||
public interface IOdfMarkerPoleImagesService : IBaseService<OdfMarkerPoleImages>
|
||||
{
|
||||
/// <summary>
|
||||
/// 按标石 ID 查询图片列表
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <returns></returns>
|
||||
List<OdfMarkerPoleImages> GetByMarkerPoleId(int markerPoleId);
|
||||
|
||||
/// <summary>
|
||||
/// 批量插入图片记录
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <param name="imageUrls"></param>
|
||||
/// <returns></returns>
|
||||
int BatchInsert(int markerPoleId, string[] imageUrls);
|
||||
|
||||
/// <summary>
|
||||
/// 按标石 ID 删除所有图片记录
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <returns></returns>
|
||||
int DeleteByMarkerPoleId(int markerPoleId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using ZR.Model;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
|
||||
namespace ZR.Service.Business.IBusinessService
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌service接口
|
||||
/// </summary>
|
||||
public interface IOdfMarkerPolesService : IBaseService<OdfMarkerPoles>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询标石/杆号牌列表,支持按 CableId 过滤,应用部门数据隔离
|
||||
/// </summary>
|
||||
PagedInfo<OdfMarkerPoleListDto> GetList(OdfMarkerPolesQueryDto parm, long deptId);
|
||||
|
||||
/// <summary>
|
||||
/// 查询标石/杆号牌详情,含图片 URL 列表
|
||||
/// </summary>
|
||||
OdfMarkerPoleDetailDto GetDetail(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 新增标石/杆号牌(含图片记录),自动填充 DeptId/DeptName
|
||||
/// </summary>
|
||||
int Add(OdfMarkerPoleAddDto dto, long deptId);
|
||||
|
||||
/// <summary>
|
||||
/// 编辑标石/杆号牌
|
||||
/// </summary>
|
||||
int Update(OdfMarkerPoleEditDto dto);
|
||||
|
||||
/// <summary>
|
||||
/// 删除标石/杆号牌及关联图片
|
||||
/// </summary>
|
||||
int Delete(int id);
|
||||
}
|
||||
}
|
||||
52
server/ZR.Service/Business/OdfAuditLogsService.cs
Normal file
52
server/ZR.Service/Business/OdfAuditLogsService.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Infrastructure.Attribute;
|
||||
using ZR.Model;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Repository;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
namespace ZR.Service.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志Service业务层处理
|
||||
/// </summary>
|
||||
[AppService(ServiceType = typeof(IOdfAuditLogsService), ServiceLifetime = LifeTime.Transient)]
|
||||
public class OdfAuditLogsService : BaseService<OdfAuditLogs>, IOdfAuditLogsService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询审计日志列表,支持时间范围、操作类型、表名筛选
|
||||
/// </summary>
|
||||
public PagedInfo<OdfAuditLogs> GetList(OdfAuditLogsQueryDto parm)
|
||||
{
|
||||
var predicate = Expressionable.Create<OdfAuditLogs>();
|
||||
|
||||
predicate = predicate.AndIF(parm.BeginTime != null, it => it.OperationTime >= parm.BeginTime);
|
||||
predicate = predicate.AndIF(parm.EndTime != null, it => it.OperationTime <= parm.EndTime);
|
||||
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.OperationType), it => it.OperationType == parm.OperationType);
|
||||
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.TableName), it => it.TableName == parm.TableName);
|
||||
|
||||
var total = 0;
|
||||
var list = Queryable()
|
||||
.Where(predicate.ToExpression())
|
||||
.OrderByDescending(it => it.OperationTime)
|
||||
.ToPageList(parm.PageNum, parm.PageSize, ref total);
|
||||
|
||||
return new PagedInfo<OdfAuditLogs>
|
||||
{
|
||||
Result = list,
|
||||
TotalNum = total,
|
||||
PageIndex = parm.PageNum,
|
||||
PageSize = parm.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入审计日志记录
|
||||
/// </summary>
|
||||
public int AddLog(OdfAuditLogs log)
|
||||
{
|
||||
log.OperationTime ??= DateTime.Now;
|
||||
return Insert(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,16 +16,20 @@ namespace ZR.Service.Business
|
|||
public class OdfCableFaultsService : BaseService<OdfCableFaults>, IOdfCableFaultsService
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序
|
||||
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序,应用部门数据隔离
|
||||
/// </summary>
|
||||
public PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm)
|
||||
public PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm, long deptId)
|
||||
{
|
||||
var predicate = QueryExp(parm);
|
||||
|
||||
// 部门数据隔离:通过关联光缆的 DeptId 过滤
|
||||
var visibleDeptIds = DeptDataScopeHelper.GetVisibleDeptIds(Context, deptId);
|
||||
|
||||
var total = 0;
|
||||
var list = Queryable()
|
||||
.Where(predicate.ToExpression())
|
||||
.LeftJoin<OdfCables>((f, c) => f.CableId == c.Id)
|
||||
.Where((f, c) => visibleDeptIds.Contains(c.DeptId))
|
||||
.Select((f, c) => new
|
||||
{
|
||||
f.Id,
|
||||
|
|
@ -234,13 +238,17 @@ namespace ZR.Service.Business
|
|||
/// <summary>
|
||||
/// 导出故障列表
|
||||
/// </summary>
|
||||
public List<OdfCableFaultExportDto> ExportList(OdfCableFaultsQueryDto parm)
|
||||
public List<OdfCableFaultExportDto> ExportList(OdfCableFaultsQueryDto parm, long deptId)
|
||||
{
|
||||
var predicate = QueryExp(parm);
|
||||
|
||||
// 部门数据隔离:通过关联光缆的 DeptId 过滤
|
||||
var visibleDeptIds = DeptDataScopeHelper.GetVisibleDeptIds(Context, deptId);
|
||||
|
||||
return Queryable()
|
||||
.Where(predicate.ToExpression())
|
||||
.LeftJoin<OdfCables>((f, c) => f.CableId == c.Id)
|
||||
.Where((f, c) => visibleDeptIds.Contains(c.DeptId))
|
||||
.Select((f, c) => new OdfCableFaultExportDto
|
||||
{
|
||||
Id = f.Id,
|
||||
|
|
|
|||
|
|
@ -14,15 +14,19 @@ namespace ZR.Service.Business
|
|||
public class OdfCablesService : BaseService<OdfCables>, IOdfCablesService
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询)
|
||||
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询),应用部门数据隔离
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <param name="deptId">当前用户所属部门ID,用于数据隔离</param>
|
||||
/// <returns></returns>
|
||||
public PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm)
|
||||
public PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm, long deptId)
|
||||
{
|
||||
var predicate = Expressionable.Create<OdfCables>();
|
||||
|
||||
predicate = predicate.AndIF(parm.DeptId != null, it => it.DeptId == parm.DeptId);
|
||||
// 部门数据隔离:查询本级及所有下级部门的数据
|
||||
var visibleDeptIds = DeptDataScopeHelper.GetVisibleDeptIds(Context, deptId);
|
||||
predicate = predicate.And(it => visibleDeptIds.Contains(it.DeptId));
|
||||
|
||||
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.CableName), it => it.CableName.Contains(parm.CableName));
|
||||
|
||||
var response = Queryable()
|
||||
|
|
@ -34,7 +38,7 @@ namespace ZR.Service.Business
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定公司范围内搜索光缆和故障
|
||||
/// 在指定公司范围内搜索光缆、故障和标石/杆号牌
|
||||
/// </summary>
|
||||
/// <param name="deptId"></param>
|
||||
/// <param name="keyword"></param>
|
||||
|
|
@ -67,7 +71,24 @@ namespace ZR.Service.Business
|
|||
})
|
||||
.ToList();
|
||||
|
||||
return new { cables, faults };
|
||||
// 搜索标石/杆号牌:联查 odf_cables 按 DeptId 过滤,按 Name LIKE keyword
|
||||
var markerPoles = Context.Queryable<OdfMarkerPoles>()
|
||||
.LeftJoin<OdfCables>((m, c) => m.CableId == c.Id)
|
||||
.Where((m, c) => c.DeptId == deptId)
|
||||
.WhereIF(!string.IsNullOrEmpty(keyword), (m, c) => m.Name.Contains(keyword))
|
||||
.OrderByDescending((m, c) => m.RecordTime)
|
||||
.Select((m, c) => new
|
||||
{
|
||||
m.Id,
|
||||
m.Name,
|
||||
m.RecordTime,
|
||||
m.Personnel,
|
||||
m.CableId,
|
||||
CableName = c.CableName
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new { cables, faults, markerPoles };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,12 +138,13 @@ namespace ZR.Service.Business
|
|||
/// 导出光缆列表
|
||||
/// </summary>
|
||||
/// <param name="parm"></param>
|
||||
/// <param name="deptId">当前用户所属部门ID,用于数据隔离</param>
|
||||
/// <returns></returns>
|
||||
public PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm)
|
||||
public PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm, long deptId)
|
||||
{
|
||||
parm.PageNum = 1;
|
||||
parm.PageSize = 100000;
|
||||
return GetList(parm);
|
||||
return GetList(parm, deptId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
server/ZR.Service/Business/OdfMarkerPoleImagesService.cs
Normal file
64
server/ZR.Service/Business/OdfMarkerPoleImagesService.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using Infrastructure.Attribute;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Repository;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
namespace ZR.Service.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌图片Service业务层处理
|
||||
/// </summary>
|
||||
[AppService(ServiceType = typeof(IOdfMarkerPoleImagesService), ServiceLifetime = LifeTime.Transient)]
|
||||
public class OdfMarkerPoleImagesService : BaseService<OdfMarkerPoleImages>, IOdfMarkerPoleImagesService
|
||||
{
|
||||
/// <summary>
|
||||
/// 按标石 ID 查询图片列表
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <returns></returns>
|
||||
public List<OdfMarkerPoleImages> GetByMarkerPoleId(int markerPoleId)
|
||||
{
|
||||
return Queryable()
|
||||
.Where(x => x.MarkerPoleId == markerPoleId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量插入图片记录
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <param name="imageUrls"></param>
|
||||
/// <returns></returns>
|
||||
public int BatchInsert(int markerPoleId, string[] imageUrls)
|
||||
{
|
||||
if (imageUrls == null || imageUrls.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var list = imageUrls
|
||||
.Where(url => !string.IsNullOrWhiteSpace(url))
|
||||
.Select(url => new OdfMarkerPoleImages
|
||||
{
|
||||
MarkerPoleId = markerPoleId,
|
||||
ImageUrl = url,
|
||||
CreatedAt = DateTime.Now
|
||||
}).ToList();
|
||||
|
||||
return Insert(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按标石 ID 删除所有图片记录
|
||||
/// </summary>
|
||||
/// <param name="markerPoleId"></param>
|
||||
/// <returns></returns>
|
||||
public int DeleteByMarkerPoleId(int markerPoleId)
|
||||
{
|
||||
return Deleteable()
|
||||
.Where(x => x.MarkerPoleId == markerPoleId)
|
||||
.ExecuteCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
192
server/ZR.Service/Business/OdfMarkerPolesService.cs
Normal file
192
server/ZR.Service/Business/OdfMarkerPolesService.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
using Infrastructure;
|
||||
using Infrastructure.Attribute;
|
||||
using ZR.Model;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
using ZR.Model.System;
|
||||
using ZR.Repository;
|
||||
using ZR.Service.Business.IBusinessService;
|
||||
|
||||
namespace ZR.Service.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// 标石/杆号牌Service业务层处理
|
||||
/// </summary>
|
||||
[AppService(ServiceType = typeof(IOdfMarkerPolesService), ServiceLifetime = LifeTime.Transient)]
|
||||
public class OdfMarkerPolesService : BaseService<OdfMarkerPoles>, IOdfMarkerPolesService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询标石/杆号牌列表,应用部门数据隔离
|
||||
/// </summary>
|
||||
public PagedInfo<OdfMarkerPoleListDto> GetList(OdfMarkerPolesQueryDto parm, long deptId)
|
||||
{
|
||||
var predicate = Expressionable.Create<OdfMarkerPoles>();
|
||||
|
||||
// 部门数据隔离
|
||||
var visibleDeptIds = DeptDataScopeHelper.GetVisibleDeptIds(Context, deptId);
|
||||
predicate = predicate.And(it => visibleDeptIds.Contains(it.DeptId));
|
||||
|
||||
// 按 CableId 过滤
|
||||
predicate = predicate.AndIF(parm.CableId != null, it => it.CableId == parm.CableId);
|
||||
|
||||
var total = 0;
|
||||
var list = Queryable()
|
||||
.Where(predicate.ToExpression())
|
||||
.OrderByDescending(it => it.RecordTime)
|
||||
.ToPageList(parm.PageNum, parm.PageSize, ref total);
|
||||
|
||||
var result = list.Select(it => new OdfMarkerPoleListDto
|
||||
{
|
||||
Id = it.Id,
|
||||
CableId = it.CableId,
|
||||
Name = it.Name,
|
||||
RecordTime = it.RecordTime,
|
||||
Personnel = it.Personnel,
|
||||
Latitude = it.Latitude,
|
||||
Longitude = it.Longitude,
|
||||
ActualMileage = it.ActualMileage,
|
||||
DeptName = it.DeptName
|
||||
}).ToList();
|
||||
|
||||
return new PagedInfo<OdfMarkerPoleListDto>
|
||||
{
|
||||
Result = result,
|
||||
TotalNum = total,
|
||||
PageIndex = parm.PageNum,
|
||||
PageSize = parm.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询标石/杆号牌详情,含图片 URL 列表
|
||||
/// </summary>
|
||||
public OdfMarkerPoleDetailDto GetDetail(int id)
|
||||
{
|
||||
var entity = GetFirst(it => it.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new CustomException("标石记录不存在");
|
||||
}
|
||||
|
||||
var images = Context.Queryable<OdfMarkerPoleImages>()
|
||||
.Where(img => img.MarkerPoleId == id)
|
||||
.OrderBy(img => img.Id)
|
||||
.Select(img => img.ImageUrl)
|
||||
.ToList();
|
||||
|
||||
return new OdfMarkerPoleDetailDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
CableId = entity.CableId,
|
||||
Name = entity.Name,
|
||||
RecordTime = entity.RecordTime,
|
||||
Personnel = entity.Personnel,
|
||||
Latitude = entity.Latitude,
|
||||
Longitude = entity.Longitude,
|
||||
ActualMileage = entity.ActualMileage,
|
||||
DeptId = entity.DeptId,
|
||||
DeptName = entity.DeptName,
|
||||
UserId = entity.UserId,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
ImageUrls = images
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增标石/杆号牌,自动填充 DeptId/DeptName
|
||||
/// </summary>
|
||||
public int Add(OdfMarkerPoleAddDto dto, long deptId)
|
||||
{
|
||||
// 校验 CableId 存在
|
||||
var cable = Context.Queryable<OdfCables>()
|
||||
.Where(c => c.Id == dto.CableId)
|
||||
.First();
|
||||
if (cable == null)
|
||||
{
|
||||
throw new CustomException("光缆不存在");
|
||||
}
|
||||
|
||||
// 校验至少 1 张图片
|
||||
if (dto.ImageUrls == null || dto.ImageUrls.Length == 0)
|
||||
{
|
||||
throw new CustomException("请至少上传一张图片");
|
||||
}
|
||||
|
||||
// 查询部门名称
|
||||
var dept = Context.Queryable<SysDept>()
|
||||
.Where(d => d.DeptId == deptId)
|
||||
.First();
|
||||
|
||||
var model = new OdfMarkerPoles
|
||||
{
|
||||
CableId = dto.CableId,
|
||||
Name = dto.Name,
|
||||
RecordTime = DateTime.Parse(dto.RecordTime),
|
||||
Personnel = dto.Personnel,
|
||||
Latitude = dto.Latitude,
|
||||
Longitude = dto.Longitude,
|
||||
ActualMileage = dto.ActualMileage,
|
||||
DeptId = deptId,
|
||||
DeptName = dept?.DeptName,
|
||||
UserId = dto.UserId,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
var entity = Insertable(model).ExecuteReturnEntity();
|
||||
|
||||
// 插入图片记录
|
||||
foreach (var imageUrl in dto.ImageUrls)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageUrl)) continue;
|
||||
|
||||
var imageRecord = new OdfMarkerPoleImages
|
||||
{
|
||||
MarkerPoleId = entity.Id,
|
||||
ImageUrl = imageUrl,
|
||||
CreatedAt = DateTime.Now
|
||||
};
|
||||
Context.Insertable(imageRecord).ExecuteCommand();
|
||||
}
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑标石/杆号牌
|
||||
/// </summary>
|
||||
public int Update(OdfMarkerPoleEditDto dto)
|
||||
{
|
||||
var entity = GetFirst(it => it.Id == dto.Id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new CustomException("标石记录不存在");
|
||||
}
|
||||
|
||||
entity.Name = dto.Name;
|
||||
entity.Personnel = dto.Personnel;
|
||||
entity.Latitude = dto.Latitude;
|
||||
entity.Longitude = dto.Longitude;
|
||||
entity.ActualMileage = dto.ActualMileage;
|
||||
entity.CableId = dto.CableId;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
return base.Update(entity, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除标石/杆号牌及关联图片
|
||||
/// </summary>
|
||||
public int Delete(int id)
|
||||
{
|
||||
// 删除关联图片记录
|
||||
Context.Deleteable<OdfMarkerPoleImages>()
|
||||
.Where(img => img.MarkerPoleId == id)
|
||||
.ExecuteCommand();
|
||||
|
||||
// 删除标石记录
|
||||
return base.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
server/ZR.Vue/src/api/business/odfauditlogs.js
Normal file
13
server/ZR.Vue/src/api/business/odfauditlogs.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 审计日志列表分页查询
|
||||
* @param {查询条件} query
|
||||
*/
|
||||
export function listOdfAuditLogs(query) {
|
||||
return request({
|
||||
url: 'business/OdfAuditLogs/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
59
server/ZR.Vue/src/api/business/odfmarkerpoles.js
Normal file
59
server/ZR.Vue/src/api/business/odfmarkerpoles.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 标石/杆号牌列表分页查询
|
||||
* @param {查询条件} query
|
||||
*/
|
||||
export function listOdfMarkerPoles(query) {
|
||||
return request({
|
||||
url: 'business/OdfMarkerPoles/list',
|
||||
method: 'get',
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标石/杆号牌详情
|
||||
* @param {Id}
|
||||
*/
|
||||
export function getOdfMarkerPoles(id) {
|
||||
return request({
|
||||
url: 'business/OdfMarkerPoles/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增标石/杆号牌
|
||||
* @param data
|
||||
*/
|
||||
export function addOdfMarkerPoles(data) {
|
||||
return request({
|
||||
url: 'business/OdfMarkerPoles/add',
|
||||
method: 'post',
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改标石/杆号牌
|
||||
* @param data
|
||||
*/
|
||||
export function updateOdfMarkerPoles(data) {
|
||||
return request({
|
||||
url: 'business/OdfMarkerPoles/edit',
|
||||
method: 'put',
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除标石/杆号牌
|
||||
* @param {主键} pid
|
||||
*/
|
||||
export function delOdfMarkerPoles(pid) {
|
||||
return request({
|
||||
url: 'business/OdfMarkerPoles/delete/' + pid,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ service.interceptors.response.use(
|
|||
})
|
||||
|
||||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
||||
} else if (code == 0 || code == 1 || code == 110 || code == 101 || code == 403 || code == 500 || code == 429) {
|
||||
} else if (code == 0 || code == 1 || code == 110 || code == 101 || code == 105 || code == 403 || code == 500 || code == 429) {
|
||||
ElMessage({
|
||||
message: msg,
|
||||
type: 'error'
|
||||
|
|
|
|||
216
server/ZR.Vue/src/views/business/OdfAuditLogs.vue
Normal file
216
server/ZR.Vue/src/views/business/OdfAuditLogs.vue
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<!--
|
||||
* @Descripttion: (审计日志/odf_audit_logs)
|
||||
* @Author: (admin)
|
||||
* @Date: (2025-08-05)
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
|
||||
<el-form-item label="操作类型" prop="operationType">
|
||||
<el-select v-model="queryParams.operationType" placeholder="全部" clearable style="width: 150px;">
|
||||
<el-option label="新增" value="INSERT" />
|
||||
<el-option label="修改" value="UPDATE" />
|
||||
<el-option label="删除" value="DELETE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围" prop="dateRange">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
|
||||
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 工具区域 -->
|
||||
<el-row :gutter="15" class="mb10">
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
ref="table"
|
||||
border
|
||||
header-cell-class-name="el-table-header-cell"
|
||||
highlight-current-row
|
||||
@sort-change="sortChange">
|
||||
<el-table-column prop="id" label="Id" align="center" width="80" v-if="columns.showColumn('id')" />
|
||||
<el-table-column prop="operatorName" label="操作人" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('operatorName')" />
|
||||
<el-table-column prop="operationTime" label="操作时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('operationTime')" />
|
||||
<el-table-column prop="operationType" label="操作类型" align="center" width="100" v-if="columns.showColumn('operationType')">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.operationType === 'INSERT'" type="success">新增</el-tag>
|
||||
<el-tag v-else-if="scope.row.operationType === 'UPDATE'" type="warning">修改</el-tag>
|
||||
<el-tag v-else-if="scope.row.operationType === 'DELETE'" type="danger">删除</el-tag>
|
||||
<span v-else>{{ scope.row.operationType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sourceClient" label="操作来源" align="center" width="100" v-if="columns.showColumn('sourceClient')">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.sourceClient === 'App'" type="">App</el-tag>
|
||||
<el-tag v-else-if="scope.row.sourceClient === 'Admin'" type="info">管理后台</el-tag>
|
||||
<span v-else>{{ scope.row.sourceClient }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tableName" label="表名" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('tableName')" />
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="view"
|
||||
title="查看详情"
|
||||
@click="handleViewDetail(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 数据对比对话框 -->
|
||||
<el-dialog title="修改前后数据对比" v-model="detailVisible" width="800px" destroy-on-close>
|
||||
<el-descriptions :column="2" border class="mb10">
|
||||
<el-descriptions-item label="操作人">{{ detailRow.operatorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间">{{ detailRow.operationTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">{{ detailRow.operationType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作来源">{{ detailRow.sourceClient }}</el-descriptions-item>
|
||||
<el-descriptions-item label="表名">{{ detailRow.tableName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="记录ID">{{ detailRow.recordId }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="data-compare-title">修改前数据 (OldData)</div>
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
:model-value="formatJson(detailRow.oldData)"
|
||||
readonly />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="data-compare-title">修改后数据 (NewData)</div>
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
:model-value="formatJson(detailRow.newData)"
|
||||
readonly />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="odfauditlogs">
|
||||
import { listOdfAuditLogs } from '@/api/business/odfauditlogs.js'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const loading = ref(false)
|
||||
const showSearch = ref(true)
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref({})
|
||||
const dateRange = ref([])
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
sort: undefined,
|
||||
sortType: undefined,
|
||||
operationType: undefined,
|
||||
beginTime: undefined,
|
||||
endTime: undefined
|
||||
})
|
||||
|
||||
const columns = ref([
|
||||
{ visible: true, align: 'center', type: '', prop: 'id', label: 'Id' },
|
||||
{ visible: true, align: 'center', type: '', prop: 'operatorName', label: '操作人', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'operationTime', label: '操作时间', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'operationType', label: '操作类型' },
|
||||
{ visible: true, align: 'center', type: '', prop: 'sourceClient', label: '操作来源' },
|
||||
{ visible: true, align: 'center', type: '', prop: 'tableName', label: '表名', showOverflowTooltip: true }
|
||||
])
|
||||
|
||||
const total = ref(0)
|
||||
const dataList = ref([])
|
||||
const queryRef = ref()
|
||||
|
||||
function getList() {
|
||||
loading.value = true
|
||||
// 处理时间范围
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.beginTime = dateRange.value[0]
|
||||
queryParams.endTime = dateRange.value[1]
|
||||
} else {
|
||||
queryParams.beginTime = undefined
|
||||
queryParams.endTime = undefined
|
||||
}
|
||||
listOdfAuditLogs(queryParams).then((res) => {
|
||||
const { code, data } = res
|
||||
if (code == 200) {
|
||||
dataList.value = data.result
|
||||
total.value = data.totalNum
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查询
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置查询操作
|
||||
function resetQuery() {
|
||||
dateRange.value = []
|
||||
proxy.resetForm('queryRef')
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 自定义排序
|
||||
function sortChange(column) {
|
||||
var sort = undefined
|
||||
var sortType = undefined
|
||||
|
||||
if (column.prop != null && column.order != null) {
|
||||
sort = column.prop
|
||||
sortType = column.order
|
||||
}
|
||||
queryParams.sort = sort
|
||||
queryParams.sortType = sortType
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(row) {
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化 JSON 字符串
|
||||
function formatJson(jsonStr) {
|
||||
if (!jsonStr) return '(无数据)'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(jsonStr), null, 2)
|
||||
} catch {
|
||||
return jsonStr
|
||||
}
|
||||
}
|
||||
|
||||
handleQuery()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-compare-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
378
server/ZR.Vue/src/views/business/OdfMarkerPoles.vue
Normal file
378
server/ZR.Vue/src/views/business/OdfMarkerPoles.vue
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
<!--
|
||||
* @Descripttion: (标石/杆号牌管理/odf_marker_poles)
|
||||
* @Author: (admin)
|
||||
* @Date: (2025-08-05)
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
|
||||
<el-form-item label="名称" prop="keyword">
|
||||
<el-input v-model="queryParams.keyword" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属光缆" prop="cableId">
|
||||
<el-select
|
||||
v-model="queryParams.cableId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
placeholder="请输入光缆名称搜索"
|
||||
:remote-method="remoteCableSearch"
|
||||
:loading="cableSearchLoading">
|
||||
<el-option v-for="item in cableOptions" :key="item.id" :label="item.cableName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
|
||||
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 工具区域 -->
|
||||
<el-row :gutter="15" class="mb10">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" v-hasPermi="['odfmarkerpoles:add']" plain icon="plus" @click="handleAdd">
|
||||
{{ $t('btn.add') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" :disabled="single" v-hasPermi="['odfmarkerpoles:edit']" plain icon="edit" @click="handleUpdate">
|
||||
{{ $t('btn.edit') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" :disabled="multiple" v-hasPermi="['odfmarkerpoles:delete']" plain icon="delete" @click="handleDelete">
|
||||
{{ $t('btn.delete') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
ref="table"
|
||||
border
|
||||
header-cell-class-name="el-table-header-cell"
|
||||
highlight-current-row
|
||||
@sort-change="sortChange"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column prop="id" label="Id" align="center" v-if="columns.showColumn('id')" />
|
||||
<el-table-column prop="name" label="名称" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('name')" />
|
||||
<el-table-column prop="recordTime" label="时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('recordTime')" />
|
||||
<el-table-column prop="personnel" label="责任人" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('personnel')" />
|
||||
<el-table-column label="经纬度" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('longitude')">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.longitude || scope.row.latitude">{{ scope.row.longitude }}, {{ scope.row.latitude }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptName" label="所属公司" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('deptName')" />
|
||||
<el-table-column prop="cableName" label="所属光缆" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('cableName')" />
|
||||
<el-table-column prop="actualMileage" label="实际里程" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('actualMileage')" />
|
||||
<el-table-column label="操作" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
icon="edit"
|
||||
title="编辑"
|
||||
v-hasPermi="['odfmarkerpoles:edit']"
|
||||
@click="handleUpdate(scope.row)"></el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="delete"
|
||||
title="删除"
|
||||
v-hasPermi="['odfmarkerpoles:delete']"
|
||||
@click="handleDelete(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog :title="formTitle" v-model="formVisible" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="责任人" prop="personnel">
|
||||
<el-input v-model="form.personnel" placeholder="请输入责任人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经度" prop="longitude">
|
||||
<el-input-number v-model="form.longitude" :precision="7" :step="0.0000001" controls-position="right" placeholder="经度" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纬度" prop="latitude">
|
||||
<el-input-number v-model="form.latitude" :precision="7" :step="0.0000001" controls-position="right" placeholder="纬度" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属光缆" prop="cableId">
|
||||
<el-select
|
||||
v-model="form.cableId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
placeholder="请输入光缆名称搜索"
|
||||
:remote-method="remoteFormCableSearch"
|
||||
:loading="formCableSearchLoading"
|
||||
style="width: 100%;">
|
||||
<el-option v-for="item in formCableOptions" :key="item.id" :label="item.cableName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="实际里程" prop="actualMileage">
|
||||
<el-input v-model="form.actualMileage" placeholder="请输入实际里程" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitting">{{ $t('common.ok') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="odfmarkerpoles">
|
||||
import { listOdfMarkerPoles, getOdfMarkerPoles, addOdfMarkerPoles, updateOdfMarkerPoles, delOdfMarkerPoles } from '@/api/business/odfmarkerpoles.js'
|
||||
import { listOdfCables } from '@/api/business/odfcables.js'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const ids = ref([])
|
||||
const loading = ref(false)
|
||||
const showSearch = ref(true)
|
||||
const formVisible = ref(false)
|
||||
const formTitle = ref('')
|
||||
const submitting = ref(false)
|
||||
const cableOptions = ref([])
|
||||
const cableSearchLoading = ref(false)
|
||||
const formCableOptions = ref([])
|
||||
const formCableSearchLoading = ref(false)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
sort: undefined,
|
||||
sortType: undefined,
|
||||
keyword: undefined,
|
||||
cableId: undefined
|
||||
})
|
||||
|
||||
const columns = ref([
|
||||
{ visible: true, align: 'center', type: '', prop: 'id', label: 'Id' },
|
||||
{ visible: true, align: 'center', type: '', prop: 'name', label: '名称', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'recordTime', label: '时间', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'personnel', label: '责任人', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'longitude', label: '经纬度', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'deptName', label: '所属公司', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'cableName', label: '所属光缆', showOverflowTooltip: true },
|
||||
{ visible: true, align: 'center', type: '', prop: 'actualMileage', label: '实际里程', showOverflowTooltip: true }
|
||||
])
|
||||
|
||||
const total = ref(0)
|
||||
const dataList = ref([])
|
||||
const queryRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
const data = reactive({
|
||||
form: {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
personnel: undefined,
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
cableId: undefined,
|
||||
actualMileage: undefined
|
||||
},
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
cableId: [{ required: true, message: '请选择所属光缆', trigger: 'change' }]
|
||||
}
|
||||
})
|
||||
|
||||
const { form, rules } = toRefs(data)
|
||||
|
||||
const state = reactive({
|
||||
single: true,
|
||||
multiple: true
|
||||
})
|
||||
|
||||
const { single, multiple } = toRefs(state)
|
||||
|
||||
// 远程搜索光缆(查询区域)
|
||||
function remoteCableSearch(query) {
|
||||
if (query) {
|
||||
cableSearchLoading.value = true
|
||||
listOdfCables({ cableName: query, pageNum: 1, pageSize: 50 }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
cableOptions.value = res.data.result || []
|
||||
}
|
||||
cableSearchLoading.value = false
|
||||
}).catch(() => {
|
||||
cableSearchLoading.value = false
|
||||
})
|
||||
} else {
|
||||
cableOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 远程搜索光缆(表单区域)
|
||||
function remoteFormCableSearch(query) {
|
||||
if (query) {
|
||||
formCableSearchLoading.value = true
|
||||
listOdfCables({ cableName: query, pageNum: 1, pageSize: 50 }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
formCableOptions.value = res.data.result || []
|
||||
}
|
||||
formCableSearchLoading.value = false
|
||||
}).catch(() => {
|
||||
formCableSearchLoading.value = false
|
||||
})
|
||||
} else {
|
||||
formCableOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function getList() {
|
||||
loading.value = true
|
||||
listOdfMarkerPoles(queryParams).then((res) => {
|
||||
const { code, data } = res
|
||||
if (code == 200) {
|
||||
dataList.value = data.result
|
||||
total.value = data.totalNum
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查询
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置查询操作
|
||||
function resetQuery() {
|
||||
proxy.resetForm('queryRef')
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 多选框选中数据
|
||||
function handleSelectionChange(selection) {
|
||||
ids.value = selection.map((item) => item.id)
|
||||
state.single = selection.length != 1
|
||||
state.multiple = !selection.length
|
||||
}
|
||||
|
||||
// 自定义排序
|
||||
function sortChange(column) {
|
||||
var sort = undefined
|
||||
var sortType = undefined
|
||||
|
||||
if (column.prop != null && column.order != null) {
|
||||
sort = column.prop
|
||||
sortType = column.order
|
||||
}
|
||||
queryParams.sort = sort
|
||||
queryParams.sortType = sortType
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
data.form = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
personnel: undefined,
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
cableId: undefined,
|
||||
actualMileage: undefined
|
||||
}
|
||||
formCableOptions.value = []
|
||||
}
|
||||
|
||||
// 添加按钮操作
|
||||
function handleAdd() {
|
||||
resetForm()
|
||||
formTitle.value = '新增标石/杆号牌'
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 修改按钮操作
|
||||
function handleUpdate(row) {
|
||||
resetForm()
|
||||
const id = row?.id || ids.value[0]
|
||||
getOdfMarkerPoles(id).then((res) => {
|
||||
if (res.code == 200) {
|
||||
const d = res.data
|
||||
data.form = {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
personnel: d.personnel,
|
||||
longitude: d.longitude,
|
||||
latitude: d.latitude,
|
||||
cableId: d.cableId,
|
||||
actualMileage: d.actualMileage
|
||||
}
|
||||
// 预填充光缆选项
|
||||
if (d.cableId && d.cableName) {
|
||||
formCableOptions.value = [{ id: d.cableId, cableName: d.cableName }]
|
||||
}
|
||||
formTitle.value = '编辑标石/杆号牌'
|
||||
formVisible.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitForm() {
|
||||
proxy.$refs['formRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
if (form.value.id) {
|
||||
updateOdfMarkerPoles(form.value).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('修改成功')
|
||||
formVisible.value = false
|
||||
getList()
|
||||
}
|
||||
submitting.value = false
|
||||
}).catch(() => {
|
||||
submitting.value = false
|
||||
})
|
||||
} else {
|
||||
addOdfMarkerPoles(form.value).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('新增成功')
|
||||
formVisible.value = false
|
||||
getList()
|
||||
}
|
||||
submitting.value = false
|
||||
}).catch(() => {
|
||||
submitting.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除按钮操作
|
||||
function handleDelete(row) {
|
||||
const Ids = row.id || ids.value
|
||||
|
||||
proxy
|
||||
.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?', '警告', {
|
||||
confirmButtonText: proxy.$t('common.ok'),
|
||||
cancelButtonText: proxy.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
})
|
||||
.then(function () {
|
||||
return delOdfMarkerPoles(Ids)
|
||||
})
|
||||
.then(() => {
|
||||
getList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
handleQuery()
|
||||
</script>
|
||||
15
sql/v1.0.2.1/01_create_odf_cable_fault_times.sql
Normal file
15
sql/v1.0.2.1/01_create_odf_cable_fault_times.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- =============================================
|
||||
-- 创建故障频次时间记录表
|
||||
-- 版本: v1.0.2.1
|
||||
-- =============================================
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'odf_cable_fault_times')
|
||||
BEGIN
|
||||
CREATE TABLE odf_cable_fault_times (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
FaultId INT NOT NULL, -- 关联故障 odf_cable_faults.Id
|
||||
FaultTime DATETIME NOT NULL, -- 故障发生时间
|
||||
CreatedAt DATETIME NOT NULL DEFAULT GETDATE() -- 创建时间
|
||||
);
|
||||
END
|
||||
GO
|
||||
26
sql/v1.2.0/01_create_odf_marker_poles.sql
Normal file
26
sql/v1.2.0/01_create_odf_marker_poles.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- =============================================
|
||||
-- ODF v1.2.0 - 创建标石/杆号牌表 odf_marker_poles
|
||||
-- 需求: 2.1, 4.8, 6.1
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE odf_marker_poles (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY, -- 主键
|
||||
CableId INT NOT NULL, -- 关联光缆ID (odf_cables.Id)
|
||||
Name NVARCHAR(200) NOT NULL, -- 标石/杆号牌名称
|
||||
RecordTime DATETIME NOT NULL, -- 记录时间(拍照时间)
|
||||
Personnel NVARCHAR(100) NULL, -- 责任人
|
||||
Latitude DECIMAL(10,7) DEFAULT 0, -- 纬度
|
||||
Longitude DECIMAL(10,7) DEFAULT 0, -- 经度
|
||||
ActualMileage NVARCHAR(100) NULL, -- 实际里程
|
||||
DeptId BIGINT NOT NULL, -- 所属公司/部门ID
|
||||
DeptName NVARCHAR(200) NULL, -- 部门名称(冗余)
|
||||
UserId BIGINT NULL, -- 提交人用户ID
|
||||
CreatedAt DATETIME DEFAULT GETDATE(), -- 创建时间
|
||||
UpdatedAt DATETIME DEFAULT GETDATE() -- 更新时间
|
||||
);
|
||||
|
||||
-- 索引:按光缆ID查询标石列表
|
||||
CREATE INDEX IX_odf_marker_poles_CableId ON odf_marker_poles(CableId);
|
||||
|
||||
-- 索引:按部门ID进行数据隔离查询
|
||||
CREATE INDEX IX_odf_marker_poles_DeptId ON odf_marker_poles(DeptId);
|
||||
14
sql/v1.2.0/02_create_odf_marker_pole_images.sql
Normal file
14
sql/v1.2.0/02_create_odf_marker_pole_images.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- =============================================
|
||||
-- ODF v1.2.0 - 创建标石/杆号牌图片表 odf_marker_pole_images
|
||||
-- 需求: 2.1, 4.8, 6.1
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE odf_marker_pole_images (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
MarkerPoleId INT NOT NULL, -- 关联标石ID (odf_marker_poles.Id)
|
||||
ImageUrl NVARCHAR(500) NOT NULL, -- COS 图片 URL
|
||||
CreatedAt DATETIME DEFAULT GETDATE()
|
||||
);
|
||||
|
||||
-- 索引:按标石ID查询图片
|
||||
CREATE INDEX IX_odf_marker_pole_images_MarkerPoleId ON odf_marker_pole_images(MarkerPoleId);
|
||||
28
sql/v1.2.0/03_create_odf_audit_logs.sql
Normal file
28
sql/v1.2.0/03_create_odf_audit_logs.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- =============================================
|
||||
-- ODF v1.2.0 - 创建审计日志表 odf_audit_logs
|
||||
-- 需求: 8.1, 8.2
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE odf_audit_logs (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
TableName NVARCHAR(100) NOT NULL, -- 操作的表名
|
||||
RecordId INT NULL, -- 操作的记录ID
|
||||
OperationType NVARCHAR(20) NOT NULL, -- 操作类型: INSERT/UPDATE/DELETE
|
||||
OperatorId BIGINT NOT NULL, -- 操作人用户ID
|
||||
OperatorName NVARCHAR(100) NULL, -- 操作人用户名
|
||||
SourceClient NVARCHAR(20) NOT NULL, -- 操作来源: App/Admin
|
||||
OldData NVARCHAR(MAX) NULL, -- 修改前数据 (JSON)
|
||||
NewData NVARCHAR(MAX) NULL, -- 修改后数据 (JSON)
|
||||
OperationTime DATETIME DEFAULT GETDATE(), -- 操作时间
|
||||
DeptId BIGINT NULL, -- 操作人所属部门
|
||||
Remark NVARCHAR(500) NULL -- 备注
|
||||
);
|
||||
|
||||
-- 索引:按操作时间范围查询
|
||||
CREATE INDEX IX_odf_audit_logs_OperationTime ON odf_audit_logs(OperationTime);
|
||||
|
||||
-- 索引:按表名筛选
|
||||
CREATE INDEX IX_odf_audit_logs_TableName ON odf_audit_logs(TableName);
|
||||
|
||||
-- 索引:按操作类型筛选
|
||||
CREATE INDEX IX_odf_audit_logs_OperationType ON odf_audit_logs(OperationType);
|
||||
62
sql/v1.2.0/04_insert_marker_pole_menus_permissions.sql
Normal file
62
sql/v1.2.0/04_insert_marker_pole_menus_permissions.sql
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
-- =============================================
|
||||
-- ODF v1.2.0 - 标石/杆号牌管理 & 审计日志 菜单权限
|
||||
-- 需求: 6.1, 8.1
|
||||
-- MenuId 从 11230 开始,避免与 v1.0.2.1 (11224) 冲突
|
||||
-- =============================================
|
||||
|
||||
SET IDENTITY_INSERT sys_menu ON;
|
||||
|
||||
-- =============================================
|
||||
-- 1. 标石/杆号牌管理(一级菜单)
|
||||
-- =============================================
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
|
||||
VALUES (11230, N'标石杆号牌管理', 0, 9, 'OdfMarkerPoles', 'business/OdfMarkerPoles', 0, 0, 'C', '0', '0', 'odfmarkerpoles:list', 'icon1', 'admin', GETDATE());
|
||||
|
||||
-- 标石/杆号牌管理 - 按钮权限
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, MenuType, Perms, Status, IsFrame, IsCache, Visible, Create_by, Create_time)
|
||||
VALUES (11231, N'查询', 11230, 1, 'F', 'odfmarkerpoles:query', 0, 1, 0, '0', 'admin', GETDATE());
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, MenuType, Perms, Status, IsFrame, IsCache, Visible, Create_by, Create_time)
|
||||
VALUES (11232, N'新增', 11230, 2, 'F', 'odfmarkerpoles:add', 0, 1, 0, '0', 'admin', GETDATE());
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, MenuType, Perms, Status, IsFrame, IsCache, Visible, Create_by, Create_time)
|
||||
VALUES (11233, N'修改', 11230, 3, 'F', 'odfmarkerpoles:edit', 0, 1, 0, '0', 'admin', GETDATE());
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, MenuType, Perms, Status, IsFrame, IsCache, Visible, Create_by, Create_time)
|
||||
VALUES (11234, N'删除', 11230, 4, 'F', 'odfmarkerpoles:delete', 0, 1, 0, '0', 'admin', GETDATE());
|
||||
|
||||
-- =============================================
|
||||
-- 2. 审计日志/修改统计(一级菜单)
|
||||
-- =============================================
|
||||
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
|
||||
VALUES (11240, N'修改统计', 0, 10, 'OdfAuditLogs', 'business/OdfAuditLogs', 0, 0, 'C', '0', '0', 'odfauditlogs:list', 'icon1', 'admin', GETDATE());
|
||||
|
||||
SET IDENTITY_INSERT sys_menu OFF;
|
||||
|
||||
-- =============================================
|
||||
-- 角色菜单权限分配
|
||||
-- Role 2 (common): 标石全部权限 + 审计日志查看
|
||||
-- Role 3 (editor): 标石查询 + 审计日志查看
|
||||
-- Role 4 (lock): 标石查询 + 审计日志查看
|
||||
-- =============================================
|
||||
|
||||
-- Role 2 (common) — 标石/杆号牌管理:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11230);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11231);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11232);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11233);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11234);
|
||||
|
||||
-- Role 2 (common) — 审计日志:查看权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (2, 11240);
|
||||
|
||||
-- Role 3 (editor) — 标石/杆号牌管理:菜单 + 查询
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (3, 11230);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (3, 11231);
|
||||
|
||||
-- Role 3 (editor) — 审计日志:查看权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (3, 11240);
|
||||
|
||||
-- Role 4 (lock) — 标石/杆号牌管理:菜单 + 查询
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (4, 11230);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (4, 11231);
|
||||
|
||||
-- Role 4 (lock) — 审计日志:查看权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (4, 11240);
|
||||
54
sql/v1.2.0/05_insert_branch_admin_role.sql
Normal file
54
sql/v1.2.0/05_insert_branch_admin_role.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
-- =============================================
|
||||
-- ODF v1.2.0 - 新增「分公司管理员」角色
|
||||
-- 需求: 11.1
|
||||
-- 角色权限:查看本公司及下属公司数据,管理本公司及下级公司人员账号
|
||||
-- dataScope = 3 (本部门数据权限),后端通过 DeptDataScopeHelper 扩展为本级+下级
|
||||
-- =============================================
|
||||
|
||||
SET IDENTITY_INSERT sys_role ON;
|
||||
|
||||
INSERT INTO sys_role (roleId, roleName, roleKey, roleSort, dataScope, menu_check_strictly, dept_check_strictly, status, delFlag, create_by, create_time, remark)
|
||||
VALUES (5, N'分公司管理员', 'branch_admin', 5, 3, 1, 1, 0, 0, 'admin', GETDATE(), N'分公司级别管理角色,可管理本公司及下属公司人员账号');
|
||||
|
||||
SET IDENTITY_INSERT sys_role OFF;
|
||||
|
||||
-- =============================================
|
||||
-- 分公司管理员菜单权限分配
|
||||
-- 拥有所有 ODF 业务菜单的全部权限 + 审计日志查看
|
||||
-- =============================================
|
||||
|
||||
-- 光缆管理:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11190);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11191);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11192);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11193);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11194);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11195);
|
||||
|
||||
-- 干线故障管理:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11200);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11201);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11202);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11203);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11223);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11224);
|
||||
|
||||
-- 签到记录管理:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11210);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11211);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11212);
|
||||
|
||||
-- 用户模块权限:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11220);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11221);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11222);
|
||||
|
||||
-- 标石/杆号牌管理:全部权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11230);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11231);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11232);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11233);
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11234);
|
||||
|
||||
-- 审计日志:查看权限
|
||||
INSERT INTO sys_role_menu (Role_id, Menu_id) VALUES (5, 11240);
|
||||
Loading…
Reference in New Issue
Block a user