Compare commits

..

No commits in common. "1307eaf65181c0f17d31fa48f4aefd50e8416c4d" and "11c9d33f94cb0cd46a69499c55a0ab0478328a35" have entirely different histories.

229 changed files with 18395 additions and 11452 deletions

BIN
.env Normal file

Binary file not shown.

View File

@ -4,8 +4,7 @@ VITE_API_PREFIX = '/dev-api'
# 接口地址
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://localhost:8888/'
VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
VITE_API_BASE_URL = 'http://localhost:8888/'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'

View File

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Industrial-image-management-system---web" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,147 @@
# 设备中心模块问题修复和改进总结
## 发现的问题
### 1. 弹窗组件问题
- **缺少查看模式**:原弹窗组件没有区分查看、编辑、新增模式
- **表单验证不完善**:缺少详细的验证规则和错误处理
- **缺少加载状态**提交时没有loading状态管理
- **表单重置逻辑不完善**:取消时没有正确重置表单
- **缺少调试功能**:开发环境下缺少调试工具
### 2. 主页面问题
- **数据转换逻辑复杂**:存在过多的兼容性处理,可能导致数据不一致
- **错误处理不够详细**:错误信息不够具体
- **缺少详情页面跳转**:没有设备详情页面的入口
- **对象引用问题**:直接传递对象引用可能导致数据污染
### 3. 功能缺失
- **缺少设备详情页面**:没有专门的设备详情展示页面
- **缺少维护记录管理**:没有设备维护记录的展示和管理
- **缺少使用记录**:没有设备使用历史的展示
- **缺少文件管理**:没有设备相关文件的管理功能
## 修复和改进内容
### 1. 弹窗组件优化 (`DeviceModal.vue`)
#### 新增功能
- ✅ **查看模式支持**:添加了查看模式,禁用所有输入框
- ✅ **完善的表单验证**:添加了详细的验证规则和长度限制
- ✅ **加载状态管理**添加了提交时的loading状态
- ✅ **表单重置优化**:完善了表单重置逻辑
- ✅ **调试功能**:开发环境下添加了调试按钮
- ✅ **错误处理优化**:更详细的错误信息处理
#### 技术改进
- ✅ **响应式表单数据**使用reactive管理表单数据
- ✅ **计算属性优化**:添加了表单有效性计算
- ✅ **监听器优化**:优化了数据变化监听逻辑
- ✅ **类型安全**完善了TypeScript类型定义
### 2. 主页面优化 (`index.vue`)
#### 功能改进
- ✅ **数据转换优化**:简化了数据转换逻辑,提高一致性
- ✅ **错误处理增强**:添加了更详细的错误信息处理
- ✅ **详情页面跳转**:添加了设备详情页面的跳转功能
- ✅ **对象深拷贝**:使用展开运算符避免对象引用问题
#### 用户体验改进
- ✅ **删除确认优化**:添加了更明确的删除确认提示
- ✅ **操作反馈优化**:改进了操作成功/失败的提示信息
### 3. 新增设备详情页面 (`detail.vue`)
#### 功能特性
- ✅ **基本信息展示**:设备的基本信息展示
- ✅ **状态信息展示**:设备的各种状态信息
- ✅ **维护记录管理**:设备维护记录的展示和管理
- ✅ **使用记录展示**:设备使用历史的展示
- ✅ **位置变更记录**:设备位置变更历史
- ✅ **文件管理**:设备相关文件的上传和管理
#### 技术实现
- ✅ **响应式数据管理**使用ref管理页面数据
- ✅ **路由参数处理**正确处理路由参数获取设备ID
- ✅ **API集成**集成设备详情API
- ✅ **状态文本转换**:统一的状态文本转换函数
### 4. 路由配置优化
#### 新增路由
- ✅ **设备详情路由**:添加了设备详情页面的路由配置
- ✅ **参数传递**支持通过URL参数传递设备ID
## 参考training模块的实现
### 借鉴的设计模式
1. **弹窗组件设计**参考了TrainingPlanModal的弹窗设计模式
2. **表单验证机制**:采用了相同的表单验证和错误处理机制
3. **数据管理方式**:使用了相同的响应式数据管理方式
4. **调试功能**:借鉴了开发环境下的调试工具设计
5. **详情页面设计**参考了TrainingDetail页面的布局和功能设计
### 技术实现对比
| 功能 | Training模块 | 设备管理模块 | 改进状态 |
|------|-------------|-------------|----------|
| 弹窗模式 | 查看/编辑/新增 | 查看/编辑/新增 | ✅ 已实现 |
| 表单验证 | 完善 | 完善 | ✅ 已实现 |
| 加载状态 | 有 | 有 | ✅ 已实现 |
| 调试功能 | 有 | 有 | ✅ 已实现 |
| 详情页面 | 有 | 有 | ✅ 已实现 |
| 错误处理 | 详细 | 详细 | ✅ 已实现 |
## 使用说明
### 1. 设备列表页面
- **搜索功能**:支持按设备名称、类型、状态等条件搜索
- **新增设备**:点击"新增设备"按钮打开新增弹窗
- **查看设备**:点击"查看"按钮以只读模式查看设备信息
- **编辑设备**:点击"编辑"按钮修改设备信息
- **详情页面**:点击"详情"按钮跳转到设备详情页面
- **设备操作**:支持分配、归还、删除等操作
### 2. 设备详情页面
- **基本信息**:展示设备的基本信息
- **状态信息**:展示设备的各种状态
- **维护记录**:查看和管理设备维护记录
- **使用记录**:查看设备使用历史
- **位置变更**:查看设备位置变更历史
- **文件管理**:上传和管理设备相关文件
### 3. 开发调试
- **调试按钮**:开发环境下显示调试按钮
- **表单测试**:可以测试表单数据绑定
- **测试数据**:可以填充测试数据
- **数据清空**:可以清空表单数据
## 后续优化建议
### 1. 功能扩展
- [ ] 添加设备维护记录的增删改功能
- [ ] 实现设备使用记录的实时更新
- [ ] 添加设备状态变更的审批流程
- [ ] 实现设备文件的在线预览功能
### 2. 性能优化
- [ ] 添加数据缓存机制
- [ ] 实现分页加载优化
- [ ] 添加数据预加载功能
### 3. 用户体验
- [ ] 添加操作确认的快捷键支持
- [ ] 实现批量操作功能
- [ ] 添加数据导出功能
- [ ] 实现高级搜索功能
## 总结
通过参考training模块的实现成功修复了设备中心模块的主要问题并添加了缺失的功能。主要改进包括
1. **弹窗组件**:完善了查看、编辑、新增模式,添加了完善的表单验证和错误处理
2. **主页面**:优化了数据转换逻辑,改进了错误处理,添加了详情页面跳转
3. **详情页面**:新增了完整的设备详情展示页面,包含维护记录、使用记录等功能
4. **路由配置**:添加了设备详情页面的路由配置
这些改进大大提升了设备中心模块的功能完整性和用户体验使其与training模块保持了一致的设计标准和实现质量。

View File

@ -8,7 +8,7 @@ export default function appInfo(): Plugin {
apply: 'serve',
async buildStart() {
const { bold, green, cyan, bgGreen, underline } = picocolors
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`${bgGreen('ContiNew Admin v4.0.0-SNAPSHOT')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/admin/faq.html')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,

View File

@ -35,7 +35,7 @@ export default antfu(
rules: {
'curly': ['off', 'all'], // 对所有控制语句强制使用一致的大括号样式
'no-new': 'off', // 不允许在赋值或比较之外使用 new 运算符
// 'no-console': 'error', // 禁止使用 console
'no-console': 'off', // 允许使用 console
'style/arrow-parens': ['error', 'always'], // 箭头函数参数需要括号
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], // 对块执行一致的大括号样式
'regexp/no-unused-capturing-group': 'off',

4301
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,9 +34,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.4",
"echarts": "^5.4.2",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"jspdf": "^3.0.1",
"lint-staged": "^15.2.10",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",

View File

@ -17,13 +17,14 @@
<script setup lang="ts">
import { useAppStore, useUserStore } from '@/stores'
// 1
defineOptions({ name: 'App' })
const userStore = useUserStore()
const appStore = useAppStore()
appStore.initTheme()
appStore.initSiteConfig()
</script>
<style scoped lang="scss">
.loading-icon {
animation: arco-loading-circle 1s infinite cubic-bezier(0,0,1,1);

View File

@ -1,12 +1,12 @@
import http from '@/utils/http'
const { request } = http
import type { AttachInfoData, BusinessTypeResult } from './type'
import http from '@/utils/http'
const { request } = http
/**
*
* @param businessType
* @param files
* @returns
*/
export function batchAddAttachment(businessType: string, formData: FormData) {
return request<AttachInfoData[]>({
@ -14,8 +14,8 @@ export function batchAddAttachment(businessType: string, formData: FormData) {
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -30,8 +30,8 @@ export function addAttachment(formData: FormData) {
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
/**
@ -45,8 +45,8 @@ export function addAttachmentByDefectMarkPic(formData: FormData) {
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
/**
@ -60,8 +60,8 @@ export function addAttachInsurance(formData: FormData) {
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
/**
@ -70,7 +70,7 @@ export function addAttachInsurance(formData: FormData) {
export function getAttachBusinessTypes() {
return request<BusinessTypeResult>({
url: '/common/list/attach-business_type',
method: 'get'
method: 'get',
})
}
@ -81,7 +81,7 @@ export function getAttachBusinessTypes() {
export function getAttachmentList(businessType: string) {
return request<AttachInfoData[]>({
url: `/attach-info/list/${businessType}`,
method: 'get'
method: 'get',
})
}
@ -92,6 +92,6 @@ export function getAttachmentList(businessType: string) {
export function deleteAttachment(id: string | number) {
return request<boolean>({
url: `/attach-info/${id}`,
method: 'delete'
method: 'delete',
})
}
}

View File

@ -31,4 +31,4 @@ export interface BusinessType {
name: string
code: string
description?: string
}
}

View File

@ -1,5 +1,5 @@
import http from '@/utils/http'
import type { AttendanceRecordReq, AttendanceRecordResp } from './type'
import http from '@/utils/http'
const BASE_URL = '/attendance-record'
@ -7,7 +7,7 @@ const BASE_URL = '/attendance-record'
export function addAttendanceRecord(data: AttendanceRecordReq) {
return http.post<AttendanceRecordResp>(BASE_URL, data, {
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
})
}
}

View File

@ -1,15 +1,15 @@
/** 新增考勤记录请求体 */
export interface AttendanceRecordReq {
recordImage?: string
recordPosition?: string
recordPositionLabel?: string
}
/** 新增考勤记录响应体 */
export interface AttendanceRecordResp {
code: number
data: object
msg: string
status: number
success: boolean
}
recordImage?: string
recordPosition?: string
recordPositionLabel?: string
}
/** 新增考勤记录响应体 */
export interface AttendanceRecordResp {
code: number
data: object
msg: string
status: number
success: boolean
}

View File

@ -1,6 +1,6 @@
import type * as T from './type'
import http from '@/utils/http'
import { convertMenuData, type ApiMenuItem } from '@/utils/menuConverter'
import { type ApiMenuItem, convertMenuData } from '@/utils/menuConverter'
export type * from './type'

View File

@ -66,9 +66,9 @@ export interface DefectTypeResp {
/** 缺陷类型选项类型 - 用于前端组件 */
export interface DefectTypeOption {
code: string;
label: string;
value: string;
name?: string; // 兼容性字段
sort?: number; // 兼容性字段
code: string
label: string
value: string
name?: string // 兼容性字段
sort?: number // 兼容性字段
}

View File

@ -1,5 +1,5 @@
import type { CertificationInfo, CertificationListParams, CertificationListResponse, CertificationPageResponse, CertificationReq, SimpleUserInfo } from './type'
import http from '@/utils/http'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo,CertificationPageResponse, CertificationReq } from './type'
const { request } = http
@ -11,7 +11,7 @@ export function createCertification(data: CertificationReq) {
return request({
url: '/certification',
method: 'post',
data
data,
})
}
@ -20,7 +20,7 @@ export function getCertificationList(params: CertificationListParams) {
return request<CertificationListResponse>({
url: '/certification/list',
method: 'get',
params
params,
})
}
@ -28,7 +28,7 @@ export function getCertificationList(params: CertificationListParams) {
export function getCertificationDetail(certificationId: string) {
return request<CertificationInfo>({
url: `/certification/detail/${certificationId}`,
method: 'get'
method: 'get',
})
}
@ -37,7 +37,7 @@ export function updateCertification(certificationId: string, data: Certification
return request({
url: `/certification/${certificationId}`,
method: 'put',
data
data,
})
}
@ -45,7 +45,7 @@ export function updateCertification(certificationId: string, data: Certification
export function deleteCertification(certificationId: string) {
return request({
url: `/certification/${certificationId}`,
method: 'delete'
method: 'delete',
})
}
@ -54,7 +54,7 @@ export function batchDeleteCertification(ids: string[]) {
return request({
url: '/certification/batch',
method: 'delete',
data: { ids }
data: { ids },
})
}
@ -64,7 +64,7 @@ export function exportCertification(params: CertificationListParams) {
url: '/certification/export',
method: 'get',
params,
responseType: 'blob'
responseType: 'blob',
})
}
@ -72,14 +72,14 @@ export function exportCertification(params: CertificationListParams) {
export function getUserList() {
return request<SimpleUserInfo[]>({
url: '/user/list',
method: 'get'
method: 'get',
})
}
}
// 查询人员资质信息分页列表(新接口)
export function getCertificationPage(params: CertificationListParams) {
return request<CertificationPageResponse>({
url: '/certification/page',
method: 'get',
params
params,
})
}
}

View File

@ -40,7 +40,7 @@ export interface SimpleUserInfo {
userId: string
name: string
account: string
}
}
export interface CertificationInfo {
certificationId?: string
certificationCode: string
@ -56,4 +56,4 @@ export interface CertificationPageResponse {
code?: number
msg?: string
[key: string]: any
}
}

View File

@ -0,0 +1,49 @@
import http from '@/utils/http'
import type * as T from '@/types/equipment.d'
const BASE_URL = '/equipment'
/** @desc 分页查询设备列表 */
export function pageEquipment(query: T.EquipmentPageQuery) {
return http.get<T.EquipmentResp[]>(`${BASE_URL}/page`, query)
}
/** @desc 查询设备列表 */
export function listEquipment(query?: T.EquipmentPageQuery) {
return http.get<T.EquipmentResp[]>(`${BASE_URL}/list`, query)
}
/** @desc 查询设备详情 */
export function getEquipmentDetail(equipmentId: string) {
return http.get<T.EquipmentResp>(`${BASE_URL}/detail/${equipmentId}`)
}
/** @desc 新增设备 */
export function createEquipment(data: T.EquipmentReq) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 更新设备 */
export function updateEquipment(equipmentId: string, data: T.EquipmentReq) {
return http.put(`${BASE_URL}/${equipmentId}`, data)
}
/** @desc 删除设备 */
export function deleteEquipment(equipmentId: string) {
return http.del(`${BASE_URL}/${equipmentId}`)
}
/** @desc 设备状态变更 */
export function changeEquipmentStatus(equipmentId: string, status: string) {
return http.put(`${BASE_URL}/${equipmentId}/status`, { status })
}
/** @desc 设备分配 */
export function assignEquipment(equipmentId: string, userId: string) {
return http.put(`${BASE_URL}/${equipmentId}/assign`, { userId })
}
/** @desc 设备归还 */
export function returnEquipment(equipmentId: string) {
return http.put(`${BASE_URL}/${equipmentId}/return`)
}

207
src/apis/equipment/type.ts Normal file
View File

@ -0,0 +1,207 @@
/**
*
*/
export interface EquipmentListReq {
/** 设备名称 */
equipmentName?: string
/** 设备类型 */
equipmentType?: string
/** 设备状态 */
equipmentStatus?: string
/** 设备序列号 */
equipmentSn?: string
/** 资产编号 */
assetCode?: string
/** 品牌 */
brand?: string
/** 位置状态 */
locationStatus?: string
/** 健康状态 */
healthStatus?: string
/** 负责人 */
responsiblePerson?: string
/** 使用状态 */
useStatus?: string
/** 项目ID */
projectId?: string
/** 使用人ID */
userId?: string
/** 当前页码 */
pageNum?: number
/** 每页大小 */
pageSize?: number
/** 排序字段 */
orderBy?: string
/** 排序方向 */
orderDirection?: string
}
/**
*
*/
export interface PageResult<T> {
code: number
msg: string
rows: T[]
total: number
}
/**
*
*/
export interface EquipmentResp {
/** 设备ID */
equipmentId: string
/** 资产编号 */
assetCode?: string
/** 设备名称 */
equipmentName: string
/** 设备类型 */
equipmentType: string
/** 设备类型描述 */
equipmentTypeLabel?: string
/** 设备型号 */
equipmentModel: string
/** 设备SN */
equipmentSn: string
/** 品牌 */
brand?: string
/** 配置规格/参数 */
specification?: string
/** 设备状态 */
equipmentStatus: string
/** 设备状态描述 */
equipmentStatusLabel?: string
/** 使用状态 */
useStatus: string
/** 位置状态 */
locationStatus?: string
/** 位置状态描述 */
locationStatusLabel?: string
/** 设备当前物理位置 */
physicalLocation?: string
/** 负责人 */
responsiblePerson?: string
/** 健康状态 */
healthStatus?: string
/** 健康状态描述 */
healthStatusLabel?: string
/** 采购时间 */
purchaseTime?: string
/** 入库时间 */
inStockTime?: string
/** 启用时间 */
activationTime?: string
/** 预计报废时间 */
expectedScrapTime?: string
/** 实际报废时间 */
actualScrapTime?: string
/** 状态变更时间 */
statusChangeTime?: string
/** 采购订单 */
purchaseOrder?: string
/** 供应商名称 */
supplierName?: string
/** 采购价格 */
purchasePrice?: number
/** 当前净值 */
currentNetValue?: number
/** 折旧方法 */
depreciationMethod?: string
/** 折旧年限 */
depreciationYears?: number
/** 残值 */
salvageValue?: number
/** 保修截止日期 */
warrantyExpireDate?: string
/** 上次维护日期 */
lastMaintenanceDate?: string
/** 下次维护日期 */
nextMaintenanceDate?: string
/** 维护人员 */
maintenancePerson?: string
/** 库存条码 */
inventoryBarcode?: string
/** 资产备注 */
assetRemark?: string
/** 项目ID */
projectId?: string
/** 项目名称 */
projectName?: string
/** 使用人ID */
userId?: string
/** 使用人 */
name?: string
/** 创建时间 */
createTime?: string
/** 更新时间 */
updateTime?: string
}
/**
*
*/
export interface EquipmentReq {
/** 设备名称 */
equipmentName: string
/** 设备型号 */
equipmentModel: string
/** 设备类型 */
equipmentType: string
/** 设备状态 */
equipmentStatus: string
/** 使用状态 */
useStatus: string
/** 设备序列号 */
equipmentSn: string
/** 资产编号 */
assetCode?: string
/** 品牌 */
brand?: string
/** 配置规格/参数 */
specification?: string
/** 位置状态 */
locationStatus?: string
/** 设备当前物理位置 */
physicalLocation?: string
/** 负责人 */
responsiblePerson?: string
/** 健康状态 */
healthStatus?: string
/** 采购时间 */
purchaseTime?: string
/** 入库时间 */
inStockTime?: string
/** 启用时间 */
activationTime?: string
/** 预计报废时间 */
expectedScrapTime?: string
/** 实际报废时间 */
actualScrapTime?: string
/** 采购订单 */
purchaseOrder?: string
/** 供应商名称 */
supplierName?: string
/** 采购价格 */
purchasePrice?: number
/** 当前净值 */
currentNetValue?: number
/** 折旧方法 */
depreciationMethod?: string
/** 折旧年限 */
depreciationYears?: number
/** 残值 */
salvageValue?: number
/** 保修截止日期 */
warrantyExpireDate?: string
/** 上次维护日期 */
lastMaintenanceDate?: string
/** 下次维护日期 */
nextMaintenanceDate?: string
/** 维护人员 */
maintenancePerson?: string
/** 库存条码 */
inventoryBarcode?: string
/** 资产备注 */
assetRemark?: string
}

View File

@ -40,7 +40,7 @@ export function createHealthRecord(data: HealthRecord) {
return request({
url: '/health-record',
method: 'post',
data
data,
})
}
@ -49,7 +49,7 @@ export function getHealthRecordList(params: HealthRecordListParams) {
return request<HealthRecordListResponse>({
url: '/health-record/list',
method: 'get',
params
params,
})
}
@ -57,7 +57,7 @@ export function getHealthRecordList(params: HealthRecordListParams) {
export function getHealthRecordDetail(id: string) {
return request<HealthRecord>({
url: `/health-record/detail/${id}`,
method: 'get'
method: 'get',
})
}
@ -66,7 +66,7 @@ export function updateHealthRecord(id: string, data: HealthRecord) {
return request({
url: `/health-record/${id}`,
method: 'put',
data
data,
})
}
@ -74,7 +74,7 @@ export function updateHealthRecord(id: string, data: HealthRecord) {
export function deleteHealthRecord(id: string) {
return request({
url: `/health-record/${id}`,
method: 'delete'
method: 'delete',
})
}
@ -83,14 +83,14 @@ export function uploadHealthReport(file: File, recordId: string) {
const formData = new FormData()
formData.append('file', file)
formData.append('recordId', recordId)
return request({
url: '/health-record/upload-report',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -99,7 +99,7 @@ export function downloadHealthReport(fileId: string) {
return request({
url: `/health-record/download-report/${fileId}`,
method: 'get',
responseType: 'blob'
responseType: 'blob',
})
}
@ -107,7 +107,7 @@ export function downloadHealthReport(fileId: string) {
export function getEmployeeHealthHistory(employeeId: string) {
return request<HealthRecord[]>({
url: `/health-record/employee/${employeeId}`,
method: 'get'
method: 'get',
})
}
@ -117,7 +117,7 @@ export function exportHealthRecords(params: HealthRecordListParams) {
url: '/health-record/export',
method: 'get',
params,
responseType: 'blob'
responseType: 'blob',
})
}
@ -131,6 +131,6 @@ export function scheduleHealthCheck(data: {
return request({
url: '/health-record/schedule',
method: 'post',
data
data,
})
}
}

View File

@ -16,7 +16,6 @@ export * as InsuranceTypeAPI from './insurance-type'
export * as HealthRecordAPI from './health-record'
export * as InsuranceFileAPI from './insurance-file'
export * as EmployeeAPI from './employee'
export * as RegulationAPI from './regulation'
export * from './area/type'
export * from './auth/type'

View File

@ -87,7 +87,7 @@ export const detectDefects = (params: DefectDetectionRequest) => {
}
/** @desc 手动添加缺陷记录 */
export const addManualDefect = (params: ManualDefectAddRequest,imageId:string) => {
export const addManualDefect = (params: ManualDefectAddRequest, imageId: string) => {
return http.post<ManualDefectAddResponse>(`/defect/${imageId}`, params)
}
@ -95,12 +95,12 @@ export const addManualDefect = (params: ManualDefectAddRequest,imageId:string) =
// 缺陷列表查询参数接口
export interface DefectListParams {
defectId?: string;
defectLevel?: string;
defectType?: string;
keyword?: string;
turbineId?: string;
imageId?: string; // 添加imageId参数用于按图像筛选缺陷
defectId?: string
defectLevel?: string
defectType?: string
keyword?: string
turbineId?: string
imageId?: string // 添加imageId参数用于按图像筛选缺陷
}
/** @desc 获取缺陷列表 */
@ -111,7 +111,7 @@ export const getDefectList = (params: DefectListParams) => {
msg: string
status: number
success: boolean
}>('/defect/list', params )
}>('/defect/list', params)
}
/** @desc 添加缺陷 */
@ -150,7 +150,7 @@ export const deleteDefect = (defectId: string) => {
export const uploadAnnotatedImage = (imageBlob: Blob, fileName: string) => {
const formData = new FormData()
formData.append('file', imageBlob, fileName)
return http.post<{
code: number
data: AttachInfoData
@ -159,8 +159,8 @@ export const uploadAnnotatedImage = (imageBlob: Blob, fileName: string) => {
success: boolean
}>('/attach-info/defect_mark_pic', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -177,63 +177,63 @@ export interface AttachInfoData {
// 缺陷信息接口
export interface DefectInfo {
id: string;
defectId?: string;
defectName?: string;
defectLevel?: string;
defectType?: string;
defectPosition?: string;
detectionDate?: string;
description?: string;
repairStatus?: string;
repairIdea?: string;
labelInfo?: string;
id: string
defectId?: string
defectName?: string
defectLevel?: string
defectType?: string
defectPosition?: string
detectionDate?: string
description?: string
repairStatus?: string
repairIdea?: string
labelInfo?: string
markInfo?: {
bbox?: number[];
clsId?: number;
confidence?: number;
label?: string;
[key: string]: any;
};
[key: string]: any;
bbox?: number[]
clsId?: number
confidence?: number
label?: string
[key: string]: any
}
[key: string]: any
}
// 缺陷等级类型
export interface DefectLevelType {
code: string;
name: string;
value: string;
sort: number;
description?: string;
code: string
name: string
value: string
sort: number
description?: string
}
// 缺陷类型
export interface DefectType {
code: string;
name: string;
value: string;
sort: number;
description?: string;
code: string
name: string
value: string
sort: number
description?: string
}
// 获取缺陷等级列表
export const getDefectLevels = () => {
return http.get<{
code: number;
data: DefectLevelType[];
msg: string;
status: number;
success: boolean;
code: number
data: DefectLevelType[]
msg: string
status: number
success: boolean
}>('/common/list/defect-level')
}
// 获取缺陷类型列表
export const getDefectTypes = () => {
return http.get<{
code: number;
data: DefectType[];
msg: string;
status: number;
success: boolean;
code: number
data: DefectType[]
msg: string
status: number
success: boolean
}>('/common/list/defect-type')
}
}

View File

@ -114,7 +114,7 @@ export const uploadSingleImage = (imageSource: string, file: File, params?: {
}) => {
const formData = new FormData()
formData.append('file', file)
// 构建查询参数
const queryParams = new URLSearchParams()
if (params?.altitude) queryParams.append('altitude', params.altitude)
@ -122,13 +122,13 @@ export const uploadSingleImage = (imageSource: string, file: File, params?: {
if (params?.longitude) queryParams.append('longitude', params.longitude)
if (params?.partId) queryParams.append('partId', params.partId)
if (params?.uploadUser) queryParams.append('uploadUser', params.uploadUser)
const url = `/common/upload-image/${imageSource}${queryParams.toString() ? '?' + queryParams.toString() : ''}`
const url = `/common/upload-image/${imageSource}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
return http.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -141,12 +141,12 @@ export const batchUploadImages = (imageSource: string, files: File[], params?: {
uploadUser?: string
}) => {
const formData = new FormData()
// 添加文件
files.forEach(file => {
files.forEach((file) => {
formData.append('files', file)
})
// 构建查询参数
const queryParams = new URLSearchParams()
if (params?.altitude) queryParams.append('altitude', params.altitude)
@ -154,13 +154,13 @@ export const batchUploadImages = (imageSource: string, files: File[], params?: {
if (params?.longitude) queryParams.append('longitude', params.longitude)
if (params?.partId) queryParams.append('partId', params.partId)
if (params?.uploadUser) queryParams.append('uploadUser', params.uploadUser)
const url = `/common/batch-upload-image/${imageSource}${queryParams.toString() ? '?' + queryParams.toString() : ''}`
const url = `/common/batch-upload-image/${imageSource}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
return http.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -173,8 +173,8 @@ export const detectDefects = (params: {
}) => {
return http.post('/defect/detect', params, {
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
})
}
@ -183,15 +183,15 @@ export const uploadImageToPartV2 = (
imageSource: string,
partId: string,
files: File[],
params: Partial<T.ImageUploadParams>
params: Partial<T.ImageUploadParams>,
) => {
const formData = new FormData()
// 添加文件
files.forEach(file => {
files.forEach((file) => {
formData.append('files', file)
})
// 添加其他参数
if (params.collectorId) formData.append('collectorId', params.collectorId)
if (params.collectorName) formData.append('collectorName', params.collectorName)
@ -223,23 +223,23 @@ export const uploadImageToPartV2 = (
if (params.temperatureMin !== undefined) formData.append('temperatureMin', params.temperatureMin.toString())
if (params.weather) formData.append('weather', params.weather)
if (params.windLevel !== undefined) formData.append('windLevel', params.windLevel.toString())
return http.post(`/image/${imageSource}/upload/${partId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
/** @desc 图像导入接口(更新为使用真实接口) */
export const importImages = (files: FileList | File[], params: T.ImageImportParams) => {
const fileArray = Array.from(files)
// 使用批量上传接口
return batchUploadImages(params.imageSource || 'default', fileArray, {
partId: params.componentId,
uploadUser: params.uploadUser
}).then(response => {
uploadUser: params.uploadUser,
}).then((response) => {
// 如果需要自动标注
if (params.autoAnnotate && params.annotationTypes && params.annotationTypes.length > 0) {
// 这里可以添加自动标注逻辑
@ -255,7 +255,7 @@ export const autoAnnotateImage = (params: T.AutoAnnotationParams) => {
confThreshold: params.confidenceThreshold || 0.5,
defectTypeList: params.annotationTypes,
imageId: params.imageId,
modelId: params.params?.modelId || 'default'
modelId: params.params?.modelId || 'default',
})
}
@ -275,10 +275,10 @@ export const confirmAnnotation = (imageId: string, annotationId: string) => {
}
/** @desc 上传图像(保留旧接口兼容性) */
export const uploadImage = (file: File, params: { projectId: string; componentId?: string }) => {
export const uploadImage = (file: File, params: { projectId: string, componentId?: string }) => {
return uploadSingleImage('default', file, {
partId: params.componentId,
uploadUser: 'current-user'
uploadUser: 'current-user',
})
}
@ -321,21 +321,20 @@ export function reprocessImage(params: T.ImageProcessParams) {
export const batchProcessImages = (imageIds: string[], processType: string) => {
return http.post<T.ImageProcessResult[]>(`/industrial-image/batch-process`, {
imageIds,
processType
processType,
})
}
/** @desc 导出处理结果 */
export function exportProcessResults(query: T.ImageQuery) {
return http.get(`/industrial-image/export/results`, query, {
responseType: 'blob'
responseType: 'blob',
})
}
/** @desc 生成检测报告 */
export function generateReport(projectId: string) {
return http.post(`/industrial-image/report/generate`, { projectId }, {
responseType: 'blob'
responseType: 'blob',
})
}

View File

@ -153,7 +153,7 @@ export interface IndustrialImage {
name: string
/** 图像路径 */
path: string
/** 图像路径API返回字段*/
/** 图像路径API返回字段 */
imagePath?: string
/** 缩略图路径 */
thumbnailPath?: string
@ -185,7 +185,7 @@ export interface IndustrialImage {
createTime?: string
/** 更新时间 */
updateTime?: string
// 扩展字段 - 来自真实API
/** 部件名称 */
partName?: string
@ -412,4 +412,4 @@ export interface ImageProcessResult {
createTime?: string
/** 完成时间 */
completeTime?: string
}
}

View File

@ -34,7 +34,7 @@ export function createInsuranceCompany(data: InsuranceCompany) {
return request({
url: '/insurance-company',
method: 'post',
data
data,
})
}
@ -43,7 +43,7 @@ export function getInsuranceCompanyList(params: InsuranceCompanyListParams) {
return request<InsuranceCompanyListResponse>({
url: '/insurance-company/list',
method: 'get',
params
params,
})
}
@ -51,7 +51,7 @@ export function getInsuranceCompanyList(params: InsuranceCompanyListParams) {
export function getInsuranceCompanyDetail(id: string) {
return request<InsuranceCompany>({
url: `/insurance-company/detail/${id}`,
method: 'get'
method: 'get',
})
}
@ -60,7 +60,7 @@ export function updateInsuranceCompany(id: string, data: InsuranceCompany) {
return request({
url: `/insurance-company/${id}`,
method: 'put',
data
data,
})
}
@ -68,7 +68,7 @@ export function updateInsuranceCompany(id: string, data: InsuranceCompany) {
export function deleteInsuranceCompany(id: string) {
return request({
url: `/insurance-company/${id}`,
method: 'delete'
method: 'delete',
})
}
@ -76,7 +76,7 @@ export function deleteInsuranceCompany(id: string) {
export function terminateCooperation(id: string) {
return request({
url: `/insurance-company/terminate/${id}`,
method: 'post'
method: 'post',
})
}
@ -84,7 +84,7 @@ export function terminateCooperation(id: string) {
export function resumeCooperation(id: string) {
return request({
url: `/insurance-company/resume/${id}`,
method: 'post'
method: 'post',
})
}

View File

@ -48,14 +48,14 @@ export function uploadInsuranceFile(data: UploadInsuranceFileParams) {
if (data.description) {
formData.append('description', data.description)
}
return request({
url: '/insurance-file/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -64,7 +64,7 @@ export function getInsuranceFileList(params: InsuranceFileListParams) {
return request<InsuranceFileListResponse>({
url: '/insurance-file/list',
method: 'get',
params
params,
})
}
@ -72,7 +72,7 @@ export function getInsuranceFileList(params: InsuranceFileListParams) {
export function getInsuranceFileDetail(id: string) {
return request<InsuranceFile>({
url: `/insurance-file/detail/${id}`,
method: 'get'
method: 'get',
})
}
@ -81,7 +81,7 @@ export function updateInsuranceFile(id: string, data: Partial<InsuranceFile>) {
return request({
url: `/insurance-file/${id}`,
method: 'put',
data
data,
})
}
@ -89,7 +89,7 @@ export function updateInsuranceFile(id: string, data: Partial<InsuranceFile>) {
export function deleteInsuranceFile(id: string) {
return request({
url: `/insurance-file/${id}`,
method: 'delete'
method: 'delete',
})
}
@ -98,7 +98,7 @@ export function batchDeleteInsuranceFiles(ids: string[]) {
return request({
url: '/insurance-file/batch',
method: 'delete',
data: { ids }
data: { ids },
})
}
@ -107,7 +107,7 @@ export function downloadInsuranceFile(id: string) {
return request({
url: `/insurance-file/download/${id}`,
method: 'get',
responseType: 'blob'
responseType: 'blob',
})
}
@ -116,7 +116,7 @@ export function previewInsuranceFile(id: string) {
return request({
url: `/insurance-file/preview/${id}`,
method: 'get',
responseType: 'blob'
responseType: 'blob',
})
}
@ -124,7 +124,7 @@ export function previewInsuranceFile(id: string) {
export function getEmployeeFiles(employeeId: string) {
return request<InsuranceFile[]>({
url: `/insurance-file/employee/${employeeId}`,
method: 'get'
method: 'get',
})
}
@ -136,7 +136,7 @@ export function getInsuranceFileStatistics() {
totalSize: number
}[]>({
url: '/insurance-file/statistics',
method: 'get'
method: 'get',
})
}
@ -156,13 +156,13 @@ export function batchUploadFiles(data: {
if (data.description) {
formData.append('description', data.description)
}
return request({
url: '/insurance-file/batch-upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
}

View File

@ -28,7 +28,7 @@ export function createInsuranceType(data: InsuranceType) {
return request({
url: '/insurance-type',
method: 'post',
data
data,
})
}
@ -37,7 +37,7 @@ export function getInsuranceTypeList(params?: InsuranceTypeListParams) {
return request<InsuranceTypeListResponse>({
url: '/insurance-type/list',
method: 'get',
params
params,
})
}
@ -45,7 +45,7 @@ export function getInsuranceTypeList(params?: InsuranceTypeListParams) {
export function getInsuranceTypeDetail(insuranceTypeId: string) {
return request<InsuranceType>({
url: `/insurance-type/detail/${insuranceTypeId}`,
method: 'get'
method: 'get',
})
}
@ -54,7 +54,7 @@ export function updateInsuranceType(id: string, data: InsuranceType) {
return request({
url: `/insurance-type/${id}`,
method: 'put',
data
data,
})
}
@ -62,7 +62,7 @@ export function updateInsuranceType(id: string, data: InsuranceType) {
export function deleteInsuranceType(id: string) {
return request({
url: `/insurance-type/${id}`,
method: 'delete'
method: 'delete',
})
}
@ -71,6 +71,6 @@ export function batchDeleteInsuranceType(ids: string[]) {
return request({
url: '/insurance-type/batch',
method: 'delete',
data: { ids }
data: { ids },
})
}

View File

@ -1,5 +1,5 @@
import http from '@/utils/http'
import type { InsuranceInfo, InsuranceListParams, InsuranceListResponse, RenewInsuranceParams } from './type'
import http from '@/utils/http'
const { request } = http
@ -11,7 +11,7 @@ export function createInsurance(data: InsuranceInfo) {
return request({
url: '/insurance-info',
method: 'post',
data
data,
})
}
@ -20,7 +20,7 @@ export function getInsuranceList(params: InsuranceListParams) {
return request<InsuranceListResponse>({
url: '/insurance-info/list',
method: 'get',
params
params,
})
}
@ -28,7 +28,7 @@ export function getInsuranceList(params: InsuranceListParams) {
export function getInsuranceDetail(id: string) {
return request<InsuranceInfo>({
url: `/insurance-info/detail/${id}`,
method: 'get'
method: 'get',
})
}
@ -37,7 +37,7 @@ export function updateInsurance(id: string, data: InsuranceInfo) {
return request({
url: `/insurance-info/${id}`,
method: 'put',
data
data,
})
}
@ -45,7 +45,7 @@ export function updateInsurance(id: string, data: InsuranceInfo) {
export function deleteInsurance(id: string) {
return request({
url: `/insurance-info/${id}`,
method: 'delete'
method: 'delete',
})
}
@ -54,7 +54,7 @@ export function renewInsurance(id: string, data: RenewInsuranceParams) {
return request({
url: `/insurance-info/renew/${id}`,
method: 'post',
data
data,
})
}
@ -63,7 +63,7 @@ export function batchDeleteInsurance(ids: string[]) {
return request({
url: '/insurance-info/batch',
method: 'delete',
data: { ids }
data: { ids },
})
}
@ -73,6 +73,6 @@ export function exportInsurance(params: InsuranceListParams) {
url: '/insurance-info/export',
method: 'get',
params,
responseType: 'blob'
responseType: 'blob',
})
}
}

View File

@ -1,7 +1,7 @@
/** 保险信息接口 */
export interface InsuranceInfo {
id?: string
attachInfoId:string
attachInfoId: string
insuranceCompanyId: string
insuranceTypeId: string
userId: string
@ -38,4 +38,4 @@ export interface InsuranceListResponse {
export interface RenewInsuranceParams {
expireDate: string
insurancePremium: number
}
}

View File

@ -1,5 +1,5 @@
import type { ModelConfigDetailResponse, ModelConfigListResponse, ModelConfigRequest, ModelConfigResponse } from './type'
import http from '@/utils/http'
import type { ModelConfigRequest, ModelConfigResponse, ModelConfigListResponse, ModelConfigDetailResponse } from './type'
const { request } = http
@ -11,7 +11,7 @@ export function createModelConfig(data: ModelConfigRequest) {
return request<ModelConfigResponse>({
url: '/model-config',
method: 'post',
data
data,
})
}
@ -23,7 +23,7 @@ export function updateModelConfig(data: ModelConfigRequest) {
return request<ModelConfigResponse>({
url: '/model-config',
method: 'put',
data
data,
})
}
@ -44,7 +44,7 @@ export function getModelConfigList(params?: {
return request<ModelConfigListResponse>({
url: '/model-config/list',
method: 'get',
params
params,
})
}
@ -55,7 +55,7 @@ export function getModelConfigList(params?: {
export function getModelConfigDetail(modelId: string) {
return request<ModelConfigDetailResponse>({
url: `/model-config/${modelId}`,
method: 'get'
method: 'get',
})
}
@ -66,6 +66,6 @@ export function getModelConfigDetail(modelId: string) {
export function deleteModelConfig(modelId: string) {
return request<any>({
url: `/model-config/${modelId}`,
method: 'delete'
method: 'delete',
})
}
}

View File

@ -49,4 +49,4 @@ export interface ModelConfigDetailResponse {
data: ModelConfigResponse
msg: string
status: number
}
}

View File

@ -1,5 +1,5 @@
import type { DimensionQuery, PerformanceDimension, PerformanceRule, RuleQuery } from './type'
import http from '@/utils/http'
import type { PerformanceDimension, PerformanceRule, DimensionQuery, RuleQuery } from './type'
/** 维度相关 */
export function getDimensionList(params?: DimensionQuery) {
@ -36,5 +36,5 @@ export function deleteRule(id: string) {
}
// 我的绩效
export function getMyEvaluation() {
return http.get('/performance-evaluation/my')
}
return http.get('/performance-evaluation/my')
}

View File

@ -1,39 +1,39 @@
/** 绩效维度 */
export interface PerformanceDimension {
dimensionId: string
dimensionName: string
description?: string
deptName: string
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 绩效细则 */
export interface PerformanceRule {
ruleId: string
ruleName: string
description?: string
dimensionName: string
bonus?: string
score?: number
weight?: number
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 查询参数 */
export interface DimensionQuery {
dimensionName?: string
status?: 0 | 1
}
export interface RuleQuery {
dimensionName?: string
ruleName?: string
status?: 0 | 1
}
dimensionId: string
dimensionName: string
description?: string
deptName: string
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 绩效细则 */
export interface PerformanceRule {
ruleId: string
ruleName: string
description?: string
dimensionName: string
bonus?: string
score?: number
weight?: number
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 查询参数 */
export interface DimensionQuery {
dimensionName?: string
status?: 0 | 1
}
export interface RuleQuery {
dimensionName?: string
ruleName?: string
status?: 0 | 1
}

View File

@ -89,17 +89,17 @@ export function auditBudget(id: string, data: BudgetAuditReq) {
/** @desc 获取预算类型选项 */
export function getBudgetTypes() {
return http.get<Array<{ label: string; value: string }>>(`${BASE_URL}/types`)
return http.get<Array<{ label: string, value: string }>>(`${BASE_URL}/types`)
}
/** @desc 上传预算附件 */
export function uploadBudgetAttachment(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.post<{ id: string; name: string; url: string }>(`${BASE_URL}/upload`, formData, {
return http.post<{ id: string, name: string, url: string }>(`${BASE_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
@ -111,6 +111,6 @@ export function deleteBudgetAttachment(id: string) {
/** @desc 导出预算记录 */
export function exportBudgetRecord(query: BudgetQuery) {
return http.get(`${BASE_URL}/export`, query, {
responseType: 'blob'
responseType: 'blob',
})
}
}

View File

@ -41,12 +41,12 @@ export function importProject(file: File) {
formData.append('file', file)
return http.post(`${BASE_URL}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
/** @desc 导出项目 */
export function exportProject(query: T.ProjectQuery) {
return http.download(`${BASE_URL}/export`, query)
}
}

View File

@ -23,9 +23,9 @@ export function deleteTaskGroup(id: number) {
return http.del(`${BASE_URL}/group/${id}`)
}
/** @desc 查询任务列表(标准导出) */
export const listTask = (params: any) => {
return http.get('/project-task/list', params)
/** @desc 查询任务列表 */
export function listTask(query: T.TaskPageQuery) {
return http.get<PageRes<T.TaskResp[]>>(`${BASE_URL}`, query)
}
/** @desc 获取任务详情 */
@ -65,7 +65,7 @@ export function importTask(file: File, projectId: number) {
formData.append('projectId', projectId.toString())
return http.post(`${BASE_URL}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
'Content-Type': 'multipart/form-data',
},
})
}
}

View File

@ -1,10 +1,10 @@
/** 项目类型 */
export interface ProjectResp {
projectId: string // 项目ID (API返回的是字符串)
projectCode?: string // 项目编号
projectName: string // 项目名称
projectCode?: string // 项目编号
projectName: string // 项目名称
projectIntro?: string // 项目简介
farmName: string // 风场名称 (API字段名是farmName)
farmName: string // 风场名称 (API字段名是farmName)
farmAddress?: string // 风场地址 (API字段名是farmAddress)
client?: string // 委托单位
clientContact?: string // 委托单位联系人
@ -28,10 +28,10 @@ export interface ProjectResp {
coverUrl?: string // 封面URL
createDt?: Date
updateDt?: Date
// 为了保持向后兼容,添加一些别名字段
id?: string // projectId的别名
fieldName?: string // farmName的别名
fieldName?: string // farmName的别名
fieldLocation?: string // farmAddress的别名
commissionUnit?: string // client的别名
commissionContact?: string // clientContact的别名
@ -85,4 +85,4 @@ export interface TaskQuery {
status?: string
}
export interface TaskPageQuery extends TaskQuery, PageQuery {}
export interface TaskPageQuery extends TaskQuery, PageQuery {}

View File

@ -1,4 +1,4 @@
import type { SalaryRecord, SalaryQuery, SalaryCreateRequest } from '@/views/salary-management/types'
import type { SalaryCreateRequest, SalaryQuery, SalaryRecord } from '@/views/salary-management/types'
import http from '@/utils/http'
const BASE_URL = '/salary'
@ -34,7 +34,7 @@ export const submitApproval = (id: string) => {
}
// 审批工资单
export const approveSalary = (id: string, data: { status: string; comment?: string }) => {
export const approveSalary = (id: string, data: { status: string, comment?: string }) => {
return http.put<boolean>(`${BASE_URL}/${id}/approve`, data)
}

View File

@ -1,4 +1,3 @@
import type * as T from './type'
import http from '@/utils/http'
import { convertMenuData } from '@/utils/menuConverter'
@ -6,13 +5,13 @@ import { convertMenuData } from '@/utils/menuConverter'
* API获取菜单树形数据
*/
export function getMenuTreeForRole(query?: { terminalType?: string }) {
return http.get<any[]>('/menu/tree', query).then(res => {
return http.get<any[]>('/menu/tree', query).then((res) => {
// 假设响应格式为 { data: [...菜单数据], success: true, msg: "", code: 200 }
const data = res.data || [];
const data = res.data || []
// 转换菜单数据为角色管理组件需要的格式
const convertedData = convertMenuData(data);
return convertedData;
});
const convertedData = convertMenuData(data)
return convertedData
})
}
/**
@ -23,31 +22,31 @@ export function getMenuTreeForRole(query?: { terminalType?: string }) {
*/
export function transformMenusWithPermissions(menus: any[], selectedMenuIds: string[] = []) {
// 深拷贝菜单数据,避免修改原始数据
const result = JSON.parse(JSON.stringify(menus));
const result = JSON.parse(JSON.stringify(menus))
// 递归处理菜单树,添加权限标记
const processMenus = (items: any[]) => {
return items.map(item => {
return items.map((item) => {
// 设置选中状态
item.isChecked = selectedMenuIds.includes(item.id.toString());
item.isChecked = selectedMenuIds.includes(item.id.toString())
// 如果有子菜单,递归处理
if (item.children && item.children.length > 0) {
item.children = processMenus(item.children);
item.children = processMenus(item.children)
}
return item;
});
};
return processMenus(result);
return item
})
}
return processMenus(result)
}
/**
* ID列表
*/
export function getRoleMenuIds(roleId: string) {
return http.get<string[]>(`/role/get-menus/${roleId}`);
return http.get<string[]>(`/role/get-menus/${roleId}`)
}
/**
@ -56,6 +55,6 @@ export function getRoleMenuIds(roleId: string) {
export function assignRoleMenus(roleId: string, menuIds: string[]) {
return http.post('/role/bind-menu', {
roleId,
menuIds
});
}
menuIds,
})
}

View File

@ -1,45 +1,46 @@
import type * as T from './type';
import http from '@/utils/http';
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/post';
const BASE_URL = '/post'
/**
*
*/
export function addPost(data: T.PostAddReq) {
return http.post<any>(BASE_URL, data);
return http.post<any>(BASE_URL, data)
}
/**
*
*/
export function getPostDetail(postId: string) {
return http.get<T.PostVO>(`${BASE_URL}/detail/${postId}`);
return http.get<T.PostVO>(`${BASE_URL}/detail/${postId}`)
}
/**
*
*/
export function listPost(params?: T.PostPageQuery) {
return http.get<T.PostVO[]>(`${BASE_URL}/list`, params);
return http.get<T.PostVO[]>(`${BASE_URL}/list`, params)
}
/**
*
*/
export function pagePost(params?: T.PostPageQuery) {
return http.get<PageRes<T.PostVO[]>>(`${BASE_URL}/page`, params);
return http.get<PageRes<T.PostVO[]>>(`${BASE_URL}/page`, params)
}
/**
*
*/
export function updatePost(postId: string, data: T.PostUpdateReq) {
return http.put<any>(`${BASE_URL}/${postId}`, data);
return http.put<any>(`${BASE_URL}/${postId}`, data)
}
/**
*
*/
export function deletePost(postId: string) {
return http.del<any>(`${BASE_URL}/${postId}`)
}

View File

@ -4,7 +4,6 @@ import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/role'
const BASE_URL_NEW = '/role'
/** @desc 查询角色列表(已废弃) */
export function listRole(query: T.RoleQuery) {
@ -73,7 +72,7 @@ export function updateRolePermission(id: string, data: any) {
/** @desc 查询角色关联用户 */
export function listRoleUser(id: string, query: T.RoleUserPageQuery) {
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL_NEW}/${id}/user`, query)
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
@ -88,5 +87,5 @@ export function unassignFromUsers(userRoleIds: Array<string | number>) {
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL_NEW}/${id}/user`)
return http.get(`${BASE_URL}/${id}/user/id`)
}

View File

@ -564,113 +564,113 @@ export interface MessagePageQuery extends MessageQuery, PageQuery {
/** 新增菜单请求参数 */
export interface MenuAddReq {
menuName: string;
menuType: string;
orderNum: number;
parentId: string;
perms: string;
terminalType: string;
url: string;
visible: string;
menuName: string
menuType: string
orderNum: number
parentId: string
perms: string
terminalType: string
url: string
visible: string
}
/** 新菜单树查询参数 */
export interface MenuTreeQuery {
menuName?: string;
terminalType?: string;
menuName?: string
terminalType?: string
}
/** 新菜单详情响应类型 */
export interface MenuDetailResp {
menuId: string;
menuName: string;
menuType: string;
orderNum: number;
parentId: string;
perms: string;
url: string;
visible: string;
menuId: string
menuName: string
menuType: string
orderNum: number
parentId: string
perms: string
url: string
visible: string
}
/** 菜单更新请求参数 */
export interface MenuUpdateReq {
menuName: string;
menuType: string;
orderNum: number;
parentId: string;
perms: string;
terminalType: string;
url: string;
visible: string;
menuName: string
menuType: string
orderNum: number
parentId: string
perms: string
terminalType: string
url: string
visible: string
}
/** 新角色信息请求实体 */
export interface RoleAddReq {
remark: string;
roleCode: string;
roleKey: string;
roleName: string;
status: number;
remark: string
roleCode: string
roleKey: string
roleName: string
status: number
}
/** 角色信息更新请求实体 */
export interface RoleUpdateReq {
remark: string;
roleCode: string;
roleKey: string;
roleName: string;
status: number;
remark: string
roleCode: string
roleKey: string
roleName: string
status: number
}
/** 新角色信息响应实体 */
export interface RoleNewResp {
remark: string;
roleCode: string;
roleId: string;
roleKey: string;
roleName: string;
status: string;
isSystem?: boolean;
remark: string
roleCode: string
roleId: string
roleKey: string
roleName: string
status: string
isSystem?: boolean
}
/** 角色菜单绑定请求 */
export interface RoleBindMenuReq {
menuIds: string[];
roleId: string;
menuIds: string[]
roleId: string
}
/** 角色查询参数(新接口) */
export interface RoleNewQuery {
roleName?: string;
roleName?: string
}
// 岗位相关类型定义
export interface PostVO {
postId: string;
postName: string;
postSort: number;
remark: string;
status: string | number;
createTime?: string;
updateTime?: string;
postId: string
postName: string
postSort: number
remark: string
status: string | number
createTime?: string
updateTime?: string
}
export interface PostPageQuery {
postName?: string;
page?: number;
size?: number;
postName?: string
page?: number
size?: number
}
export interface PostAddReq {
postName: string;
postSort: number;
remark: string;
status: number;
postName: string
postSort: number
remark: string
status: number
}
export interface PostUpdateReq {
postName: string;
postSort: number;
remark: string;
status: number;
postName: string
postSort: number
remark: string
status: number
}

View File

@ -36,4 +36,4 @@ export function updateUserNew(userId: string, data: T.UserNewUpdateReq) {
/** @desc 删除用户信息 */
export function deleteUserNew(userId: string) {
return http.del(`${BASE_URL}/${userId}`)
}
}

View File

@ -0,0 +1,44 @@
import http from '@/utils/http'
import type * as T from '@/types/training.d'
const BASE_URL = '/training'
/** @desc 分页查询培训计划列表 */
export function pageTrainingPlan(query: T.TrainingPlanPageQuery) {
return http.get<T.TrainingPlanResp[]>(`${BASE_URL}/plan/page`, query)
}
/** @desc 查询培训计划列表 */
export function listTrainingPlan(query?: T.TrainingPlanPageQuery) {
return http.get<T.TrainingPlanResp[]>(`${BASE_URL}/plan/list`, query)
}
/** @desc 查询培训计划详情 */
export function getTrainingPlanDetail(planId: string) {
return http.get<T.TrainingPlanResp>(`${BASE_URL}/plan/detail/${planId}`)
}
/** @desc 新增培训计划 */
export function createTrainingPlan(data: T.TrainingPlanReq) {
return http.post(`${BASE_URL}/plan`, data)
}
/** @desc 更新培训计划 */
export function updateTrainingPlan(planId: string, data: T.TrainingPlanReq) {
return http.put(`${BASE_URL}/plan/${planId}`, data)
}
/** @desc 删除培训计划 */
export function deleteTrainingPlan(planId: string) {
return http.del(`${BASE_URL}/plan/${planId}`)
}
/** @desc 发布培训计划 */
export function publishTrainingPlan(planId: string) {
return http.put(`${BASE_URL}/plan/${planId}/publish`)
}
/** @desc 取消培训计划 */
export function cancelTrainingPlan(planId: string) {
return http.put(`${BASE_URL}/plan/${planId}/cancel`)
}

View File

@ -39,18 +39,18 @@ const breadcrumbList = computed(() => {
if (route.path.startsWith('/redirect/')) {
return []
}
const cloneRoutes = JSON.parse(JSON.stringify(routes)) as RouteLocationMatched[]
const obj = findTree(cloneRoutes, (i) => i.path === route.path)
//
const arr = obj ? obj.nodes.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false) : []
// home
if (home.value && !arr.some(item => item.path === home.value?.path)) {
if (home.value && !arr.some((item) => item.path === home.value?.path)) {
return [home.value, ...arr]
}
return arr
})

View File

@ -19,7 +19,7 @@
:loading="loadingImageSources"
/>
</a-form-item>
<a-form-item label="目标项目" required>
<a-tree-select
v-model="form.projectId"
@ -31,7 +31,7 @@
@change="onProjectChange"
/>
</a-form-item>
<a-form-item label="目标组件">
<a-select
v-model="form.componentId"
@ -40,11 +40,11 @@
allow-clear
/>
</a-form-item>
<a-form-item label="上传用户">
<a-input v-model="form.uploadUser" placeholder="请输入上传用户" />
</a-form-item>
<a-form-item label="位置信息">
<a-row :gutter="16">
<a-col :span="8">
@ -58,15 +58,15 @@
</a-col>
</a-row>
</a-form-item>
<a-form-item label="导入设置">
<a-checkbox-group v-model="form.settings">
<a-checkbox value="autoAnnotate">导入后自动标注</a-checkbox>
<a-checkbox value="overwrite">覆盖同名文件</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="标注类型" v-if="form.settings.includes('autoAnnotate')">
<a-form-item v-if="form.settings.includes('autoAnnotate')" label="标注类型">
<a-select
v-model="form.annotationTypes"
:options="defectTypeOptions"
@ -75,7 +75,7 @@
/>
</a-form-item>
</a-form>
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
@ -88,9 +88,9 @@
>
<template #upload-button>
<div class="upload-area">
<div class="upload-drag-icon">
<icon-upload size="48" />
</div>
<div class="upload-drag-icon">
<IconUpload size="48" />
</div>
<div class="upload-text">
<p>点击或拖拽图像文件到此区域</p>
<p class="upload-hint">支持 JPGPNGJPEG 格式可同时选择多个文件</p>
@ -99,14 +99,14 @@
</template>
</a-upload>
</div>
<!-- 文件列表 -->
<div class="file-list" v-if="fileList.length > 0">
<div v-if="fileList.length > 0" class="file-list">
<div class="list-header">
<h4>待导入文件 ({{ fileList.length }})</h4>
<a-button type="text" @click="clearFiles">
<template #icon>
<icon-delete />
<IconDelete />
</template>
清空
</a-button>
@ -133,16 +133,16 @@
@click="removeFile(index)"
>
<template #icon>
<icon-close />
<IconClose />
</template>
</a-button>
</div>
</div>
</div>
</div>
<!-- 导入进度 -->
<div class="import-progress" v-if="importing">
<div v-if="importing" class="import-progress">
<a-progress
:percent="importProgress"
:status="importStatus"
@ -150,17 +150,17 @@
/>
<p class="progress-text">{{ progressText }}</p>
</div>
<!-- 导入结果 -->
<div class="import-result" v-if="importResult">
<div v-if="importResult" class="import-result">
<a-alert
:type="importResult.failed.length > 0 ? 'warning' : 'success'"
:title="getResultTitle()"
:description="getResultDescription()"
show-icon
/>
<div class="result-details" v-if="importResult.failed.length > 0">
<div v-if="importResult.failed.length > 0" class="result-details">
<h4>失败文件列表:</h4>
<div class="failed-list">
<div
@ -180,22 +180,22 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconUpload,
IconDelete,
IconClose
import {
IconClose,
IconDelete,
IconUpload,
} from '@arco-design/web-vue/es/icon'
import {
getProjectTree,
importImages,
getImageSources
import {
getImageSources,
getProjectTree,
importImages,
} from '@/apis/industrial-image'
import type {
ProjectTreeNode,
import type {
ImageImportParams,
IndustrialImage,
ImageImportParams
ProjectTreeNode,
} from '@/apis/industrial-image/type'
interface Props {
@ -233,54 +233,54 @@ const form = ref({
latitude: '',
longitude: '',
settings: [] as string[],
annotationTypes: [] as string[]
annotationTypes: [] as string[],
})
const fileList = ref<FileItem[]>([])
const projectTree = ref<ProjectTreeNode[]>([])
const defectTypes = ref<Array<{ id: string; name: string; description?: string; color?: string }>>([])
const imageSources = ref<Array<{ id: string; name: string; code: string }>>([])
const defectTypes = ref<Array<{ id: string, name: string, description?: string, color?: string }>>([])
const imageSources = ref<Array<{ id: string, name: string, code: string }>>([])
const loadingImageSources = ref(false)
//
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
set: (value) => emit('update:visible', value),
})
const componentOptions = computed(() => {
const findComponents = (nodes: ProjectTreeNode[]): Array<{ label: string; value: string }> => {
const options: Array<{ label: string; value: string }> = []
nodes.forEach(node => {
const findComponents = (nodes: ProjectTreeNode[]): Array<{ label: string, value: string }> => {
const options: Array<{ label: string, value: string }> = []
nodes.forEach((node) => {
if (node.type === 'component' || node.type === 'blade' || node.type === 'tower') {
options.push({
label: node.name,
value: node.id
value: node.id,
})
}
if (node.children) {
options.push(...findComponents(node.children))
}
})
return options
}
return form.value.projectId ? findComponents(projectTree.value) : []
})
const defectTypeOptions = computed(() => {
return defectTypes.value.map(type => ({
return defectTypes.value.map((type) => ({
label: type.name,
value: type.id
value: type.id,
}))
})
const imageSourceOptions = computed(() => {
return imageSources.value.map(source => ({
return imageSources.value.map((source) => ({
label: source.name,
value: source.code
value: source.code,
}))
})
@ -326,25 +326,25 @@ const onProjectChange = (value: string) => {
const handleFileChange = (fileList: any) => {
const files = Array.from(fileList.target?.files || []) as File[]
files.forEach(file => {
files.forEach((file) => {
if (!file.type.startsWith('image/')) {
Message.warning(`文件 ${file.name} 不是图像文件`)
return
}
if (file.size > 10 * 1024 * 1024) { // 10MB
Message.warning(`文件 ${file.name} 大小超过10MB`)
return
}
const reader = new FileReader()
reader.onload = (e) => {
const fileItem: FileItem = {
file,
name: file.name,
size: file.size,
preview: e.target?.result as string
preview: e.target?.result as string,
}
fileList.value.push(fileItem)
}
@ -365,7 +365,7 @@ const formatFileSize = (bytes: number): string => {
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
}
const handleImport = async () => {
@ -373,25 +373,25 @@ const handleImport = async () => {
Message.warning('请选择图像来源')
return
}
if (!form.value.projectId) {
Message.warning('请选择目标项目')
return
}
if (fileList.value.length === 0) {
Message.warning('请选择要导入的图像文件')
return
}
importing.value = true
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = '正在导入图像...'
importResult.value = null
try {
const files = fileList.value.map(item => item.file)
const files = fileList.value.map((item) => item.file)
const params: ImageImportParams = {
imageSource: form.value.imageSource,
projectId: form.value.projectId,
@ -401,23 +401,23 @@ const handleImport = async () => {
latitude: form.value.latitude || undefined,
longitude: form.value.longitude || undefined,
autoAnnotate: form.value.settings.includes('autoAnnotate'),
annotationTypes: form.value.settings.includes('autoAnnotate') ? form.value.annotationTypes : undefined
annotationTypes: form.value.settings.includes('autoAnnotate') ? form.value.annotationTypes : undefined,
}
//
const progressInterval = setInterval(() => {
if (importProgress.value < 90) {
importProgress.value += 10
}
}, 200)
const res = await importImages(files, params)
clearInterval(progressInterval)
importProgress.value = 100
importStatus.value = 'success'
progressText.value = '导入完成!'
// API
const mockResult = {
success: files.map((file, index) => ({
@ -428,16 +428,15 @@ const handleImport = async () => {
type: file.type,
projectId: form.value.projectId,
componentId: form.value.componentId,
createTime: new Date().toISOString()
createTime: new Date().toISOString(),
})),
failed: []
failed: [],
}
importResult.value = mockResult
emit('importSuccess', mockResult)
Message.success(`成功导入 ${files.length} 个图像文件`)
} catch (error) {
console.error('导入失败:', error)
importProgress.value = 100
@ -454,7 +453,7 @@ const handleCancel = () => {
Message.warning('正在导入中,请稍后再试')
return
}
resetForm()
visible.value = false
}
@ -469,7 +468,7 @@ const resetForm = () => {
latitude: '',
longitude: '',
settings: [],
annotationTypes: []
annotationTypes: [],
}
fileList.value = []
importResult.value = null
@ -480,7 +479,7 @@ const resetForm = () => {
const getResultTitle = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return `导入成功!共导入 ${success.length} 个图像文件`
@ -491,7 +490,7 @@ const getResultTitle = () => {
const getResultDescription = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return '所有图像文件都已成功导入到指定项目中'
@ -518,11 +517,11 @@ watch(visible, (newValue) => {
max-height: 70vh;
overflow-y: auto;
}
.upload-section {
margin: 20px 0;
}
.upload-area {
display: flex;
flex-direction: column;
@ -534,24 +533,24 @@ watch(visible, (newValue) => {
background: #fafafa;
transition: all 0.3s;
cursor: pointer;
&:hover {
border-color: #1890ff;
background: #f0f8ff;
}
}
.upload-drag-icon {
margin-bottom: 16px;
color: #999;
}
.upload-text {
text-align: center;
p {
margin: 0;
&.upload-hint {
margin-top: 8px;
font-size: 12px;
@ -559,14 +558,14 @@ watch(visible, (newValue) => {
}
}
}
.file-list {
margin-top: 20px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
}
.list-header {
display: flex;
justify-content: space-between;
@ -574,37 +573,37 @@ watch(visible, (newValue) => {
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
h4 {
margin: 0;
font-size: 14px;
font-weight: 500;
}
}
.list-content {
max-height: 300px;
overflow-y: auto;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.file-info {
display: flex;
align-items: center;
flex: 1;
}
.file-preview {
width: 40px;
height: 40px;
@ -612,81 +611,81 @@ watch(visible, (newValue) => {
overflow: hidden;
border-radius: 4px;
border: 1px solid #e8e8e8;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.file-details {
flex: 1;
}
.file-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.file-size {
font-size: 12px;
color: #999;
}
.import-progress {
margin-top: 20px;
text-align: center;
}
.progress-text {
margin-top: 8px;
font-size: 14px;
color: #666;
}
.import-result {
margin-top: 20px;
}
.result-details {
margin-top: 16px;
h4 {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
}
.failed-list {
max-height: 150px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 6px;
}
.failed-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.filename {
font-size: 12px;
color: #333;
}
.error {
font-size: 12px;
color: #ff4d4f;
}
}
</style>
</style>

View File

@ -8,7 +8,7 @@
<button class="dialog-close-btn" @click="closeDialog">×</button>
</div>
</div>
<div class="dialog-body">
<!-- 左侧步骤指示器 -->
<div class="steps-sidebar">
@ -28,22 +28,22 @@
<span class="step-text">设置信息</span>
</div>
</div>
<!-- 右侧内容区域 -->
<div class="dialog-content-container">
<!-- 步骤1: 选择部件 -->
<div v-if="currentStep === 1" class="dialog-content">
<div class="select-part-content">
<div class="section-title">选择要导入图像的部件</div>
<div class="parts-container">
<div
v-for="part in availableParts"
:key="getPartId(part)"
<div
v-for="part in availableParts"
:key="getPartId(part)"
class="part-item"
:class="{ selected: String(selectedPartId) === String(getPartId(part)) }"
@click="selectPart(part)"
:title="`部件ID: ${getPartId(part)}, 选中: ${String(selectedPartId) === String(getPartId(part))}`"
@click="selectPart(part)"
>
<div class="part-icon">
<svg v-if="part.partType === 'engine'" width="40" height="40" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
@ -66,8 +66,8 @@
<div class="part-name">{{ getPartName(part) }}</div>
</div>
</div>
<div class="part-info" v-if="selectedPart">
<div v-if="selectedPart" class="part-info">
<div class="info-line">
<div class="info-label">部件:</div>
<div class="info-value">{{ getPartName(selectedPart) }}</div>
@ -79,38 +79,38 @@
</div>
</div>
</div>
<!-- 步骤2: 导入图像 -->
<div v-if="currentStep === 2" class="dialog-content">
<div class="import-image-content">
<div class="section-title">导入图像到部件"{{ selectedPart ? getPartName(selectedPart) : '' }}"</div>
<div class="image-actions">
<button class="action-button" @click="handleAddImages">
<span class="button-icon">+</span>
添加图像
</button>
<button class="action-button" @click="handleRemoveImages" :disabled="!hasSelectedImages">
<button class="action-button" :disabled="!hasSelectedImages" @click="handleRemoveImages">
<span class="button-icon">-</span>
移除图像
</button>
<!-- 隐藏的文件输入框 -->
<input
type="file"
ref="fileInput"
accept="image/*"
style="display: none;"
<input
ref="fileInput"
type="file"
accept="image/*"
style="display: none;"
multiple
@change="handleFileSelected"
/>
</div>
<div class="image-list-container">
<table class="image-list">
<thead>
<tr>
<th class="checkbox-column">
<input type="checkbox" @change="toggleSelectAll" :checked="allImagesSelected">
<input type="checkbox" :checked="allImagesSelected" @change="toggleSelectAll">
</th>
<th class="preview-column">预览</th>
<th>图像名称</th>
@ -123,7 +123,7 @@
<tbody>
<tr v-for="(image, index) in importImages" :key="index" @click="toggleImageSelection(image)">
<td>
<input type="checkbox" v-model="image.selected" @click.stop>
<input v-model="image.selected" type="checkbox" @click.stop>
</td>
<td class="preview-cell">
<img v-if="image.previewUrl" :src="image.previewUrl" class="preview-thumbnail" alt="预览">
@ -140,22 +140,22 @@
</div>
</div>
</div>
<!-- 步骤3: 设置图像采集信息 -->
<div v-if="currentStep === 3" class="dialog-content">
<div class="image-info-content">
<div class="section-title">步骤3: 设置图像采集信息</div>
<div class="form-container">
<div class="form-row">
<div class="form-label">拍摄时间范围</div>
<div class="form-input datetime-range">
<input type="text" v-model="imageInfo.startTime" placeholder="开始时间">
<input v-model="imageInfo.startTime" type="text" placeholder="开始时间">
<span class="range-separator"></span>
<input type="text" v-model="imageInfo.endTime" placeholder="结束时间">
<input v-model="imageInfo.endTime" type="text" placeholder="结束时间">
</div>
</div>
<div class="form-row">
<div class="form-label">天气</div>
<div class="form-input">
@ -167,35 +167,35 @@
</select>
</div>
</div>
<div class="form-row">
<div class="form-label">环境温度(°C)</div>
<div class="form-input temperature-range">
<div class="range-input-group">
<button class="range-btn" @click="imageInfo.minTemperature = Math.max(0, imageInfo.minTemperature - 1)">-</button>
<input type="number" v-model="imageInfo.minTemperature" step="0.1" min="0" max="50">
<input v-model="imageInfo.minTemperature" type="number" step="0.1" min="0" max="50">
<button class="range-btn" @click="imageInfo.minTemperature = Math.min(50, imageInfo.minTemperature + 1)">+</button>
</div>
<span class="range-separator"></span>
<div class="range-input-group">
<button class="range-btn" @click="imageInfo.maxTemperature = Math.max(0, imageInfo.maxTemperature - 1)">-</button>
<input type="number" v-model="imageInfo.maxTemperature" step="0.1" min="0" max="50">
<input v-model="imageInfo.maxTemperature" type="number" step="0.1" min="0" max="50">
<button class="range-btn" @click="imageInfo.maxTemperature = Math.min(50, imageInfo.maxTemperature + 1)">+</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-label">湿度(%)</div>
<div class="form-input">
<div class="range-input-group">
<button class="range-btn" @click="imageInfo.humidity = Math.max(0, imageInfo.humidity - 1)">-</button>
<input type="number" v-model="imageInfo.humidity" min="0" max="100">
<input v-model="imageInfo.humidity" type="number" min="0" max="100">
<button class="range-btn" @click="imageInfo.humidity = Math.min(100, imageInfo.humidity + 1)">+</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-label">风力</div>
<div class="form-input">
@ -212,43 +212,43 @@
</select>
</div>
</div>
<div class="form-row">
<div class="form-label">拍摄方式</div>
<div class="form-input capture-method">
<label class="radio-option">
<input type="radio" v-model="imageInfo.captureMethod" value="无人机航拍">
<input v-model="imageInfo.captureMethod" type="radio" value="无人机航拍">
<span class="radio-label">无人机航拍</span>
</label>
<label class="radio-option">
<input type="radio" v-model="imageInfo.captureMethod" value="人工拍摄">
<input v-model="imageInfo.captureMethod" type="radio" value="人工拍摄">
<span class="radio-label">人工拍摄</span>
</label>
</div>
</div>
<div class="form-row">
<div class="form-label">拍摄距离()</div>
<div class="form-input">
<div class="range-input-group">
<button class="range-btn" @click="imageInfo.captureDistance = Math.max(0, imageInfo.captureDistance - 1)">-</button>
<input type="number" v-model="imageInfo.captureDistance" min="0">
<input v-model="imageInfo.captureDistance" type="number" min="0">
<button class="range-btn" @click="imageInfo.captureDistance = imageInfo.captureDistance + 1">+</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-label">采集员</div>
<div class="form-input">
<input type="text" v-model="imageInfo.operator">
<input v-model="imageInfo.operator" type="text">
</div>
</div>
<div class="form-row">
<div class="form-label">相机型号</div>
<div class="form-input">
<input type="text" v-model="imageInfo.cameraModel">
<input v-model="imageInfo.cameraModel" type="text">
</div>
</div>
</div>
@ -256,24 +256,30 @@
</div>
</div>
</div>
<div class="dialog-footer">
<button
v-if="currentStep > 1"
class="dialog-button"
<button
v-if="currentStep > 1"
class="dialog-button"
@click="currentStep--"
>上一步</button>
<button
v-if="currentStep < 3"
class="dialog-button"
@click="nextStep"
>
上一步
</button>
<button
v-if="currentStep < 3"
class="dialog-button"
:disabled="!canGoNext"
>下一步</button>
<button
v-if="currentStep === 3"
class="dialog-button primary"
@click="nextStep"
>
下一步
</button>
<button
v-if="currentStep === 3"
class="dialog-button primary"
@click="finishImport"
>完成导入</button>
>
完成导入
</button>
<button class="dialog-button" @click="closeDialog">取消</button>
</div>
</div>
@ -281,7 +287,7 @@
</template>
<script setup lang="ts">
import { ref, computed, reactive, onBeforeUnmount } from 'vue'
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
//
@ -352,7 +358,7 @@ const selectedPartId = ref('')
//
const selectedPart = computed(() => {
if (!selectedPartId.value) return null
return props.availableParts?.find(part => String(getPartId(part)) === String(selectedPartId.value))
return props.availableParts?.find((part) => String(getPartId(part)) === String(selectedPartId.value))
})
//
@ -360,8 +366,8 @@ const importImages = ref<ImportImage[]>([])
//
const imageInfo = reactive<ImageInfo>({
startTime: formatCurrentDate() + ' 00:00',
endTime: formatCurrentDate() + ' 23:59',
startTime: `${formatCurrentDate()} 00:00`,
endTime: `${formatCurrentDate()} 23:59`,
weather: '晴天',
humidity: 50,
minTemperature: 15,
@ -371,7 +377,7 @@ const imageInfo = reactive<ImageInfo>({
captureMethod: '无人机航拍',
captureDistance: 50,
operator: '',
cameraModel: 'ILCE-7RM4'
cameraModel: 'ILCE-7RM4',
})
//
@ -416,12 +422,12 @@ function getPartName(part: any): string {
//
function selectPart(part: any) {
const partId = getPartId(part)
if (!part || !partId) {
console.error('部件数据无效:', part)
return
}
selectedPartId.value = String(partId)
}
@ -434,10 +440,10 @@ function handleAddImages() {
function handleFileSelected(event: Event) {
const target = event.target as HTMLInputElement
if (target.files && target.files.length > 0) {
const newImages: ImportImage[] = Array.from(target.files).map(file => {
const newImages: ImportImage[] = Array.from(target.files).map((file) => {
// URL
const previewUrl = URL.createObjectURL(file)
//
return {
file,
@ -448,14 +454,14 @@ function handleFileSelected(event: Event) {
timestamp: new Date().toISOString(),
pixelSize: '155.00',
selected: false,
previewUrl
previewUrl,
}
})
//
importImages.value = [...importImages.value, ...newImages]
}
//
if (fileInput.value) fileInput.value.value = ''
}
@ -463,13 +469,13 @@ function handleFileSelected(event: Event) {
//
function handleRemoveImages() {
// URL
importImages.value.filter(image => image.selected).forEach(image => {
importImages.value.filter((image) => image.selected).forEach((image) => {
if (image.previewUrl) {
URL.revokeObjectURL(image.previewUrl)
}
})
importImages.value = importImages.value.filter(image => !image.selected)
importImages.value = importImages.value.filter((image) => !image.selected)
}
//
@ -480,17 +486,17 @@ function toggleImageSelection(image: ImportImage) {
// /
function toggleSelectAll(event: Event) {
const checked = (event.target as HTMLInputElement).checked
importImages.value.forEach(image => image.selected = checked)
importImages.value.forEach((image) => image.selected = checked)
}
//
const hasSelectedImages = computed(() => {
return importImages.value.some(image => image.selected)
return importImages.value.some((image) => image.selected)
})
//
const allImagesSelected = computed(() => {
return importImages.value.length > 0 && importImages.value.every(image => image.selected)
return importImages.value.length > 0 && importImages.value.every((image) => image.selected)
})
//
@ -525,33 +531,33 @@ function finishImport() {
Message.error('请选择部件')
return
}
if (importImages.value.length === 0) {
Message.error('请添加图像')
return
}
//
const files = importImages.value.map(image => image.file!).filter(Boolean)
const files = importImages.value.map((image) => image.file!).filter(Boolean)
//
const partData = {
partId: getPartId(selectedPart.value),
id: getPartId(selectedPart.value), //
name: getPartName(selectedPart.value),
partName: getPartName(selectedPart.value), //
partType: selectedPart.value.partType
partType: selectedPart.value.partType,
}
//
emit('import-success', {
part: partData,
images: files,
imageInfo: { ...imageInfo }
imageInfo: { ...imageInfo },
})
Message.success('图像导入成功')
//
closeDialog()
}
@ -567,19 +573,19 @@ function closeDialog() {
function resetState() {
currentStep.value = 1
selectedPartId.value = ''
// URL
importImages.value.forEach(image => {
importImages.value.forEach((image) => {
if (image.previewUrl) {
URL.revokeObjectURL(image.previewUrl)
}
})
importImages.value = []
//
Object.assign(imageInfo, {
startTime: formatCurrentDate() + ' 00:00',
endTime: formatCurrentDate() + ' 23:59',
startTime: `${formatCurrentDate()} 00:00`,
endTime: `${formatCurrentDate()} 23:59`,
weather: '晴天',
humidity: 70,
minTemperature: 20,
@ -587,14 +593,14 @@ function resetState() {
windPower: 0,
captureMethod: '无人机拍摄',
captureDistance: 15,
operator: ''
operator: '',
})
}
// URL
onBeforeUnmount(() => {
// URL
importImages.value.forEach(image => {
importImages.value.forEach((image) => {
if (image.previewUrl) {
URL.revokeObjectURL(image.previewUrl)
}
@ -1127,4 +1133,4 @@ onBeforeUnmount(() => {
.dialog-button.primary:hover {
background-color: #2563eb;
}
</style>
</style>

View File

@ -1,4 +1,4 @@
import IndustrialImageList from './index.vue'
export type { IndustrialImage } from './index.vue'
export default IndustrialImageList
export default IndustrialImageList

View File

@ -1,13 +1,13 @@
<template>
<div class="industrial-image-list" :class="{ 'collapsed': isCollapsed }">
<div class="header-actions" v-if="!isCollapsed">
<div class="industrial-image-list" :class="{ collapsed: isCollapsed }">
<div v-if="!isCollapsed" class="header-actions">
<slot name="header-left">
<a-button v-if="showImportButton" type="primary" @click="handleImportImages">
<template #icon><icon-upload /></template>
<template #icon><IconUpload /></template>
导入图像
</a-button>
</slot>
<div class="search-bar" v-if="showSearch">
<div v-if="showSearch" class="search-bar">
<a-input-search
v-model="searchKeyword"
placeholder="输入关键字搜索"
@ -18,24 +18,24 @@
</div>
<slot name="header-right"></slot>
<div class="collapse-button">
<a-button
type="text"
<a-button
type="text"
@click="toggleCollapse"
>
<template #icon>
<icon-up />
<IconUp />
</template>
收起
</a-button>
</div>
</div>
<div class="image-grid" v-show="!isCollapsed">
<div v-show="!isCollapsed" class="image-grid">
<div v-if="imageList.length === 0" class="empty-data">
<icon-image class="empty-icon" />
<IconImage class="empty-icon" />
<p>{{ emptyText }}</p>
</div>
<div v-else class="image-thumbnails">
<div
v-for="image in imageList"
@ -45,14 +45,14 @@
@click="handleImageSelect(image)"
>
<div class="thumbnail-image">
<img
:src="getImageUrl(image.imagePath)"
:alt="image.imageName"
<img
:src="getImageUrl(image.imagePath)"
:alt="image.imageName"
@error="handleImageError"
@load="handleImageLoad"
/>
<div class="image-placeholder" v-if="!image.imagePath">
<icon-image />
<div v-if="!image.imagePath" class="image-placeholder">
<IconImage />
<span>暂无图像</span>
</div>
<div class="thumbnail-overlay">
@ -62,13 +62,13 @@
</div>
<div class="image-actions">
<a-button v-if="showPreviewAction" type="text" size="small" @click.stop="handleImagePreview(image)">
<icon-eye />
<IconEye />
</a-button>
<a-button v-if="showProcessAction" type="text" size="small" @click.stop="handleImageProcess(image)">
<icon-settings />
<IconSettings />
</a-button>
<a-button v-if="showDeleteAction" type="text" size="small" status="danger" @click.stop="handleImageDelete(image)">
<icon-delete />
<IconDelete />
</a-button>
<slot name="item-actions" :image="image"></slot>
</div>
@ -81,7 +81,7 @@
<span v-if="image.defectCount" class="defect-count">缺陷: {{ image.defectCount }}</span>
<slot name="item-meta" :image="image"></slot>
</div>
<div class="thumbnail-extra" v-if="image.partName || image.shootingTime">
<div v-if="image.partName || image.shootingTime" class="thumbnail-extra">
<span v-if="image.partName" class="part-name">{{ image.partName }}</span>
<span v-if="image.shootingTime" class="capture-time">{{ formatTime(image.shootingTime) }}</span>
</div>
@ -90,15 +90,15 @@
</div>
</div>
</div>
<!-- 收起状态下的展开按钮 -->
<div v-if="isCollapsed" class="expand-button-container">
<a-button
<a-button
type="primary"
@click="toggleCollapse"
>
<template #icon>
<icon-down />
<IconDown />
</template>
展开图像列表
</a-button>
@ -108,14 +108,14 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
IconUpload,
IconImage,
IconEye,
IconSettings,
import {
IconDelete,
IconDown,
IconEye,
IconImage,
IconSettings,
IconUp,
IconDown
IconUpload,
} from '@arco-design/web-vue/es/icon'
export interface IndustrialImage {
@ -133,40 +133,40 @@ export interface IndustrialImage {
const props = defineProps({
imageList: {
type: Array as () => IndustrialImage[],
default: () => []
default: () => [],
},
selectedImageId: {
type: String,
default: ''
default: '',
},
baseUrl: {
type: String,
default: 'http://pms.dtyx.net:9158'
default: 'http://localhost:8080',
},
emptyText: {
type: String,
default: '暂无图像数据'
default: '暂无图像数据',
},
showImportButton: {
type: Boolean,
default: true
default: true,
},
showSearch: {
type: Boolean,
default: true
default: true,
},
showPreviewAction: {
type: Boolean,
default: true
default: true,
},
showProcessAction: {
type: Boolean,
default: true
default: true,
},
showDeleteAction: {
type: Boolean,
default: true
}
default: true,
},
})
const emit = defineEmits<{
@ -252,7 +252,7 @@ const formatTime = (timeString: string): string => {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
})
} catch {
return timeString
@ -297,7 +297,7 @@ const formatTime = (timeString: string): string => {
margin-left: auto;
}
}
/* 收起状态下的展开按钮容器 */
.expand-button-container {
position: absolute;
@ -312,21 +312,21 @@ const formatTime = (timeString: string): string => {
overflow-y: auto;
padding: 16px;
height: calc(100% - 60px); /* 减去header-actions的高度 */
//
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
&:hover {
background: #94a3b8;
}
@ -526,4 +526,4 @@ const formatTime = (timeString: string): string => {
}
}
}
</style>
</style>

View File

@ -1,102 +1,105 @@
<template>
<div class="turbine-grid-container">
<div class="turbine-grid">
<div v-for="turbine in turbines" :key="turbine.id" class="turbine-card"
:class="getStatusClass(turbine.status)">
<div class="turbine-status-badge" :class="`status-${turbine.status}`">
{{ getStatusText(turbine.status) }}
</div>
<div class="turbine-icon">
<img src="/static/images/wind-turbine-icon.svg" alt="风机图标" class="turbine-image" />
</div>
<div class="turbine-info">
<div class="turbine-number">
<a-input v-model="turbine.turbineNo" size="small" class="turbine-input" placeholder="请输入机组编号"
@change="handleTurbineNoChange(turbine)" />
</div>
</div>
<div class="turbine-actions">
<a-button type="text" size="mini" @click="openMapModal(turbine)" title="地图选点">
<template #icon><icon-location /></template>
</a-button>
<a-button type="text" size="mini" @click="editTurbine(turbine)" title="编辑">
<template #icon><icon-edit /></template>
</a-button>
</div>
</div>
<div class="turbine-grid-container">
<div class="turbine-grid">
<div
v-for="turbine in turbines" :key="turbine.id" class="turbine-card"
:class="getStatusClass(turbine.status)"
>
<div class="turbine-status-badge" :class="`status-${turbine.status}`">
{{ getStatusText(turbine.status) }}
</div>
<!-- 添加新机组按钮 -->
<div v-if="showAddButton" class="turbine-card add-turbine-card" @click="addTurbine">
<div class="add-icon">
<icon-plus />
</div>
<div class="add-text">添加机组</div>
<div class="turbine-icon">
<img src="/static/images/wind-turbine-icon.svg" alt="风机图标" class="turbine-image" />
</div>
<div class="turbine-info">
<div class="turbine-number">
<a-input
v-model="turbine.turbineNo" size="small" class="turbine-input" placeholder="请输入机组编号"
@change="handleTurbineNoChange(turbine)"
/>
</div>
</div>
<div class="turbine-actions">
<a-button type="text" size="mini" title="地图选点" @click="openMapModal(turbine)">
<template #icon><icon-location /></template>
</a-button>
<a-button type="text" size="mini" title="编辑" @click="editTurbine(turbine)">
<template #icon><icon-edit /></template>
</a-button>
</div>
</div>
</div>
<!-- 添加新机组按钮 -->
<div v-if="showAddButton" class="turbine-card add-turbine-card" @click="addTurbine">
<div class="add-icon">
<icon-plus />
</div>
<div class="add-text">添加机组</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0: , 1: , 2:
lat?: number
lng?: number
id: number
turbineNo: string
status: 0 | 1 | 2 // 0: , 1: , 2:
lat?: number
lng?: number
}
interface Props {
turbines: Turbine[]
showAddButton?: boolean
turbines: Turbine[]
showAddButton?: boolean
}
interface Emits {
(e: 'update:turbines', turbines: Turbine[]): void
(e: 'turbine-change', turbine: Turbine): void
(e: 'add-turbine'): void
(e: 'update:turbines', turbines: Turbine[]): void
(e: 'turbine-change', turbine: Turbine): void
(e: 'add-turbine'): void
}
const props = withDefaults(defineProps<Props>(), {
showAddButton: false
showAddButton: false,
})
const emit = defineEmits<Emits>()
const getStatusText = (status: number) => {
const statusMap = {
0: '待施工',
1: '施工中',
2: '已完成'
}
return statusMap[status] || '未知状态'
const statusMap = {
0: '待施工',
1: '施工中',
2: '已完成',
}
return statusMap[status] || '未知状态'
}
const getStatusClass = (status: number) => {
return `status-${status}`
return `status-${status}`
}
const handleTurbineNoChange = (turbine: Turbine) => {
emit('turbine-change', turbine)
emit('update:turbines', props.turbines)
emit('turbine-change', turbine)
emit('update:turbines', props.turbines)
}
const openMapModal = (turbine: Turbine) => {
Message.info(`地图选点功能待开发,当前机组编号:${turbine.turbineNo}`)
Message.info(`地图选点功能待开发,当前机组编号:${turbine.turbineNo}`)
}
const editTurbine = (turbine: Turbine) => {
//
Message.info(`编辑机组:${turbine.turbineNo}`)
//
Message.info(`编辑机组:${turbine.turbineNo}`)
}
const addTurbine = () => {
emit('add-turbine')
emit('add-turbine')
}
</script>

View File

@ -3,7 +3,7 @@
<a-row :gutter="16">
<a-col :span="24" :md="17">
<GiTable
v-model:selectedKeys="selectedKeys"
v-model:selected-keys="selectedKeys"
row-key="id"
:data="dataList"
:columns="listColumns"

View File

@ -225,9 +225,9 @@ export default {
//
const captchaVerification = secretKey.value
? encryptByAes(
`${backToken.value}---${JSON.stringify(checkPosArr)}`,
secretKey.value,
)
`${backToken.value}---${JSON.stringify(checkPosArr)}`,
secretKey.value,
)
: `${backToken.value}---${JSON.stringify(checkPosArr)}`
const data = {
captchaType: captchaType.value,

View File

@ -231,7 +231,7 @@ export default {
) {
move_block_left
= barArea.value.offsetWidth
- Number.parseInt(blockSize.value.width, 10) / 2 - 2
- Number.parseInt(blockSize.value.width, 10) / 2 - 2
}
if (move_block_left <= 0) {
move_block_left = Number.parseInt(blockSize.value.width, 10) / 2
@ -281,9 +281,9 @@ export default {
captchaType: captchaType.value,
pointJson: secretKey.value
? encryptByAes(
JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
secretKey.value,
)
JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
secretKey.value,
)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value,
}
@ -303,21 +303,21 @@ export default {
}
passFlag.value = true
tipWords.value = `${(
(endMovetime.value - startMoveTime.value)
/ 1000
(endMovetime.value - startMoveTime.value)
/ 1000
).toFixed(2)}s验证成功`
const captchaVerification = secretKey.value
? encryptByAes(
`${backToken.value}---${JSON.stringify({
x: moveLeftDistance,
y: 5.0,
})}`,
secretKey.value,
)
`${backToken.value}---${JSON.stringify({
x: moveLeftDistance,
y: 5.0,
})}`,
secretKey.value,
)
: `${backToken.value}---${JSON.stringify({
x: moveLeftDistance,
y: 5.0,
})}`
x: moveLeftDistance,
y: 5.0,
})}`
setTimeout(() => {
tipWords.value = ''
proxy.$parent.closeBox()

View File

@ -11,18 +11,18 @@ export function useDept(options?: { onSuccess?: () => void }) {
try {
loading.value = true
const res = await getDeptTree({ deptName })
// 处理部门树数据确保有title字段用于显示
const processDeptData = (data: any[]): TreeNodeData[] => {
if (!data || !data.length) return []
return data.map(item => ({
return data.map((item) => ({
key: item.deptId,
title: item.deptName || '未命名部门', // 将deptName映射为title
children: item.children ? processDeptData(item.children) : []
title: item.deptName || '未命名部门', // 将deptName映射为title
children: item.children ? processDeptData(item.children) : [],
}))
}
deptList.value = processDeptData(res.data || [])
options?.onSuccess && options.onSuccess()
} finally {

View File

@ -2,7 +2,7 @@ import { listPost } from '@/apis/system/post'
import type { PostVO } from '@/apis/system/type'
export function usePost() {
const postList = ref<{ label: string; value: string }[]>([])
const postList = ref<{ label: string, value: string }[]>([])
const loading = ref(false)
// 获取岗位列表
@ -24,6 +24,6 @@ export function usePost() {
return {
postList,
loading,
getPostList
getPostList,
}
}
}

View File

@ -11,16 +11,16 @@ export function useRole(options?: { onSuccess?: () => void }) {
try {
loading.value = true
const res = await fetchRoleList()
// 将新的角色数据格式转换为表单需要的 LabelValueState 格式
if (res && res.data) {
roleList.value = (res.data || []).map(role => ({
roleList.value = (res.data || []).map((role) => ({
label: role.roleName,
value: role.roleId,
disabled: role.status !== '1' // 假设状态为1表示启用
disabled: role.status !== '1', // 假设状态为1表示启用
}))
}
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false

View File

@ -19,7 +19,6 @@ import Asider from './components/Asider/index.vue'
import Header from './components/Header/index.vue'
import Main from './components/Main.vue'
import Tabs from './components/Tabs/index.vue'
import GiFooter from '@/components/GiFooter/index.vue'
import NoticePopup from '@/views/user/message/components/NoticePopup.vue'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'

View File

@ -19,7 +19,6 @@
<script setup lang="ts">
import Menu from '../Menu/index.vue'
import Logo from '../Logo.vue'
import WwAds from '../WwAds.vue'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'

View File

@ -76,11 +76,10 @@
<script setup lang="ts">
import { Modal } from '@arco-design/web-vue'
import { useFullscreen } from '@vueuse/core'
import { onMounted, ref, nextTick } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import Message from './Message.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
import { getUnreadMessageCount } from '@/apis'
import { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { useBreakpoint, useDevice } from '@/hooks'
@ -107,17 +106,17 @@ const initWebSocket = (token: string) => {
if (initTimer) {
clearTimeout(initTimer)
}
initTimer = setTimeout(() => {
//
if (socket) {
socket.close()
socket = null
}
try {
socket = new WebSocket(`${import.meta.env.VITE_API_WS_URL}/websocket?token=${token}`)
socket.onopen = () => {
// console.log('WebSocket connection opened')
}
@ -140,7 +139,7 @@ const initWebSocket = (token: string) => {
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
}
initTimer = null
}, 100) // 100ms
}

View File

@ -18,10 +18,9 @@ const props = withDefaults(defineProps<Props>(), {
})
const appStore = useAppStore()
// const title = computed(() => appStore.getTitle())
const title = "数智平台"
const logo = "/logo.png"
//computed(() => appStore.getLogo())
const title = '数智平台'
const logo = '/logo.png'
// computed(() => appStore.getLogo())
interface Props {
collapsed?: boolean

View File

@ -52,7 +52,7 @@ watchEffect(() => {
const children = props.item?.children?.length ? props.item.children : []
//
const showingChildren = children.filter((i) => i.meta?.hidden === false)
if (showingChildren.length) {
// hidden: false
onlyOneChild.value = showingChildren[showingChildren.length - 1]

View File

@ -2,7 +2,7 @@ import { createApp } from 'vue'
import ArcoVue, { Card, Drawer, Modal } from '@arco-design/web-vue'
import '@/styles/arco-ui/index.less'
// import '@arco-themes/vue-gi-demo/index.less'
// import '@arco-design/web-vue/dist/arco.css'
import '@arco-design/web-vue/dist/arco.css'
// 额外引入 Arco Design Icon图标库
import ArcoVueIcon from '@arco-design/web-vue/es/icon'

View File

@ -5,6 +5,12 @@ const Layout = () => import('@/layout/index.vue')
/** 系统路由 */
export const systemRoutes: RouteRecordRaw[] = [
{
path: '/test-console',
name: 'TestConsole',
component: () => import('@/test-console.vue'),
meta: { title: 'Console测试', hidden: true },
},
{
path: '/login',
name: 'Login',
@ -26,334 +32,299 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
// ],
// },
{
path: '/regulation',
name: 'Regulation',
component: Layout,
redirect: '/regulation/system-regulation',
meta: { title: '制度管理', icon: 'file-text', hidden: false, sort: 1.5 },
children: [
{
path: '/regulation/system-regulation',
name: 'SystemRegulation',
component: () => import('@/views/regulation/repository.vue'),
meta: { title: '制度公告', icon: 'file-text', hidden: false },
},
{
path: '/regulation/process-management',
name: 'ProcessManagement',
component: () => import('@/views/regulation/confirm.vue'),
meta: { title: '制度公示', icon: 'workflow', hidden: false },
},
{
path: '/regulation/proposal',
name: 'RegulationProposal',
component: () => import('@/views/regulation/proposal/index.vue'),
meta: { title: '制度提案', icon: 'edit', hidden: false },
},
{
path: '/regulation/type',
name: 'RegulationType',
component: () => import('@/views/regulation/type/index.vue'),
meta: { title: '制度类型', icon: 'tag', hidden: false },
},
],
},
{
path: '/organization',
name: 'Organization',
component: Layout,
redirect: '/organization/dept',
redirect: '/organization/hr/member',
meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
children: [
{
path: '/organization/user',
name: 'OrganizationUser',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', icon: 'user', hidden: false, sort: 2.25 },
path: '/organization/hr',
name: 'HRManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/member',
meta: { title: '人员管理', icon: 'user', hidden: false },
children: [
{
path: '/organization/hr/member',
name: 'HRMember',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '成员', icon: 'user', hidden: false },
},
{
path: '/organization/hr/dept',
name: 'HRDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门', icon: 'dept', hidden: false },
},
{
path: '/organization/hr/workload',
name: 'HRWorkload',
component: () => import('@/views/hr/workload/index.vue'),
meta: { title: '工作量', icon: 'workload', hidden: false },
},
{
path: '/organization/hr/attendance',
name: 'HRAttendance',
component: () => import('@/views/hr/attendance/index.vue'),
meta: { title: '考勤', icon: 'attendance', hidden: false },
},
{
path: '/organization/hr/performance',
name: 'HRPerformance',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '绩效', icon: 'performance', hidden: false },
children: [
{
path: '/organization/hr/performance/dimention',
name: 'Dimention',
component: () => import('@/views/performance/setting/index.vue'),
meta: { title: '绩效维度', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/rule',
name: 'Rule',
component: () => import('@/views/performance/rule.vue'),
meta: { title: '绩效细则', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/my',
name: 'MyPerformance',
component: () => import('@/views/performance/my.vue'),
meta: { title: '我的绩效', icon: 'performance', hidden: false },
},
],
},
{
path: '/organization/hr/salary',
name: 'HRSalary',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/overview',
meta: { title: '工资', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/overview',
name: 'HRSalaryOverview',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '工资概览', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/payroll',
name: 'Payroll',
component: () => import('@/views/salary-management/index.vue'),
meta: { title: '工资单', icon: 'salary', hidden: false },
},
],
},
],
},
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// }
// ]
// },
{
path: '/organization/hr/salary/system-insurance/health-management',
name: 'HRSystemHealthManagement',
component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
meta: { title: '健康档案管理', icon: 'heart', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance',
name: 'HRSystemInsurance',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/system-insurance/overview',
meta: { title: '人员保险', icon: 'settings', hidden: false },
children: [
{
path: '/organization/hr/salary/system-insurance/overview',
name: 'HRSystemInsuranceOverview',
component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/management',
name: 'HRSystemInsuranceManagement',
component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
meta: { title: '保险管理', icon: 'shield', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/file-management',
name: 'HRSystemFileManagement',
component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
meta: { title: '保单文件管理', icon: 'file', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/company-management',
name: 'HRSystemCompanyManagement',
component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
meta: { title: '保险公司管理', icon: 'building', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/type-management',
name: 'HRSystemTypeManagement',
component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
meta: { title: '保险类型管理', icon: 'category', hidden: false },
},
],
},
{
path: '/organization/hr/salary/certification',
name: 'HRCertification',
component: () => import('@/views/hr/salary/certification/index.vue'),
meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
},
{
path: '/organization/hr/contribution',
name: 'HRContribution',
component: () => import('@/views/hr/contribution/index.vue'),
meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
},
],
},
{
path: '/organization/dept',
name: 'OrganizationDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门管理', icon: 'mind-mapping', hidden: false, sort: 2.5 },
path: '/organization/role',
name: 'OrganizationRole',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role', hidden: false },
},
{
path: '/organization/post',
name: 'OrganizationPost',
component: () => import('@/views/system/post/index.vue'),
meta: { title: '岗位管理', icon: 'nav', hidden: false, sort: 2.75 },
},
// {
// path: '/organization/hr/workload',
// name: 'HRWorkload',
// component: () => import('@/views/hr/workload/index.vue'),
// meta: { title: '任务管理', icon: 'bookmark', hidden: false },
// },
],
},
// {
// path: '/organization',
// name: 'Organization',
// component: Layout,
// redirect: '/organization/hr/member',
// meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
// children: [
// {
// path: '/organization/hr',
// name: 'HRManagement',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/member',
// meta: { title: '人员管理', icon: 'user', hidden: false },
// children: [
// {
// path: '/organization/hr/member',
// name: 'HRMember',
// component: () => import('@/views/system/user/index.vue'),
// meta: { title: '成员', icon: 'user', hidden: false },
// },
// {
// path: '/organization/hr/dept',
// name: 'HRDept',
// component: () => import('@/views/system/dept/index.vue'),
// meta: { title: '部门', icon: 'dept', hidden: false },
// },
// {
// path: '/organization/hr/workload',
// name: 'HRWorkload',
// component: () => import('@/views/hr/workload/index.vue'),
// meta: { title: '任务管理', icon: 'workload', hidden: false },
// },
// {
// path: '/organization/hr/attendance',
// name: 'HRAttendance',
// component: () => import('@/views/hr/attendance/index.vue'),
// meta: { title: '考勤', icon: 'attendance', hidden: false },
// },
// {
// path: '/organization/hr/performance',
// name: 'HRPerformance',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '绩效', icon: 'performance', hidden: false },
// children: [
// {
// path: '/organization/hr/performance/dimention',
// name: 'Dimention',
// component: () => import('@/views/performance/setting/index.vue'),
// meta: { title: '绩效维度', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/rule',
// name: 'Rule',
// component: () => import('@/views/performance/rule.vue'),
// meta: { title: '绩效细则', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/my',
// name: 'MyPerformance',
// component: () => import('@/views/performance/my.vue'),
// meta: { title: '我的绩效', icon: 'performance', hidden: false },
//
// },
// ],
// },
// {
// path: '/organization/hr/salary',
// name: 'HRSalary',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/overview',
// meta: { title: '工资', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/overview',
// name: 'HRSalaryOverview',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '工资概览', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/payroll',
// name: 'Payroll',
// component: () => import('@/views/salary-management/index.vue'),
// meta: { title: '工资单', icon: 'salary', hidden: false },
//
// },
// ],
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/system-insurance/health-management',
// name: 'HRSystemHealthManagement',
// component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
// meta: { title: '健康档案管理', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance',
// name: 'HRSystemInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/system-insurance/overview',
// meta: { title: '人员保险', icon: 'settings', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/system-insurance/overview',
// name: 'HRSystemInsuranceOverview',
// component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/management',
// name: 'HRSystemInsuranceManagement',
// component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
// meta: { title: '保险管理', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/file-management',
// name: 'HRSystemFileManagement',
// component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
// meta: { title: '保单文件管理', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/company-management',
// name: 'HRSystemCompanyManagement',
// component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
// meta: { title: '保险公司管理', icon: 'building', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/type-management',
// name: 'HRSystemTypeManagement',
// component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
// meta: { title: '保险类型管理', icon: 'category', hidden: false },
// },
// ],
// },
// {
// path: '/organization/hr/salary/certification',
// name: 'HRCertification',
// component: () => import('@/views/hr/salary/certification/index.vue'),
// meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
// },
// {
// path: '/organization/hr/contribution',
// name: 'HRContribution',
// component: () => import('@/views/hr/contribution/index.vue'),
// meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
// },
// ],
// },
// {
// path: '/organization/role',
// name: 'OrganizationRole',
// component: () => import('@/views/system/role/index.vue'),
// meta: { title: '角色管理', icon: 'role', hidden: false },
// },
// ],
// },
{
path: '/asset-management',
name: 'AssetManagement',
component: Layout,
redirect: '/asset-management/device/inventory',
redirect: '/asset-management/device-management/device-center',
meta: { title: '资产管理', icon: 'property-safety', hidden: false, sort: 3 },
children: [
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty1',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '设备管理', icon: 'copyright', hidden: false },
path: '/asset-management/device-management',
name: 'DeviceManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/asset-management/device-management/device-center',
meta: {
title: '设备管理',
icon: 'device',
hidden: false,
},
children: [
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty11',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '库存管理', hidden: false },
path: '/asset-management/device-management/device-center',
name: 'DeviceCenter',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备中心',
icon: 'appstore',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty12',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '设备采购', hidden: false },
path: '/asset-management/device-management/device-detail/:id',
name: 'DeviceDetail',
component: () => import('@/views/system-resource/device-management/detail.vue'),
meta: {
title: '设备详情',
icon: 'info-circle',
hidden: true,
},
},
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty13',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '在线管理', hidden: false },
path: '/asset-management/device-management/procurement',
name: 'DeviceProcurement',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备采购',
icon: 'shopping-cart',
hidden: false,
},
},
{
path: '/asset-management/device-management/online',
name: 'DeviceOnline',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/asset-management/device-management/online/drone',
meta: {
title: '在线管理',
icon: 'cloud',
hidden: false,
},
children: [
{
path: '/asset-management/intellectual-property11',
name: 'IntellectualProperty14',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '无人机', hidden: false },
path: '/asset-management/device-management/online/drone',
name: 'DeviceDrone',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '无人机',
icon: 'drone',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property12',
name: 'IntellectualProperty15',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '机巢', hidden: false },
path: '/asset-management/device-management/online/nest',
name: 'DeviceNest',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '机巢',
icon: 'nest',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property13',
name: 'IntellectualProperty16',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '其他智能终端', hidden: false },
},
{
path: '/asset-management/intellectual-property14',
name: 'IntellectualProperty17',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '车辆管理', hidden: false },
path: '/asset-management/device-management/online/smart-terminal',
name: 'DeviceSmartTerminal',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '其他智能终端',
icon: 'terminal',
hidden: false,
},
},
],
},
],
},
{
path: '/asset-management/intellectual-property',
name: 'IntellectualProperty',
path: '/asset-management/other-assets',
name: 'OtherAssets',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '其他资产', icon: 'copyright', hidden: false },
},
@ -558,9 +529,9 @@ export const systemRoutes: RouteRecordRaw[] = [
},
},
{
path: '/project-management/project-template/information-retrieval',
path: 'project-management/project-template/information-retrieval',
name: 'InformationRetrieval',
component: () => import ('@/views/project-management/bidding/information-retrieval/index.vue'),
component: () => import ('@/views/default/error/404.vue'),
meta: {
title: '信息检索(N)',
icon: 'trophy',
@ -672,11 +643,11 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/project-management/projects/device',
name: 'DeviceManagement',
name: 'ProjectDeviceManagement',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备管理',
icon: 'none',
title: '项目设备管理',
icon: 'device',
hidden: false,
},
},
@ -809,11 +780,11 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
component: () => import('@/views/operation-platform/data-processing/data-storage/index.vue'),
meta: { title: '附件管理', icon: 'attachment', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/model-config',
name: 'ModelConfig',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
meta: { title: '模型配置', icon: 'robot', hidden: false },
@ -824,23 +795,8 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data',
name: 'PreprocessedData',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/data-storage',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '数据预处理', icon: 'filter', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageBatchUpload',
name: 'ImageBatchUpload',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '批量上传', icon: 'file', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageSorting',
name: 'ImageSorting',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/image-sorting/index.vue'),
meta: { title: '图像分拣', icon: 'attachment', hidden: false },
},
],
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection',
@ -1016,30 +972,6 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
],
},
{
path: '/user/profile',
name: 'UserProfile',
component: Layout,
redirect: '/user/profile',
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
sort: 100,
},
children: [
{
path: '/user/profile',
name: 'UserProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
},
},
],
},
{
path: '/enterprise-settings',
name: 'EnterpriseSettings',
@ -1138,66 +1070,32 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/training',
name: 'Training',
component: Layout,
redirect: '/training/plan',
meta: { title: '培训管理', icon: 'book', hidden: false, sort: 9 },
children: [
{
path: '/training/plan',
name: 'TrainingPlan',
component: () => import('@/views/training/plan/index.vue'),
meta: {
title: '培训计划',
icon: 'calendar',
hidden: false,
},
},
],
},
{
path: '/system-resource',
name: 'SystemResource',
component: Layout,
redirect: '/system-resource/device-management/warehouse',
meta: { title: '关于平台', icon: 'server', hidden: false, sort: 9 },
redirect: '/system-resource/information-system/software-management',
meta: { title: '系统资源', icon: 'server', hidden: false, sort: 9 },
children: [
{
path: '/system-resource/device-management/warehouse',
name: 'DeviceWarehouse',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '库存管理',
icon: 'warehouse',
hidden: false,
},
},
{
path: '/system-resource/device-management/online',
name: 'DeviceOnline',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/system-resource/device-management/online/drone',
meta: {
title: '在线管理',
icon: 'cloud',
hidden: false,
},
children: [
{
path: '/system-resource/device-management/online/drone',
name: 'DeviceDrone',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '无人机',
icon: 'drone',
hidden: false,
},
},
{
path: '/system-resource/device-management/online/nest',
name: 'DeviceNest',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '机巢',
icon: 'nest',
hidden: false,
},
},
{
path: '/system-resource/device-management/online/smart-terminal',
name: 'DeviceSmartTerminal',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '其他智能终端',
icon: 'terminal',
hidden: false,
},
},
],
},
{
path: '/system-resource/information-system',
name: 'InformationSystem',
@ -1243,6 +1141,7 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/',
redirect: '/project-management/project-template/project-aproval',
@ -1268,11 +1167,6 @@ export const constantRoutes: RouteRecordRaw[] = [
},
],
},
// {
// path: '/user/profile',
// component: () => import('@/views/user/profile/index.vue'),
// meta: { hidden: true },
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { computed, reactive, toRefs, watch, watchEffect } from 'vue'
import { computed, reactive, toRefs, watchEffect } from 'vue'
import { generate, getRgbStr } from '@arco-design/color'
import { type BasicConfig, listSiteOptionDict } from '@/apis'
import type { BasicConfig } from '@/apis'
import { getSettings } from '@/config/setting'
const storeSetup = () => {
@ -81,18 +81,18 @@ const storeSetup = () => {
document.title = config.SITE_TITLE || ''
document.querySelector('link[rel="shortcut icon"]')?.setAttribute('href', config.SITE_FAVICON || '/favicon.ico')
}
// 使用watchEffect优化监听避免递归更新
watchEffect(() => {
const filters = [] as string[]
if (settingConfig.enableMourningMode) {
filters.push('grayscale(100%)')
}
if (settingConfig.enableColorWeaknessMode) {
filters.push('invert(80%)')
}
// 如果没有任何滤镜条件,移除 `filter` 样式
if (filters.length === 0) {
document.documentElement.style.removeProperty('filter')

View File

@ -149,6 +149,34 @@ const storeSetup = () => {
isHidden: false,
sort: 3,
},
{
id: 1070,
parentId: 1000,
title: '部门管理',
type: 2,
path: '/system/dept',
name: 'SystemDept',
component: 'system/dept/index',
icon: 'mind-mapping',
isExternal: false,
isCache: false,
isHidden: false,
sort: 4,
},
{
id: 1090,
parentId: 1000,
title: '岗位管理',
type: 2,
path: '/system/post',
name: 'SystemPost',
component: 'system/post/index',
icon: 'settings',
isExternal: false,
isCache: false,
isHidden: false,
sort: 5,
},
],
}]
// 使用已转换的数据生成路由

View File

@ -5,10 +5,10 @@ import {
type AccountLoginReq,
AuthTypeConstants,
type PhoneLoginReq,
type UserDetail,
type DeptDetail,
type PhoneLoginReq,
type RoleDetail,
type UserDetail,
type UserInfo,
accountLogin as accountLoginApi,
@ -21,10 +21,10 @@ import { clearToken, getToken, setToken } from '@/utils/auth'
import { resetHasRouteFlag } from '@/router/guard'
interface NewUserInfoData {
user: UserDetail;
dept: DeptDetail;
roles: RoleDetail[];
posts: any[];
user: UserDetail
dept: DeptDetail
roles: RoleDetail[]
posts: any[]
}
const storeSetup = () => {
@ -43,7 +43,7 @@ const storeSetup = () => {
deptName: '',
avatar: '',
roles: [] as string[],
permissions: [] as string[]
permissions: [] as string[],
})
const nickname = computed(() => userInfo.name)
const username = computed(() => userInfo.account)
@ -68,8 +68,6 @@ const storeSetup = () => {
token.value = res.data.tokenValue
}
// 手机号登录
const phoneLogin = async (req: PhoneLoginReq) => {
const res = await phoneLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE })
@ -77,8 +75,6 @@ const storeSetup = () => {
token.value = res.data.token
}
// 退出登录回调
const logoutCallBack = async () => {
roles.value = []
@ -102,12 +98,12 @@ const storeSetup = () => {
// 获取用户信息
const getInfo = async () => {
const res = await getUserInfoApi()
// 检查返回数据格式适配新旧API
if (res.data && 'user' in res.data) {
// 新API结构
const { user, dept, roles: userRoles } = res.data as unknown as NewUserInfoData
// 更新用户基本信息
userInfo.userId = user.userId
userInfo.account = user.account
@ -118,7 +114,7 @@ const storeSetup = () => {
userInfo.userType = user.userType
userInfo.mobile = user.mobile
userInfo.createTime = user.createTime
// 更新部门信息
if (dept) {
userInfo.deptId = dept.deptId
@ -128,23 +124,23 @@ const storeSetup = () => {
// 处理角色信息
if (userRoles && userRoles.length) {
// 提取角色键作为权限标识
const roleKeys = userRoles.map(role => role.roleKey).filter(Boolean) as string[]
const roleKeys = userRoles.map((role) => role.roleKey).filter(Boolean) as string[]
roles.value = roleKeys
// 由于新API没有直接提供permissions这里默认给管理员全部权限
permissions.value = roleKeys.includes('admin') ? ['*:*:*'] : []
}
} else if (res.data) {
// 旧API结构保留兼容
const oldData = res.data as unknown as UserInfo
// 映射旧结构到新结构
userInfo.userId = oldData.id
userInfo.account = oldData.username
userInfo.name = oldData.nickname
userInfo.avatar = oldData.avatar
userInfo.deptName = oldData.deptName
if (oldData.roles && oldData.roles.length) {
roles.value = oldData.roles
permissions.value = oldData.permissions

View File

@ -1,4 +1,4 @@
@import './var.scss';
@use './var.scss' as *;
body {
--margin: 14px; // 通用外边距

View File

@ -1,5 +1,5 @@
/* 全局样式 */
@import './var.scss';
@use './var.scss' as *;
.w-full {
width: 100%;

View File

@ -1,17 +1,17 @@
// 基础样式
@import './base.scss';
@use './base.scss';
// 全局类名样式
@import './global.scss';
@use './global.scss';
// 自定义原生滚动条样式
@import './scrollbar-reset.scss';
@use './scrollbar-reset.scss';
// 自定义 nprogress 插件进度条颜色
@import './nprogress.scss';
@use './nprogress.scss';
// 富文本的css主题颜色变量
@import './editor.scss';
@use './editor.scss';
// 动画类名
@import './animated.scss';
@use './animated.scss';

41
src/test-console.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div>
<h2>Console.log 测试</h2>
<button @click="testConsole">测试 Console.log</button>
<p>{{ message }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('')
const testConsole = () => {
console.log('测试 console.log 是否正常工作')
console.warn('测试 console.warn')
console.error('测试 console.error')
message.value = '请查看浏览器控制台,应该能看到上述日志信息'
}
</script>
<style scoped>
div {
padding: 20px;
text-align: center;
}
button {
padding: 10px 20px;
margin: 10px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #40a9ff;
}
</style>

10
src/types/api.d.ts vendored
View File

@ -1,10 +1,10 @@
/** API响应通用类型 */
interface ApiRes<T> {
code: number | string;
status?: number;
success: boolean;
msg: string;
data: T;
code: number | string
status?: number
success: boolean
msg: string
data: T
}
/** 分页响应数据格式 */

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

179
src/types/equipment.d.ts vendored Normal file
View File

@ -0,0 +1,179 @@
export interface EquipmentPageQuery {
equipmentName?: string
equipmentType?: string
equipmentStatus?: string
equipmentSn?: string
assetCode?: string
brand?: string
locationStatus?: string
healthStatus?: string
responsiblePerson?: string
useStatus?: string
projectId?: string
userId?: string
equipmentModel?: string
specification?: string
physicalLocation?: string
supplierName?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增搜索字段
usingDepartment?: string
invoice?: string
barcode?: string
importer?: string
page?: number
pageSize?: number
orderBy?: string
orderDirection?: string
}
export interface EquipmentReq {
equipmentName: string
equipmentModel: string
equipmentType: string
equipmentStatus: string
useStatus: string
equipmentSn: string
assetCode?: string
brand?: string
specification?: string
locationStatus?: string
physicalLocation?: string
responsiblePerson?: string
healthStatus?: string
purchaseTime?: string
inStockTime?: string
activationTime?: string
expectedScrapTime?: string
actualScrapTime?: string
statusChangeTime?: string
purchaseOrder?: string
supplierName?: string
purchasePrice?: number
currentNetValue?: number
depreciationMethod?: string
depreciationYears?: number
salvageValue?: number
warrantyExpireDate?: string
lastMaintenanceDate?: string
nextMaintenanceDate?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增字段
usingDepartment?: string
borrowingTime?: string
returnTime?: string
outStockTime?: string
totalUsageTime?: string
depreciationRate?: number
depreciationMethodDesc?: string
invoice?: string
invoiceStatus?: string
attachments?: string
photos?: string
barcode?: string
importer?: string
inventoryTimeStatus1?: string
inventoryTimeStatus2?: string
inventoryTimeStatus3?: string
inventoryCheckTimeStatus1?: string
inventoryCheckTimeStatus2?: string
inventoryCheckTimeStatus3?: string
}
export interface EquipmentResp {
equipmentId: string
assetCode?: string
equipmentName: string
equipmentType: string
equipmentTypeLabel?: string
equipmentModel: string
equipmentSn: string
brand?: string
specification?: string
equipmentStatus: string
equipmentStatusLabel?: string
useStatus: string
locationStatus?: string
locationStatusLabel?: string
physicalLocation?: string
responsiblePerson?: string
healthStatus?: string
healthStatusLabel?: string
purchaseTime?: string
inStockTime?: string
activationTime?: string
expectedScrapTime?: string
actualScrapTime?: string
statusChangeTime?: string
purchaseOrder?: string
supplierName?: string
purchasePrice?: number
currentNetValue?: number
depreciationMethod?: string
depreciationYears?: number
salvageValue?: number
warrantyExpireDate?: string
lastMaintenanceDate?: string
nextMaintenanceDate?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增字段
usingDepartment?: string
borrowingTime?: string
returnTime?: string
outStockTime?: string
totalUsageTime?: string
depreciationRate?: number
depreciationMethodDesc?: string
invoice?: string
invoiceStatus?: string
attachments?: string
photos?: string
barcode?: string
importer?: string
inventoryTimeStatus1?: string
inventoryTimeStatus2?: string
inventoryTimeStatus3?: string
inventoryCheckTimeStatus1?: string
inventoryCheckTimeStatus2?: string
inventoryCheckTimeStatus3?: string
projectId?: string
projectName?: string
userId?: string
name?: string
createTime?: string
updateTime?: string
}
export interface EquipmentTypeOption {
label: string
value: string
}
export interface EquipmentStatusOption {
label: string
value: string
color: string
}
export interface LocationStatusOption {
label: string
value: string
color: string
}
export interface HealthStatusOption {
label: string
value: string
color: string
}
export interface DepreciationMethodOption {
label: string
value: string
}

71
src/types/training.d.ts vendored Normal file
View File

@ -0,0 +1,71 @@
export interface TrainingPlanPageQuery {
planName?: string
trainingType?: string
trainingLevel?: string
status?: string
trainer?: string
startTime?: string
endTime?: string
page?: number
pageSize?: number
}
export interface TrainingPlanReq {
planName: string
trainingType: string
trainingLevel: string
trainingContent?: string
trainer?: string
trainingLocation?: string
startTime: string
endTime: string
status?: string
maxParticipants?: number
requirements?: string
remark?: string
}
export interface TrainingPlanResp {
planId: string
planName: string
trainingType: string
trainingLevel: string
trainingContent?: string
trainer?: string
trainingLocation?: string
startTime: string
endTime: string
status: string
maxParticipants?: number
currentParticipants?: number
requirements?: string
remark?: string
createTime: string
createBy: string
materials?: TrainingMaterialResp[]
records?: TrainingRecordResp[]
}
export interface TrainingMaterialResp {
materialId: string
materialName: string
materialType: string
materialPath?: string
materialSize?: number
description?: string
sortOrder?: number
}
export interface TrainingRecordResp {
recordId: string
userId: string
userName: string
deptId?: string
deptName?: string
attendanceStatus: string
signInTime?: string
signOutTime?: string
score?: number
feedback?: string
certificateId?: string
}

View File

@ -18,7 +18,7 @@ export function encryptByMd5(txt: string) {
const publicKey
= 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9u'
+ 'aUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ=='
+ 'aUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ=='
export function encryptByRsa(txt: string) {
const encryptor = new JSEncrypt()
@ -36,7 +36,7 @@ export function encryptByAes(word: string, account: string) {
// 对账号做md5计算然后取8-24位作为密钥16个字符
const accountMd5 = md5(account).toString()
const keyWord = accountMd5.substring(8, 24) // 取8-24位索引8-23共16位
const key = CryptoJS.enc.Utf8.parse(keyWord)
const arcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(arcs, key, {

View File

@ -30,7 +30,7 @@ const StatusCodeMessage: ICodeMessage = {
}
const http: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_PREFIX ?? import.meta.env.VITE_API_BASE_URL,
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30 * 1000,
})
@ -70,10 +70,10 @@ http.interceptors.response.use(
if (data && data.rows !== undefined && data.data === undefined) {
data.data = data.rows
}
// 兼容不同的API响应结构
const { success, code, msg } = data
// 检查响应类型是否是blob
if (response.request.responseType === 'blob') {
const contentType = data.type
@ -96,7 +96,7 @@ http.interceptors.response.use(
return response
}
}
// 判断请求是否成功明确的success字段为true或者code为200都视为成功
const isSuccess = success !== undefined ? success : (code === 200 || code === '200')
if (isSuccess) {
@ -139,17 +139,22 @@ const request = async <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<
.then((res: AxiosResponse) => {
// 处理返回数据结构兼容rows和data字段
const responseData = res.data
// 如果返回的数据中有rows字段但没有data字段将rows赋值给data
if (responseData.rows !== undefined && responseData.data === undefined) {
responseData.data = responseData.rows
}
// 兼容后端返回的 status/code 格式
if (responseData.status === 200 || responseData.code === 200 || responseData.code === '200') {
responseData.success = true
}
// 如果返回的code是200但没有设置success字段将success设置为true
if ((responseData.code === 200 || responseData.code === '200') && responseData.success === undefined) {
responseData.success = true
}
return responseData
})
.catch((err: { msg: string }) => Promise.reject(err))

View File

@ -4,31 +4,31 @@
// API返回的菜单项类型
export interface ApiMenuItem {
menuId: string;
parentId: string;
menuName: string;
menuType: string; // 'catalog' | 'route'
orderNum: number;
visible: string;
children?: ApiMenuItem[];
[key: string]: any; // 其他可能的字段
menuId: string
parentId: string
menuName: string
menuType: string // 'catalog' | 'route'
orderNum: number
visible: string
children?: ApiMenuItem[]
[key: string]: any // 其他可能的字段
}
// 前端需要的菜单项类型
export interface FrontendMenuItem {
id: number | string;
parentId: number | string;
title: string;
type: number; // 1表示目录2表示菜单
path: string;
name: string;
component: string;
icon: string;
isExternal: boolean;
isCache: boolean;
isHidden: boolean;
sort: number;
children?: FrontendMenuItem[];
id: number | string
parentId: number | string
title: string
type: number // 1表示目录2表示菜单
path: string
name: string
component: string
icon: string
isExternal: boolean
isCache: boolean
isHidden: boolean
sort: number
children?: FrontendMenuItem[]
}
/**
@ -37,14 +37,14 @@ export interface FrontendMenuItem {
const convertMenuType = (menuType: string): number => {
switch (menuType.toLowerCase()) {
case 'catalog':
return 1;
return 1
case 'route':
return 2;
return 2
case 'button':
return 3;
return 3
default:
// 默认为菜单类型
return 2;
return 2
}
}
@ -52,7 +52,7 @@ const convertMenuType = (menuType: string): number => {
* : '0' -> false, '1' -> true
*/
const convertVisible = (visible: string): boolean => {
return visible === '1'; // '1'为隐藏,'0'为显示
return visible === '1' // '1'为隐藏,'0'为显示
}
/**
@ -60,27 +60,27 @@ const convertVisible = (visible: string): boolean => {
*/
const convertMenuItem = (apiItem: ApiMenuItem): FrontendMenuItem => {
// 根据menuType生成默认的path和component
let path = '';
let component = '';
let name = '';
let path = ''
let component = ''
let name = ''
// 简单的名称生成,去掉空格,保持首字母大写,非首字母小写
const generateName = (menuName: string): string => {
return menuName.replace(/\s+/g, '')
.replace(/^./, (match) => match.toUpperCase())
.replace(/[\u4e00-\u9fa5]/g, ''); // 移除中文字符
};
.replace(/[\u4E00-\u9FA5]/g, '') // 移除中文字符
}
if (apiItem.menuType.toLowerCase() === 'catalog') {
path = `/${apiItem.menuName.toLowerCase().replace(/\s+/g, '-')}`;
component = 'Layout';
name = generateName(apiItem.menuName);
path = `/${apiItem.menuName.toLowerCase().replace(/\s+/g, '-')}`
component = 'Layout'
name = generateName(apiItem.menuName)
} else {
// 假设route类型菜单都在某个catalog下
const parentName = apiItem.menuName.toLowerCase().replace(/\s+/g, '-');
path = `/system/${parentName}`;
component = `system/${parentName}/index`;
name = `System${generateName(apiItem.menuName)}`;
const parentName = apiItem.menuName.toLowerCase().replace(/\s+/g, '-')
path = `/system/${parentName}`
component = `system/${parentName}/index`
name = `System${generateName(apiItem.menuName)}`
}
return {
@ -88,21 +88,21 @@ const convertMenuItem = (apiItem: ApiMenuItem): FrontendMenuItem => {
parentId: apiItem.parentId,
title: apiItem.menuName,
type: convertMenuType(apiItem.menuType),
path: path,
name: name,
component: component,
path,
name,
component,
icon: 'settings', // 默认图标
isExternal: false,
isCache: false,
isHidden: convertVisible(apiItem.visible),
sort: apiItem.orderNum || 0,
children: apiItem.children ? apiItem.children.map(child => convertMenuItem(child)) : []
};
children: apiItem.children ? apiItem.children.map((child) => convertMenuItem(child)) : [],
}
}
/**
* API返回的菜单数据为前端需要的格式
*/
export const convertMenuData = (apiMenuData: ApiMenuItem[]): FrontendMenuItem[] => {
return apiMenuData.map(item => convertMenuItem(item));
}
return apiMenuData.map((item) => convertMenuItem(item))
}

View File

@ -1,7 +1,7 @@
<template>
<GiPageLayout>
<GiTable
v-model:selectedKeys="selectedKeys"
v-model:selected-keys="selectedKeys"
row-key="tableName"
:data="dataList"
:columns="columns"

View File

@ -15,7 +15,7 @@
</a-descriptions>
</a-card>
</a-col>
<!-- 经营状况 -->
<a-col :span="8">
<a-card title="经营状况" size="small">
@ -28,7 +28,7 @@
</a-descriptions>
</a-card>
</a-col>
<!-- 发展目标 -->
<a-col :span="8">
<a-card title="发展目标" size="small">
@ -73,4 +73,4 @@
.company-overview {
padding: 16px;
}
</style>
</style>

View File

@ -0,0 +1,431 @@
<template>
<div class="airport-management">
<div class="page-header">
<a-card class="general-card" title="机场管理">
<template #extra>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新建机场
</a-button>
</template>
<a-row :gutter="16" style="margin-bottom: 16px">
<a-col :span="6">
<a-input-search
v-model="searchForm.name"
placeholder="请输入机场名称"
@search="search"
allow-clear
/>
</a-col>
<a-col :span="6">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 100%"
>
<a-option value="1">运营中</a-option>
<a-option value="0">停运</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
查询
</a-button>
<a-button style="margin-left: 8px" @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-col>
</a-row>
</a-card>
</div>
<div class="page-content">
<a-card class="general-card">
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag :color="record.status === '1' ? 'green' : 'red'">
{{ record.status === '1' ? '运营中' : '停运' }}
</a-tag>
</template>
<template #actions="{ record }">
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-popconfirm
content="确定要删除这个机场吗?"
@ok="handleDelete(record)"
>
<a-button type="text" size="small" status="danger">
删除
</a-button>
</a-popconfirm>
</template>
</a-table>
</a-card>
</div>
<!-- 新增/编辑机场弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
@ok="handleSave"
@cancel="handleCancel"
width="800px"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="机场名称" field="name">
<a-input v-model="form.name" placeholder="请输入机场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="机场代码" field="code">
<a-input v-model="form.code" placeholder="请输入机场代码" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所在省份" field="province">
<a-input v-model="form.province" placeholder="请输入所在省份" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="所在城市" field="city">
<a-input v-model="form.city" placeholder="请输入所在城市" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="经度" field="longitude">
<a-input-number
v-model="form.longitude"
placeholder="请输入经度"
:precision="6"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="纬度" field="latitude">
<a-input-number
v-model="form.latitude"
placeholder="请输入纬度"
:precision="6"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="海拔高度(米)" field="altitude">
<a-input-number
v-model="form.altitude"
placeholder="请输入海拔高度"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" field="status">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option value="1">运营中</a-option>
<a-option value="0">停运</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="form.remark"
placeholder="请输入备注信息"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconPlus, IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
//
const searchForm = reactive({
name: '',
status: ''
})
//
const columns = [
{
title: '机场名称',
dataIndex: 'name',
width: 150
},
{
title: '机场代码',
dataIndex: 'code',
width: 120
},
{
title: '所在地区',
dataIndex: 'location',
width: 200
},
{
title: '经纬度',
dataIndex: 'coordinates',
width: 180
},
{
title: '海拔高度(米)',
dataIndex: 'altitude',
width: 120
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180
},
{
title: '操作',
slotName: 'actions',
width: 200,
fixed: 'right'
}
]
//
const tableData = ref([
{
id: 1,
name: '北京首都国际机场',
code: 'PEK',
province: '北京市',
city: '北京市',
location: '北京市朝阳区',
longitude: 116.584556,
latitude: 40.080111,
coordinates: '116.584556, 40.080111',
altitude: 35,
status: '1',
remark: '中国最大的国际机场之一',
createTime: '2024-01-01 10:00:00'
},
{
id: 2,
name: '上海浦东国际机场',
code: 'PVG',
province: '上海市',
city: '上海市',
location: '上海市浦东新区',
longitude: 121.805214,
latitude: 31.143378,
coordinates: '121.805214, 31.143378',
altitude: 4,
status: '1',
remark: '华东地区重要的国际航空枢纽',
createTime: '2024-01-02 14:30:00'
},
{
id: 3,
name: '广州白云国际机场',
code: 'CAN',
province: '广东省',
city: '广州市',
location: '广州市白云区',
longitude: 113.298889,
latitude: 23.392436,
coordinates: '113.298889, 23.392436',
altitude: 15,
status: '1',
remark: '华南地区最大的民用机场',
createTime: '2024-01-03 09:15:00'
}
])
const loading = ref(false)
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const modalVisible = ref(false)
const modalTitle = ref('新增机场')
const formRef = ref()
//
const form = reactive({
id: null,
name: '',
code: '',
province: '',
city: '',
longitude: null,
latitude: null,
altitude: null,
status: '1',
remark: ''
})
//
const rules = {
name: [{ required: true, message: '请输入机场名称' }],
code: [{ required: true, message: '请输入机场代码' }],
province: [{ required: true, message: '请输入所在省份' }],
city: [{ required: true, message: '请输入所在城市' }],
longitude: [{ required: true, message: '请输入经度' }],
latitude: [{ required: true, message: '请输入纬度' }],
status: [{ required: true, message: '请选择状态' }]
}
//
const search = () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
Message.success('查询成功')
}, 1000)
}
//
const reset = () => {
searchForm.name = ''
searchForm.status = ''
search()
}
//
const handleAdd = () => {
modalTitle.value = '新增机场'
modalVisible.value = true
resetForm()
}
//
const handleEdit = (record: any) => {
modalTitle.value = '编辑机场'
modalVisible.value = true
Object.assign(form, record)
}
//
const handleView = (record: any) => {
Message.info(`查看机场详情: ${record.name}`)
//
}
//
const handleDelete = (record: any) => {
Message.success(`删除机场: ${record.name}`)
// API
}
//
const handleSave = async () => {
const valid = await formRef.value?.validate()
if (valid) return
try {
loading.value = true
// API
await new Promise(resolve => setTimeout(resolve, 1000))
Message.success(form.id ? '更新成功' : '新增成功')
modalVisible.value = false
search()
} catch (error) {
Message.error('操作失败')
} finally {
loading.value = false
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.keys(form).forEach(key => {
if (key === 'status') {
form[key] = '1'
} else {
form[key] = null
}
})
formRef.value?.clearValidate()
}
//
const handlePageChange = (page: number) => {
pagination.current = page
search()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
search()
}
onMounted(() => {
search()
})
</script>
<style scoped lang="scss">
.airport-management {
.page-header {
margin-bottom: 16px;
}
.general-card {
border-radius: 8px;
}
}</style>

View File

@ -37,11 +37,11 @@
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
<template #icon><IconSearch /></template>
搜索
</a-button>
<a-button @click="resetSearch">
<template #icon><icon-refresh /></template>
<template #icon><IconRefresh /></template>
重置
</a-button>
</a-space>
@ -55,17 +55,17 @@
<template #extra>
<a-space>
<a-button type="primary" size="small" :disabled="selectedRowKeys.length === 0">
<template #icon><icon-download /></template>
<template #icon><IconDownload /></template>
批量下载
</a-button>
<a-button
type="primary"
status="danger"
size="small"
<a-button
type="primary"
status="danger"
size="small"
:disabled="selectedRowKeys.length === 0"
@click="handleBatchDelete"
>
<template #icon><icon-delete /></template>
<template #icon><IconDelete /></template>
批量删除
</a-button>
</a-space>
@ -75,14 +75,14 @@
:loading="loading"
:data="tableData"
:pagination="pagination"
@page-change="onPageChange"
row-key="id"
:row-selection="{
type: 'checkbox',
showCheckedAll: true,
selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange
selectedRowKeys,
onChange: onSelectionChange,
}"
@page-change="onPageChange"
>
<template #columns>
<a-table-column title="文件名" data-index="fileName">
@ -116,11 +116,11 @@
<template #cell="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">
<template #icon><icon-eye /></template>
<template #icon><IconEye /></template>
预览
</a-button>
<a-button size="small" @click="downloadFile(record)">
<template #icon><icon-download /></template>
<template #icon><IconDownload /></template>
下载
</a-button>
<a-popconfirm
@ -128,7 +128,7 @@
@ok="deleteFile(record)"
>
<a-button size="small" status="danger">
<template #icon><icon-delete /></template>
<template #icon><IconDelete /></template>
删除
</a-button>
</a-popconfirm>
@ -160,18 +160,18 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import FilePreview from '@/components/FilePreview/index.vue'
import {
IconSearch,
IconRefresh,
IconDownload,
import {
IconDelete,
IconDownload,
IconEye,
IconFile
IconFile,
IconRefresh,
IconSearch,
} from '@arco-design/web-vue/es/icon'
import { getAttachBusinessTypes, getAttachmentList, deleteAttachment } from '@/apis/attach-info'
import FilePreview from '@/components/FilePreview/index.vue'
import { deleteAttachment, getAttachBusinessTypes, getAttachmentList } from '@/apis/attach-info'
import type { AttachInfoData, BusinessType } from '@/apis/attach-info/type'
defineOptions({ name: 'AttachmentManagement' })
@ -222,7 +222,7 @@ const fetchAttachmentList = async () => {
Message.warning('请先选择业务类型')
return
}
loading.value = true
try {
const res = await getAttachmentList(filterForm.businessType)
@ -293,7 +293,7 @@ const deleteFile = async (file: AttachInfoData) => {
if (res) {
Message.success(`已删除: ${file.fileName}`)
//
tableData.value = tableData.value.filter(item => item.id !== file.id)
tableData.value = tableData.value.filter((item) => item.id !== file.id)
pagination.total = tableData.value.length
} else {
Message.error('删除文件失败')
@ -308,7 +308,7 @@ const handleBatchDelete = () => {
if (selectedRowKeys.value.length === 0) {
return
}
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个文件吗?`,
@ -316,7 +316,7 @@ const handleBatchDelete = () => {
cancelText: '取消',
onOk: async () => {
try {
const promises = selectedRowKeys.value.map(id => deleteAttachment(id))
const promises = selectedRowKeys.value.map((id) => deleteAttachment(id))
await Promise.all(promises)
Message.success('批量删除成功')
fetchAttachmentList()
@ -325,7 +325,7 @@ const handleBatchDelete = () => {
console.error('批量删除失败:', error)
Message.error('批量删除失败')
}
}
},
})
}
@ -334,13 +334,13 @@ const getFileTypeText = (type: string) => {
image: '图片',
document: '文档',
video: '视频',
other: '其他'
other: '其他',
}
return typeMap[type] || '未知'
}
const getBusinessTypeName = (code: string) => {
const businessType = businessTypes.value.find(item => item.code === code)
const businessType = businessTypes.value.find((item) => item.code === code)
return businessType ? businessType.name : code
}
@ -349,12 +349,12 @@ const formatFileSize = (size: number) => {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let index = 0
let tempSize = size
while (tempSize >= 1024 && index < units.length - 1) {
tempSize /= 1024
index++
}
return `${tempSize.toFixed(2)} ${units[index]}`
}
@ -377,4 +377,4 @@ const getFileIcon = (fileType: string) => {
margin-bottom: 16px;
}
}
</style>
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="attachment-upload">
<a-space direction="vertical" :size="16" style="width: 100%">
<a-form :model="formData" ref="formRef">
<a-form ref="formRef" :model="formData">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="businessType" label="业务类型" required>
@ -64,7 +64,7 @@
</a-form-item>
<div class="form-actions">
<a-button type="primary" @click="handleSubmit" :loading="submitting">提交</a-button>
<a-button type="primary" :loading="submitting" @click="handleSubmit">提交</a-button>
<a-button style="margin-left: 10px" @click="resetForm">重置</a-button>
</div>
</a-form>
@ -73,10 +73,10 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconPlus } from '@arco-design/web-vue/es/icon'
import { getAttachBusinessTypes, batchAddAttachment } from '@/apis/attach-info'
import { batchAddAttachment, getAttachBusinessTypes } from '@/apis/attach-info'
import type { BusinessType } from '@/apis/attach-info/type'
defineOptions({ name: 'AttachmentUpload' })
@ -98,23 +98,15 @@ const formData = reactive({
fileType: '',
remark: '',
userDefinedPath: '',
files: [] as FileItem[]
files: [] as FileItem[],
})
//
const fetchBusinessTypes = async () => {
try {
const res = await getAttachBusinessTypes()
console.log("res:",res);
if (res.data) {
res.data.forEach(item => {
const key = Object.keys(item)[0];
const value = item[key];
businessTypes.value.push({
name: value,
code:key
});
});
businessTypes.value = res.data
}
} catch (error) {
console.error('获取业务类型失败:', error)
@ -128,23 +120,23 @@ onMounted(() => {
const customRequest = (options: any) => {
const { file, onProgress, onSuccess, onError } = options
//
const fileItem = {
file,
status: 'ready',
uid: options.fileItem.uid,
name: options.fileItem.name
name: options.fileItem.name,
}
formData.files.push(fileItem)
//
onProgress(0)
let percent = 0
const timer = setInterval(() => {
percent += 10
onProgress(percent > 100 ? 100 : percent)
if (percent >= 100) {
clearInterval(timer)
onSuccess()
@ -159,32 +151,32 @@ const handleChange = (fileList) => {
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (!formData.businessType) {
Message.error('请选择业务类型')
return
}
if (formData.files.length === 0) {
Message.error('请选择要上传的文件')
return
}
submitting.value = true
const formDataToSend = new FormData()
formData.files.forEach((item, index) => {
formDataToSend.append(`files[${index}]`, item.file)
})
const params = {
fileType: formData.fileType,
remark: formData.remark,
userDefinedPath: formData.userDefinedPath
userDefinedPath: formData.userDefinedPath,
}
const res = await batchAddAttachment(formData.businessType, formDataToSend, params)
if (res) {
Message.success('文件上传成功')
resetForm()
@ -193,7 +185,7 @@ const handleSubmit = async () => {
}
} catch (error: any) {
console.error('上传失败:', error)
Message.error('上传失败: ' + (error.msg || '未知错误'))
Message.error(`上传失败: ${error.msg || '未知错误'}`)
} finally {
submitting.value = false
}
@ -217,16 +209,16 @@ const resetForm = () => {
flex-direction: column;
align-items: center;
justify-content: center;
&-box {
text-align: center;
}
&-text {
color: rgb(var(--primary-6));
font-size: 14px;
}
&-tip {
margin-top: 10px;
color: rgb(var(--gray-6));
@ -239,4 +231,4 @@ const resetForm = () => {
text-align: center;
}
}
</style>
</style>

View File

@ -24,4 +24,4 @@ defineOptions({ name: 'DataStorage' })
.data-storage-container {
padding: 20px;
}
</style>
</style>

View File

@ -3,7 +3,7 @@
<div class="panel-header">
<h3>自动识别设置</h3>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
<template #icon><IconClose /></template>
</a-button>
</div>
@ -30,10 +30,10 @@
<div class="section-header">
<h4>置信度</h4>
</div>
<a-slider
v-model:model-value="confidence"
:min="0"
:max="100"
<a-slider
v-model:model-value="confidence"
:min="0"
:max="100"
:step="10"
show-tooltip
:format-tooltip="(value) => `${value}%`"
@ -50,8 +50,8 @@
<span>加载缺陷类型...</span>
</div>
<a-checkbox-group v-else v-model:model-value="selectedDefectTypes">
<div
v-for="defectType in defectTypes"
<div
v-for="defectType in defectTypes"
:key="defectType.value"
class="defect-item"
>
@ -64,10 +64,10 @@
</div>
<div class="panel-actions">
<a-button type="primary" @click="handleStartRecognition" :loading="isRecognizing" block>
<a-button type="primary" :loading="isRecognizing" block @click="handleStartRecognition">
开始识别
</a-button>
<a-button @click="handleResetSettings" block>
<a-button block @click="handleResetSettings">
重置设置
</a-button>
</div>
@ -76,14 +76,14 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconClose } from '@arco-design/web-vue/es/icon'
import { useRouter } from 'vue-router'
import { listDefectType } from '@/apis/common/common'
import { getModelConfigList } from '@/apis/model-config'
import type { DefectTypeResp, DefectTypeOption } from '@/apis/common/type'
import type { DefectTypeOption, DefectTypeResp } from '@/apis/common/type'
import type { ModelConfigResponse } from '@/apis/model-config/type'
import { useRouter } from 'vue-router'
const props = defineProps<{
currentImage?: {
@ -122,16 +122,16 @@ const loadModelList = async () => {
try {
loadingModels.value = true
const response = await getModelConfigList()
if (response && response.rows) {
//
if (Array.isArray(response.rows)) {
modelList.value = response.rows;
modelList.value = response.rows
} else {
//
const responseData = response.rows;
modelList.value = [];
const responseData = response.rows
modelList.value = []
// API
if (responseData && Array.isArray(responseData)) {
modelList.value = responseData.map((item: any) => ({
@ -140,11 +140,11 @@ const loadModelList = async () => {
attachId: item.attachId || '',
confThreshold: item.confThreshold || 0.5,
nmsThreshold: item.nmsThreshold || 0.5,
modelPath: item.modelPath || ''
}));
modelPath: item.modelPath || '',
}))
}
}
//
if (modelList.value.length > 0) {
selectedAlgorithm.value = modelList.value[0].modelId
@ -164,10 +164,10 @@ const loadDefectTypes = async () => {
try {
loadingDefectTypes.value = true
const response = await listDefectType()
// - API
const defectTypeOptions: DefectTypeOption[] = []
//
response.data.forEach((item: DefectTypeResp) => {
//
@ -175,16 +175,16 @@ const loadDefectTypes = async () => {
defectTypeOptions.push({
value: code,
label: name,
code: code
code,
})
})
})
defectTypes.value = defectTypeOptions
//
if (defectTypes.value.length > 0) {
selectedDefectTypes.value = defectTypes.value.slice(0, 3).map(item => item.value)
selectedDefectTypes.value = defectTypes.value.slice(0, 3).map((item) => item.value)
}
} catch (error) {
console.error('获取缺陷类型失败:', error)
@ -216,7 +216,7 @@ const handleStartRecognition = async () => {
const settings = {
algorithm: selectedAlgorithm.value,
confidence: confidence.value,
defectTypes: selectedDefectTypes.value
defectTypes: selectedDefectTypes.value,
}
try {
@ -235,7 +235,7 @@ const handleStartRecognition = async () => {
const handleResetSettings = () => {
selectedAlgorithm.value = modelList.value.length > 0 ? modelList.value[0].modelId : ''
confidence.value = 80
selectedDefectTypes.value = defectTypes.value.length > 0 ? defectTypes.value.slice(0, 3).map(item => item.value) : []
selectedDefectTypes.value = defectTypes.value.length > 0 ? defectTypes.value.slice(0, 3).map((item) => item.value) : []
}
//
@ -258,7 +258,7 @@ const goToModelManagement = () => {
}
defineExpose({
setRecognizing
setRecognizing,
})
</script>
@ -275,7 +275,7 @@ defineExpose({
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 16px;
@ -290,11 +290,11 @@ defineExpose({
flex-direction: column;
overflow-y: scroll;
height: 600px;
.panel-section {
padding: 16px 20px;
border-bottom: 1px solid #f3f4f6;
.section-header {
display: flex;
justify-content: space-between;
@ -351,4 +351,4 @@ defineExpose({
}
}
}
</style>
</style>

View File

@ -3,10 +3,10 @@
<div class="form-header">
<h3>缺陷详情</h3>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
<template #icon><IconClose /></template>
</a-button>
</div>
<div class="form-body">
<a-form :model="form" layout="vertical">
<a-form-item label="缺陷名称" field="defectName">
@ -15,12 +15,12 @@
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!-- 缺陷类型 -->
<a-form-item
field="defectType"
@ -36,42 +36,42 @@
</a-select>
</a-form-item>
<!--轴向尺寸-->
<!-- 轴向尺寸 -->
<div class="dimension-fields">
<a-form-item
field="axialDimension"
label="轴向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
field="axialDimension"
label="轴向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.axialDimension"
:min="0"
:step="1"
placeholder="请输入轴向尺寸"
mode="button"
size="large"
v-model="form.axialDimension"
:min="0"
:step="1"
placeholder="请输入轴向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
<!--弦向尺寸 -->
<!-- 弦向尺寸 -->
<a-form-item
field="chordDimension"
label="弦向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
field="chordDimension"
label="弦向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.chordDimension"
:min="0"
:step="1"
placeholder="请输入弦向尺寸"
mode="button"
size="large"
v-model="form.chordDimension"
:min="0"
:step="1"
placeholder="请输入弦向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
</div>
<!-- 缺陷等级 -->
<a-form-item
field="defectLevel"
@ -87,49 +87,46 @@
<a-option v-for="level in defectLevels" :key="level.code" :value="level.code">{{ level.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea
v-model="form.description"
<a-textarea
v-model="form.description"
placeholder="请输入缺陷描述"
:rows="3"
/>
</a-form-item>
<a-form-item label="维修建议" field="repairIdea">
<a-textarea
v-model="form.repairIdea"
<a-textarea
v-model="form.repairIdea"
placeholder="请输入维修建议"
:rows="3"
/>
</a-form-item>
<div class="form-footer">
<a-button @click="$emit('close')" size="large">取消</a-button>
<a-button
type="primary"
size="large"
@click="handleSubmit"
:loading="submitting"
>
<template #icon><icon-save /></template>
保存缺陷
</a-button>
</div>
<a-button size="large" @click="$emit('close')">取消</a-button>
<a-button
type="primary"
size="large"
:loading="submitting"
@click="handleSubmit"
>
<template #icon><IconSave /></template>
保存缺陷
</a-button>
</div>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import { onMounted, reactive, ref, watch } from 'vue'
import { IconClose, IconSave } from '@arco-design/web-vue/es/icon'
import { Message } from '@arco-design/web-vue'
import type { Annotation } from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import { getDefectLevels, getDefectTypes, type DefectLevelType, type DefectType } from '@/apis/industrial-image/defect'
import { getDefectLevels } from '@/apis/industrial-image/defect'
import { listDefectType } from '@/apis/common/common'
import type { DefectTypeResp, DefectTypeOption } from '@/apis/common/type'
import type { DefectTypeOption } from '@/apis/common/type'
interface DefectFormData {
defectName: string
@ -167,15 +164,15 @@ const loadingDefectLevels = ref(false)
//
const form = reactive<DefectFormData>({
defectName: '',
defectType: '',
defectType: '',
defectTypeLabel: '',
defectLevel: '',
defectLevel: '',
defectLevelLabel: '',
defectPosition: '',
description: '',
repairIdea: '建议进行进一步检查',
axialDimension: 0, // null
chordDimension: 0 // null
axialDimension: 0, // null
chordDimension: 0, // null
})
//
@ -183,10 +180,10 @@ const loadDefectTypes = async () => {
try {
loadingDefectTypes.value = true
const response = await listDefectType()
// - API
const defectTypeOptions: DefectTypeOption[] = []
//
if (Array.isArray(response.data)) {
response.data.forEach((item: any) => {
@ -197,15 +194,15 @@ const loadDefectTypes = async () => {
defectTypeOptions.push({
value: code,
label: name as string,
code: code
code,
})
}
})
}
defectTypes.value = defectTypeOptions
console.log('缺陷类型选项:', defectTypeOptions)
//
if (defectTypeOptions.length > 0 && !form.defectType) {
form.defectType = defectTypeOptions[0].code
@ -220,26 +217,25 @@ const loadDefectTypes = async () => {
}
}
//
const loadDefectLevels = async () => {
try {
loadingDefectLevels.value = true
const response = await getDefectLevels()
//
const defectLevelOptions: DefectTypeOption[] = []
//
if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
// API
response.data.data.forEach(item => {
response.data.data.forEach((item) => {
defectLevelOptions.push({
code: item.code,
label: item.name, // 使namelabel
label: item.name, // 使namelabel
value: item.value,
name: item.name,
sort: item.sort
sort: item.sort,
})
})
defectLevels.value = defectLevelOptions
@ -250,10 +246,10 @@ const loadDefectLevels = async () => {
if (entries.length > 0) {
const [code, name] = entries[0]
defectLevelOptions.push({
code: code,
code,
label: name as string,
value: code,
name: name as string
name: name as string,
})
}
})
@ -264,17 +260,17 @@ const loadDefectLevels = async () => {
defectLevels.value = [
{ code: 'low', label: '轻微', value: 'low', name: '轻微' },
{ code: 'medium', label: '中等', value: 'medium', name: '中等' },
{ code: 'high', label: '严重', value: 'high', name: '严重' }
{ code: 'high', label: '严重', value: 'high', name: '严重' },
]
}
console.log('缺陷等级选项:', defectLevels.value)
//
if (defectLevels.value.length > 0 && !form.defectLevel) {
const mediumLevel = defectLevels.value.find(l =>
l.code.toLowerCase().includes('medium') ||
(l.name && l.name.includes('中'))
const mediumLevel = defectLevels.value.find((l) =>
l.code.toLowerCase().includes('medium')
|| (l.name && l.name.includes('中')),
)
form.defectLevel = mediumLevel?.code || defectLevels.value[0].code
form.defectLevelLabel = mediumLevel?.name || mediumLevel?.label || defectLevels.value[0].label
@ -285,7 +281,7 @@ const loadDefectLevels = async () => {
defectLevels.value = [
{ code: 'low', label: '轻微', value: 'low', name: '轻微' },
{ code: 'medium', label: '中等', value: 'medium', name: '中等' },
{ code: 'high', label: '严重', value: 'high', name: '严重' }
{ code: 'high', label: '严重', value: 'high', name: '严重' },
]
} finally {
loadingDefectLevels.value = false
@ -299,7 +295,7 @@ watch(() => props.annotation, (newAnnotation) => {
const isVirtualAnnotation = newAnnotation.id.startsWith('virtual-')
//
const isMultiAnnotation = newAnnotation.metadata?.isMultiAnnotation
if (isVirtualAnnotation) {
//
form.description = '手动添加的缺陷,请填写详细描述'
@ -316,7 +312,7 @@ watch(() => props.annotation, (newAnnotation) => {
form.description = `标注区域大小: ${Math.round(width)}x${Math.round(height)} 像素`
}
}
//
if (newAnnotation.label) {
form.defectName = newAnnotation.label
@ -329,46 +325,45 @@ const handleSubmit = async () => {
Message.warning('没有标注数据')
return
}
//
if (!form.defectName.trim()) {
Message.warning('请输入缺陷名称')
return
}
if (!form.defectType) {
Message.warning('请选择缺陷类型')
return
}
if (!form.defectLevel) {
Message.warning('请选择严重程度')
return
}
try {
submitting.value = true
//
const isMultiAnnotation = props.annotation.metadata?.isMultiAnnotation
const annotationCount = props.annotation.metadata?.allAnnotations?.length || 0
if (isMultiAnnotation && annotationCount > 0) {
Message.loading({
content: `正在保存包含${annotationCount}个标注区域的缺陷信息...`,
duration: 0
duration: 0,
})
} else {
Message.loading({
content: '正在保存缺陷信息...',
duration: 0
duration: 0,
})
}
//
console.log("form:",form);
console.log('form:', form)
emit('submit', form, props.annotation)
} catch (error) {
console.error('提交缺陷失败:', error)
Message.error('提交失败,请重试')
@ -379,7 +374,7 @@ const handleSubmit = async () => {
//
const handleDefectTypeChange = (value: string) => {
const selectedType = defectTypes.value.find(type => type.code === value)
const selectedType = defectTypes.value.find((type) => type.code === value)
if (selectedType) {
form.defectTypeLabel = selectedType.name
}
@ -387,7 +382,7 @@ const handleDefectTypeChange = (value: string) => {
//
const handleDefectLevelChange = (value: string) => {
const selectedLevel = defectLevels.value.find(level => level.code === value)
const selectedLevel = defectLevels.value.find((level) => level.code === value)
if (selectedLevel) {
form.defectLevelLabel = selectedLevel.name
}
@ -400,13 +395,13 @@ onMounted(() => {
if (props.annotation.label) {
form.defectName = props.annotation.label
}
// position
if ('position' in props.annotation && typeof props.annotation.position === 'string') {
form.defectPosition = props.annotation.position
}
}
//
loadDefectLevels()
loadDefectTypes()
@ -429,7 +424,7 @@ onMounted(() => {
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 16px;
@ -451,22 +446,22 @@ onMounted(() => {
justify-content: flex-end;
gap: 12px;
background: #f8f9fa;
.arco-btn {
min-width: 80px;
font-weight: 500;
&.arco-btn-primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
&:hover {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.4);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
@ -502,4 +497,4 @@ onMounted(() => {
font-weight: 500;
color: #374151;
}
</style>
</style>

View File

@ -76,20 +76,20 @@
<div class="form-group">
<label class="form-label">轴向尺寸</label>
<div class="size-input-group">
<a-button
size="small"
<a-button
size="small"
@click="defectForm.axial = Math.max(0, (defectForm.axial || 0) - 1)"
>
</a-button>
<a-input-number
v-model="defectForm.axial"
:min="0"
<a-input-number
v-model="defectForm.axial"
:min="0"
size="small"
style="width: 70px"
/>
<a-button
size="small"
<a-button
size="small"
@click="defectForm.axial = (defectForm.axial || 0) + 1"
>
+
@ -100,20 +100,20 @@
<div class="form-group">
<label class="form-label">弦向尺寸</label>
<div class="size-input-group">
<a-button
size="small"
<a-button
size="small"
@click="defectForm.chordwise = Math.max(0, (defectForm.chordwise || 0) - 1)"
>
</a-button>
<a-input-number
v-model="defectForm.chordwise"
:min="0"
<a-input-number
v-model="defectForm.chordwise"
:min="0"
size="small"
style="width: 70px"
/>
<a-button
size="small"
<a-button
size="small"
@click="defectForm.chordwise = (defectForm.chordwise || 0) + 1"
>
+
@ -128,20 +128,20 @@
<div class="form-group">
<label class="form-label">面积(mm²)</label>
<div class="size-input-group">
<a-button
size="small"
<a-button
size="small"
@click="defectForm.area = Math.max(0, (defectForm.area || 0) - 1)"
>
</a-button>
<a-input-number
v-model="defectForm.area"
:min="0"
<a-input-number
v-model="defectForm.area"
:min="0"
size="small"
style="width: 70px"
/>
<a-button
size="small"
<a-button
size="small"
@click="defectForm.area = (defectForm.area || 0) + 1"
>
+
@ -154,16 +154,16 @@
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">描述</label>
<a-textarea
v-model="defectForm.description"
<a-textarea
v-model="defectForm.description"
:rows="3"
placeholder="叶片前缘纵向裂纹长度约15mm宽度约2mm"
/>
<div class="action-button-container">
<a-button
size="small"
@click="handleSelectFromStandardDescription"
<a-button
size="small"
class="standard-library-btn"
@click="handleSelectFromStandardDescription"
>
从标准描述库选择
</a-button>
@ -175,16 +175,16 @@
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">维修建议</label>
<a-textarea
v-model="defectForm.repairIdea"
<a-textarea
v-model="defectForm.repairIdea"
:rows="3"
placeholder="建议进行表面修复处理,防止裂纹扩散"
/>
<div class="action-button-container">
<a-button
size="small"
@click="handleSelectFromStandardInfo"
<a-button
size="small"
class="standard-library-btn"
@click="handleSelectFromStandardInfo"
>
从标准信息库选择
</a-button>
@ -228,8 +228,8 @@
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">技术备注</label>
<a-textarea
v-model="defectForm.technicalNotes"
<a-textarea
v-model="defectForm.technicalNotes"
:rows="4"
placeholder="记录技术细节、处理方案、注意事项等"
/>
@ -239,8 +239,8 @@
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">修复记录</label>
<a-textarea
v-model="defectForm.repairRecord"
<a-textarea
v-model="defectForm.repairRecord"
:rows="3"
placeholder="记录修复过程、使用材料、处理结果等"
/>
@ -257,7 +257,7 @@
<!-- 无缺陷选中状态 -->
<div v-else class="no-defect-selected">
<icon-file class="empty-icon" />
<IconFile class="empty-icon" />
<p>请从左侧选择缺陷进行编辑</p>
</div>
@ -272,10 +272,10 @@
<div class="standard-library-content">
<a-list :data="standardDescriptions" :bordered="false">
<template #item="{ item }">
<a-list-item
:class="{ 'selected': selectedStandardDescription === item }"
@click="selectedStandardDescription = item"
<a-list-item
:class="{ selected: selectedStandardDescription === item }"
class="clickable-item"
@click="selectedStandardDescription = item"
>
<div class="description-item">
<div class="description-title">{{ item.title }}</div>
@ -298,10 +298,10 @@
<div class="standard-library-content">
<a-list :data="standardRepairIdeas" :bordered="false">
<template #item="{ item }">
<a-list-item
:class="{ 'selected': selectedStandardInfo === item }"
@click="selectedStandardInfo = item"
<a-list-item
:class="{ selected: selectedStandardInfo === item }"
class="clickable-item"
@click="selectedStandardInfo = item"
>
<div class="description-item">
<div class="description-title">{{ item.title }}</div>
@ -316,7 +316,7 @@
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { reactive, ref, watch } from 'vue'
import { IconFile } from '@arco-design/web-vue/es/icon'
import { Message, Modal } from '@arco-design/web-vue'
import type { DefectDetectionResult } from '@/apis/industrial-image/defect'
@ -348,7 +348,7 @@ const defectForm = reactive({
inspector: '',
recheckStatus: '',
technicalNotes: '',
repairRecord: ''
repairRecord: '',
})
//
@ -357,20 +357,20 @@ const selectedStandardDescription = ref<any>(null)
const standardDescriptions = ref([
{
title: '前缘裂纹模板',
content: '叶片前缘纵向裂纹长度约15mm宽度约2mm'
content: '叶片前缘纵向裂纹长度约15mm宽度约2mm',
},
{
title: '表面磨损模板',
content: '叶片表面出现明显磨损痕迹,影响空气动力学性能'
content: '叶片表面出现明显磨损痕迹,影响空气动力学性能',
},
{
title: '截蚀损伤模板',
content: '叶片前缘截蚀损伤表面粗糙度增加深度约1-3mm'
title: '截蚀损伤模板',
content: '叶片前缘截蚀损伤表面粗糙度增加深度约1-3mm',
},
{
title: '腐蚀斑点模板',
content: '叶片表面出现腐蚀斑点直径约5-10mm深度轻微'
}
content: '叶片表面出现腐蚀斑点直径约5-10mm深度轻微',
},
])
//
@ -379,24 +379,24 @@ const selectedStandardInfo = ref<any>(null)
const standardRepairIdeas = ref([
{
title: '裂纹修复建议',
content: '建议进行表面修复处理,防止裂纹扩散'
content: '建议进行表面修复处理,防止裂纹扩散',
},
{
title: '磨损处理建议',
content: '定期监测磨损程度,必要时进行表面打磨和重新涂层'
content: '定期监测磨损程度,必要时进行表面打磨和重新涂层',
},
{
title: '截蚀修复建议',
content: '建议进行前缘修复,使用专用胶泥填补并重新整形'
content: '建议进行前缘修复,使用专用胶泥填补并重新整形',
},
{
title: '腐蚀处理建议',
content: '清理腐蚀区域,涂抹防腐涂层,定期检查'
content: '清理腐蚀区域,涂抹防腐涂层,定期检查',
},
{
title: '严重损伤建议',
content: '建议立即停机检修,更换受损部件,避免安全隐患'
}
content: '建议立即停机检修,更换受损部件,避免安全隐患',
},
])
//
@ -413,7 +413,7 @@ watch(() => props.selectedDefect, (newDefect) => {
chordwise: newDefect.chordwise || 0,
area: calculateArea(newDefect.axial || 0, newDefect.chordwise || 0),
description: newDefect.description || '',
repairIdea: newDefect.repairIdea || ''
repairIdea: newDefect.repairIdea || '',
})
}
}, { immediate: true })
@ -437,7 +437,7 @@ const handleSave = () => {
const updatedDefect = {
...props.selectedDefect,
...defectForm
...defectForm,
}
emit('edit-defect', updatedDefect)
@ -457,7 +457,7 @@ const handleDelete = () => {
onOk: () => {
emit('delete-defect', props.selectedDefect!.defectId)
Message.success('缺陷已删除')
}
},
})
}
@ -476,7 +476,7 @@ const handleCancel = () => {
chordwise: props.selectedDefect.chordwise || 0,
area: calculateArea(props.selectedDefect.axial || 0, props.selectedDefect.chordwise || 0),
description: props.selectedDefect.description || '',
repairIdea: props.selectedDefect.repairIdea || ''
repairIdea: props.selectedDefect.repairIdea || '',
})
}
}
@ -706,4 +706,4 @@ const handleCancelStandardInfo = () => {
border-bottom: none;
}
}
</style>
</style>

View File

@ -3,21 +3,21 @@
<div class="panel-header">
<h2>缺陷管理</h2>
<div class="header-actions">
<a-button
type="primary"
<a-button
type="primary"
size="small"
@click="handleAddDefect"
:disabled="!canAddDefect"
@click="handleAddDefect"
>
<template #icon><icon-plus /></template>
<template #icon><IconPlus /></template>
新增缺陷
</a-button>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
<template #icon><IconClose /></template>
</a-button>
</div>
</div>
<div class="panel-content">
<!-- 项目树形结构 -->
<div class="tree-section">
@ -34,11 +34,11 @@
<template #title="node">
<div class="tree-node">
<span class="node-icon">
<icon-folder v-if="node.type === 'project'" />
<icon-settings v-else-if="node.type === 'turbine'" />
<icon-tool v-else-if="node.type === 'part'" />
<icon-bug v-else-if="node.type === 'defect'" />
<icon-apps v-else />
<IconFolder v-if="node.type === 'project'" />
<IconSettings v-else-if="node.type === 'turbine'" />
<IconTool v-else-if="node.type === 'part'" />
<IconBug v-else-if="node.type === 'defect'" />
<IconApps v-else />
</span>
<span class="node-title">{{ node.name }}</span>
<span v-if="node.imageCount" class="node-count">({{ node.imageCount }})</span>
@ -53,9 +53,9 @@
</template>
<script setup lang="ts">
import { IconClose, IconBug, IconFolder, IconPlus, IconSettings, IconTool, IconApps } from '@arco-design/web-vue/es/icon'
import { IconApps, IconBug, IconClose, IconFolder, IconPlus, IconSettings, IconTool } from '@arco-design/web-vue/es/icon'
import type { PropType } from 'vue'
import { ref, computed, watch } from 'vue'
import { computed } from 'vue'
//
export interface TreeNode {
@ -91,28 +91,28 @@ const props = defineProps({
//
defectList: {
type: Array as PropType<DefectInfo[]>,
default: () => []
default: () => [],
},
//
selectedDefect: {
type: Object as PropType<DefectInfo | null>,
default: null
default: null,
},
//
treeData: {
type: Array as PropType<TreeNode[]>,
default: () => []
default: () => [],
},
//
selectedKeys: {
type: Array as PropType<string[]>,
default: () => []
default: () => [],
},
//
loading: {
type: Boolean,
default: false
}
default: false,
},
})
//
@ -122,7 +122,7 @@ const emit = defineEmits([
'load-more',
'add-defect',
'turbine-select',
'close'
'close',
])
//
@ -130,64 +130,64 @@ const enhancedTreeData = computed(() => {
if (!props.treeData || !Array.isArray(props.treeData)) {
return []
}
return props.treeData.map(project => enhanceTreeNode(project))
return props.treeData.map((project) => enhanceTreeNode(project))
})
//
const enhanceTreeNode = (node: TreeNode): TreeNode => {
const enhancedNode = { ...node }
if (node.type === 'turbine') {
//
const defectNodes = getDefectNodesForTurbine(node.id)
enhancedNode.children = [
...(node.children || []).map(child => enhanceTreeNode(child)),
...defectNodes
...(node.children || []).map((child) => enhanceTreeNode(child)),
...defectNodes,
]
} else if (node.children) {
enhancedNode.children = node.children.map(child => enhanceTreeNode(child))
enhancedNode.children = node.children.map((child) => enhanceTreeNode(child))
}
return enhancedNode
}
//
const getDefectNodesForTurbine = (turbineId: string): TreeNode[] => {
// ID
const turbineDefects = props.defectList.filter(defect =>
defect.turbineId === turbineId || defect.imageId === turbineId
const turbineDefects = props.defectList.filter((defect) =>
defect.turbineId === turbineId || defect.imageId === turbineId,
)
//
return turbineDefects.map(defect => ({
return turbineDefects.map((defect) => ({
id: defect.id,
name: defect.defectName || '未命名缺陷',
type: 'defect',
defectLevel: defect.defectLevel,
defectType: defect.defectType,
detectionDate: defect.detectionDate,
defectData: defect //
defectData: defect, //
}))
}
//
const handleNodeSelect = (selectedKeys: string[], e: any) => {
const selectedNode = e.node
//
if (selectedNode?.type === 'turbine') {
emit('turbine-select', selectedNode.id)
}
//
if (selectedNode?.type === 'defect') {
const defect = selectedNode.defectData || props.defectList.find(d => d.id === selectedNode.id)
const defect = selectedNode.defectData || props.defectList.find((d) => d.id === selectedNode.id)
if (defect) {
emit('defect-select', defect)
}
}
emit('node-select', selectedKeys, e)
}
@ -216,34 +216,34 @@ const canAddDefect = computed(() => {
flex-direction: column;
height: 100%;
background-color: #f8f9fa;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
}
.panel-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tree-section {
flex: 1;
background-color: white;
@ -253,7 +253,7 @@ const canAddDefect = computed(() => {
overflow: hidden;
display: flex;
flex-direction: column;
h3 {
margin: 0;
font-size: 14px;
@ -263,7 +263,7 @@ const canAddDefect = computed(() => {
border-bottom: 1px solid #f2f3f5;
background: white;
}
.project-tree {
flex: 1;
padding: 16px;
@ -271,26 +271,26 @@ const canAddDefect = computed(() => {
overflow-x: hidden;
height: 0;
min-height: 0;
//
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
// Firefox
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
@ -317,7 +317,7 @@ const canAddDefect = computed(() => {
color: #6b7280;
flex-shrink: 0;
}
.defect-count {
font-size: 12px;
color: #ff4d4f;
@ -328,4 +328,4 @@ const canAddDefect = computed(() => {
}
}
}
</style>
</style>

View File

@ -1,16 +1,16 @@
<template>
<div class="header-toolbar">
<div class="toolbar-buttons">
<a-button size="large" @click="handleAutoAnnotate" :disabled="!currentImageId">
<template #icon><icon-robot /></template>
<a-button size="large" :disabled="!currentImageId" @click="handleAutoAnnotate">
<template #icon><IconRobot /></template>
自动标注
</a-button>
<a-button size="large" @click="handleManualAnnotate" :disabled="!currentImageId">
<template #icon><icon-edit /></template>
<a-button size="large" :disabled="!currentImageId" @click="handleManualAnnotate">
<template #icon><IconEdit /></template>
手动标注
</a-button>
<a-button size="large" @click="handleGenerateReport">
<template #icon><icon-file-image /></template>
<template #icon><IconFileImage /></template>
生成检测报告
</a-button>
</div>
@ -18,11 +18,10 @@
</template>
<script setup lang="ts">
import {
IconPlayArrow,
IconRobot,
import {
IconEdit,
IconFileImage
IconFileImage,
IconRobot,
} from '@arco-design/web-vue/es/icon'
defineProps<{
@ -40,8 +39,6 @@ const handleStart = () => {
emit('start')
}
const handleAutoAnnotate = () => {
emit('autoAnnotate')
}
@ -69,11 +66,11 @@ const handleGenerateReport = () => {
.arco-btn {
font-weight: 500;
border-radius: 6px;
&.arco-btn-primary {
background: #3b82f6;
border-color: #3b82f6;
&:hover {
background: #2563eb;
border-color: #2563eb;
@ -93,4 +90,4 @@ const handleGenerateReport = () => {
}
}
}
</style>
</style>

View File

@ -1,12 +1,12 @@
<template>
<a-modal
:visible="previewModalVisible"
title="图像详情"
width="80%"
:footer="footerButtons"
:mask-closable="true"
@update:visible="emit('update:previewModalVisible', $event)"
:confirm-loading="loading"
:visible="previewModalVisible"
title="图像详情"
width="80%"
:footer="footerButtons"
:mask-closable="true"
:confirm-loading="loading"
@update:visible="emit('update:previewModalVisible', $event)"
>
<div v-if="previewImage" class="modal-image-viewer">
<img :src="getImageUrl(previewImage.imagePath)" :alt="editingData.imageName" class="preview-image" />
@ -62,8 +62,8 @@
<a-form-item label="图片类型描述" field="imageTypeLabel">
<a-textarea
v-model="editingData.imageTypeLabel"
:auto-size="{ minRows: 2, maxRows: 4 }"
v-model="editingData.imageTypeLabel"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-form>
@ -77,7 +77,7 @@
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import axios from 'axios'
@ -115,7 +115,7 @@ const editingData = ref<{
imageType?: string
imageTypeLabel?: string
partId?: string
imageId?:string
imageId?: string
}>({
imagePath: '',
imageName: '',
@ -127,7 +127,7 @@ const editingData = ref<{
imageType: '',
imageTypeLabel: '',
partId: '',
imageId:''
imageId: '',
})
//
@ -140,7 +140,7 @@ watch(() => props.previewImage, (newVal) => {
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
const baseUrl = 'http://localhost:8080'
return `${baseUrl}${imagePath}`
}
@ -177,17 +177,18 @@ const handleSave = async () => {
imageId: editingData.value.imageId,
imageName: editingData.value.imageName,
imageResolution: editingData.value.imageResolution,
}
},
],
}
console.log("requestData:",requestData);
console.log('requestData:', requestData)
//
const response = await axios.post(
`http://pms.dtyx.net:9158/image/setting-info/${editingData.value.partId}`,
requestData
const response = await axios.put(
`http://localhost:8080/image/setting-info/${editingData.value.partId}`,
requestData,
)
if (response.data && response.data.code === 200 && response.data.success) {
if (response.data && response.data.code === 0) {
Message.success('图片信息保存成功')
emit('save', editingData.value)
emit('update:previewModalVisible', false)

View File

@ -4,50 +4,50 @@
<!-- 缩放控制工具栏 -->
<div v-if="selectedImage" class="zoom-controls">
<a-space>
<a-button size="small" @click="zoomOut" :disabled="scale <= MIN_SCALE">
<a-button size="small" :disabled="scale <= MIN_SCALE" @click="zoomOut">
<template #icon>
<icon-minus />
<IconMinus />
</template>
</a-button>
<span class="zoom-text">{{ Math.round(scale * 100) }}%</span>
<a-button size="small" @click="zoomIn" :disabled="scale >= MAX_SCALE">
<a-button size="small" :disabled="scale >= MAX_SCALE" @click="zoomIn">
<template #icon>
<icon-plus />
<IconPlus />
</template>
</a-button>
<a-button size="small" @click="resetZoom">
<template #icon>
<icon-refresh />
<IconRefresh />
</template>
重置
</a-button>
</a-space>
</div>
<div v-if="!selectedImage" class="preview-header">
<h3>暂无图像预览</h3>
<p>请从左侧选择具体查看图像</p>
</div>
<div class="preview-content">
<div
v-if="selectedImage"
class="image-viewer"
:class="{ 'dragging': isDragging }"
@wheel="handleWheel"
<div
v-if="selectedImage"
class="image-viewer"
:class="{ dragging: isDragging }"
@wheel="handleWheel"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
>
<div
<div
class="image-container"
:style="{
:style="{
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
transition: isDragging ? 'none' : 'transform 0.2s ease-out'
transition: isDragging ? 'none' : 'transform 0.2s ease-out',
}"
>
<img
:src="getImageUrl(selectedImage.imagePath)"
<img
:src="getImageUrl(selectedImage.imagePath)"
:alt="selectedImage.imageName"
draggable="false"
@load="onImageLoad"
@ -55,7 +55,7 @@
</div>
</div>
<div v-else class="empty-preview">
<icon-image class="empty-icon" />
<IconImage class="empty-icon" />
<p>请从下方缩略图中选择图像</p>
</div>
</div>
@ -64,7 +64,7 @@
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { nextTick, ref, watch } from 'vue'
import { IconImage, IconMinus, IconPlus, IconRefresh } from '@arco-design/web-vue/es/icon'
const props = defineProps<{
@ -114,7 +114,7 @@ const zoomIn = () => {
const zoomOut = () => {
const newScale = Math.max(MIN_SCALE, scale.value - SCALE_STEP)
scale.value = newScale
// 1
if (newScale <= 1) {
translateX.value = 0
@ -127,7 +127,7 @@ const onImageLoad = (event: Event) => {
const img = event.target as HTMLImageElement
imageWidth.value = img.naturalWidth
imageHeight.value = img.naturalHeight
nextTick(() => {
const container = img.parentElement?.parentElement
if (container) {
@ -142,25 +142,25 @@ const getBoundaryLimits = () => {
if (!imageWidth.value || !imageHeight.value || !containerWidth.value || !containerHeight.value) {
return { maxX: 0, maxY: 0, minX: 0, minY: 0 }
}
const scaledWidth = imageWidth.value * scale.value
const scaledHeight = imageHeight.value * scale.value
const maxX = Math.max(0, (scaledWidth - containerWidth.value) / 2)
const maxY = Math.max(0, (scaledHeight - containerHeight.value) / 2)
return {
maxX,
maxY,
minX: -maxX,
minY: -maxY
minY: -maxY,
}
}
//
const applyBoundaryLimits = () => {
const { maxX, maxY, minX, minY } = getBoundaryLimits()
translateX.value = Math.max(minX, Math.min(maxX, translateX.value))
translateY.value = Math.max(minY, Math.min(maxY, translateY.value))
}
@ -168,13 +168,13 @@ const applyBoundaryLimits = () => {
//
const handleWheel = (event: WheelEvent) => {
event.preventDefault()
const delta = event.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale.value + delta))
if (newScale !== scale.value) {
scale.value = newScale
// 1
if (newScale <= 1) {
translateX.value = 0
@ -202,10 +202,10 @@ const handleMouseMove = (event: MouseEvent) => {
event.preventDefault()
const deltaX = event.clientX - dragStartX.value
const deltaY = event.clientY - dragStartY.value
translateX.value = initialTranslateX.value + deltaX
translateY.value = initialTranslateY.value + deltaY
//
applyBoundaryLimits()
}
@ -226,7 +226,7 @@ const handleMouseLeave = () => {
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
const baseUrl = 'http://localhost:8080'
return `${baseUrl}${imagePath}`
}
</script>
@ -249,7 +249,7 @@ const getImageUrl = (imagePath: string): string => {
justify-content: center;
align-items: center;
flex-shrink: 0;
.zoom-text {
min-width: 50px;
text-align: center;
@ -356,4 +356,4 @@ const getImageUrl = (imagePath: string): string => {
}
}
}
</style>
</style>

View File

@ -15,10 +15,10 @@
<template #title="node">
<div class="tree-node">
<span class="node-icon">
<icon-folder v-if="node.type === 'project'" />
<icon-settings v-else-if="node.type === 'turbine'" />
<icon-tool v-else-if="node.type === 'part'" />
<icon-apps v-else />
<IconFolder v-if="node.type === 'project'" />
<IconSettings v-else-if="node.type === 'turbine'" />
<IconTool v-else-if="node.type === 'part'" />
<IconApps v-else />
</span>
<span class="node-title">{{ node.name }}</span>
<span v-if="node.imageCount" class="node-count">({{ node.imageCount }})</span>
@ -30,11 +30,11 @@
</template>
<script setup lang="ts">
import {
IconFolder,
IconSettings,
IconTool,
IconApps
import {
IconApps,
IconFolder,
IconSettings,
IconTool,
} from '@arco-design/web-vue/es/icon'
import type { ProjectTreeNode } from '@/apis/industrial-image'
@ -87,26 +87,26 @@ const handleLoadMore = (node: ProjectTreeNode) => {
overflow-x: hidden;
height: 0; /* 让 flex: 1 生效,自动计算高度 */
min-height: 0; /* 确保可以收缩 */
//
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
// Firefox
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
@ -136,4 +136,4 @@ const handleLoadMore = (node: ProjectTreeNode) => {
}
}
}
</style>
</style>

View File

@ -4,11 +4,11 @@
<h3>识别结果</h3>
<div class="header-actions">
<a-button type="text" @click="handleSaveResults">
<template #icon><icon-save /></template>
<template #icon><IconSave /></template>
保存结果
</a-button>
<a-button type="text" @click="handleExportResults">
<template #icon><icon-export /></template>
<template #icon><IconExport /></template>
导出
</a-button>
</div>
@ -43,28 +43,28 @@
<div class="results-list">
<h4>详细结果</h4>
<div class="result-items">
<div
v-for="(result, index) in results"
<div
v-for="(result, index) in results"
:key="index"
class="result-item"
:class="{ active: selectedResultIndex === index }"
@click="handleResultSelect(index)"
>
<div class="result-info">
<span class="result-type" :class="getDefectTypeClass(result.defectType)">
{{ getDefectTypeName(result.defectType) }}
</span>
<span class="result-confidence">{{ ((result.markInfo?.confidence || 0) * 100).toFixed(1) }}%</span>
<span class="result-type" :class="getDefectTypeClass(result.defectType)">
{{ getDefectTypeName(result.defectType) }}
</span>
<span class="result-confidence">{{ ((result.markInfo?.confidence || 0) * 100).toFixed(1) }}%</span>
</div>
<div class="result-position">
位置: {{ formatPosition(result.markInfo?.bbox) }}
</div>
<div class="result-size">
尺寸: {{ formatSize(result.markInfo?.bbox) }}
</div>
<div class="result-recommendation">
建议: {{ getRecommendation(result.defectType, result.markInfo?.confidence || 0) }}
</div>
<div class="result-position">
位置: {{ formatPosition(result.markInfo?.bbox) }}
</div>
<div class="result-size">
尺寸: {{ formatSize(result.markInfo?.bbox) }}
</div>
<div class="result-recommendation">
建议: {{ getRecommendation(result.defectType, result.markInfo?.confidence || 0) }}
</div>
</div>
</div>
</div>
@ -73,10 +73,10 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
import { computed, ref } from 'vue'
import {
IconExport,
IconSave,
IconExport
} from '@arco-design/web-vue/es/icon'
import { Message } from '@arco-design/web-vue'
@ -105,7 +105,7 @@ const statistics = computed(() => {
wear: 0,
wearConfidence: 0,
deformation: 0,
deformationConfidence: 0
deformationConfidence: 0,
}
if (props.results.length === 0) return stats
@ -113,7 +113,7 @@ const statistics = computed(() => {
//
const typeMapping: Record<string, string> = {
bmlw: 'crack', // ->
hfms: 'wear', // ->
hfms: 'wear', // ->
mpps: 'deformation', // ->
bcps: 'deformation', // ->
jbps: 'deformation', // ->
@ -127,38 +127,38 @@ const statistics = computed(() => {
const categorizedResults = props.results.reduce((acc, result) => {
//
const standardType = typeMapping[result.defectType] || result.defectType
//
if (!acc[standardType]) acc[standardType] = []
//
acc[standardType].push(result)
return acc
}, {} as Record<string, DefectDetectionResult[]>)
//
const crackResults = categorizedResults['crack'] || []
const crackResults = categorizedResults.crack || []
stats.crack = crackResults.length
stats.crackConfidence = crackResults.length > 0
stats.crackConfidence = crackResults.length > 0
? Math.round(crackResults.reduce((sum, r) => sum + ((r.markInfo?.confidence || 0) * 100), 0) / crackResults.length)
: 0
//
const corrosionResults = categorizedResults['corrosion'] || []
//
const corrosionResults = categorizedResults.corrosion || []
stats.corrosion = corrosionResults.length
stats.corrosionConfidence = corrosionResults.length > 0
? Math.round(corrosionResults.reduce((sum, r) => sum + ((r.markInfo?.confidence || 0) * 100), 0) / corrosionResults.length)
: 0
//
const wearResults = categorizedResults['wear'] || []
const wearResults = categorizedResults.wear || []
stats.wear = wearResults.length
stats.wearConfidence = wearResults.length > 0
? Math.round(wearResults.reduce((sum, r) => sum + ((r.markInfo?.confidence || 0) * 100), 0) / wearResults.length)
: 0
//
const deformationResults = categorizedResults['deformation'] || []
const deformationResults = categorizedResults.deformation || []
stats.deformation = deformationResults.length
stats.deformationConfidence = deformationResults.length > 0
? Math.round(deformationResults.reduce((sum, r) => sum + ((r.markInfo?.confidence || 0) * 100), 0) / deformationResults.length)
@ -219,7 +219,7 @@ const getDefectTypeClass = (type: string): string => {
//
const getRecommendation = (type: string, confidence: number): string => {
if (confidence < 60) return '建议人工复核'
const recommendations: Record<string, string> = {
crack: '立即维修,防止扩散',
corrosion: '清洁并涂保护层',
@ -227,7 +227,7 @@ const getRecommendation = (type: string, confidence: number): string => {
deformation: '检查结构完整性',
scratch: '轻微处理即可',
hole: '立即修补',
dirt: '清洁处理'
dirt: '清洁处理',
}
return recommendations[type] || '建议进一步检查'
}
@ -442,10 +442,10 @@ const handleExportResults = () => {
.results-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
&:hover {
background: #94a3b8;
}
}
}
</style>
</style>

View File

@ -1,35 +1,18 @@
import { ref, reactive, computed } from 'vue'
import { computed, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import {
type PartInfo,
type ProjectInfo,
type ProjectTreeNode,
type TurbineInfo,
deleteImage,
getImageList,
getPartList,
getProjectList,
getTurbineList,
getPartList,
getImageList,
deleteImage,
autoAnnotateImage,
generateReport,
batchUploadImages,
uploadImageToPartV2,
type ProjectTreeNode,
type IndustrialImage,
type ProjectInfo,
type TurbineInfo,
type PartInfo
} from '@/apis/industrial-image'
import {
detectDefects,
getDefectList,
addDefect,
updateDefect,
deleteDefect,
addManualDefect,
type DefectDetectionRequest,
type DefectDetectionResult,
type ManualDefectAddRequest
} from '@/apis/industrial-image/defect'
import type { TreeNodeData, ImageInfo } from '@/apis/industrial-image/type'
import type { DefectInfo } from '@/apis/industrial-image/defect'
import type { Annotation } from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import type { type DefectDetectionRequest, type DefectDetectionResult, DefectInfo, type ManualDefectAddRequest, addDefect, addManualDefect, deleteDefect, detectDefects, getDefectList, updateDefect } from '@/apis/industrial-image/defect'
export function useIndustrialImage() {
// 项目树数据
@ -65,7 +48,7 @@ export function useIndustrialImage() {
const isManualAnnotationMode = ref(false)
const defectList = ref<DefectInfo[]>([])
const selectedDefect = ref<DefectInfo | null>(null)
// Canvas 标注相关状态
const selectedDefectAnnotations = computed(() => {
if (!selectedDefect.value) return []
@ -73,13 +56,14 @@ export function useIndustrialImage() {
return [{
id: selectedDefect.value.id,
type: 'rectangle' as const,
points: selectedDefect.value.boundingBox ? [
{ x: selectedDefect.value.boundingBox.x, y: selectedDefect.value.boundingBox.y },
{ x: selectedDefect.value.boundingBox.x + selectedDefect.value.boundingBox.width,
y: selectedDefect.value.boundingBox.y + selectedDefect.value.boundingBox.height }
] : [],
points: selectedDefect.value.boundingBox
? [
{ x: selectedDefect.value.boundingBox.x, y: selectedDefect.value.boundingBox.y },
{ x: selectedDefect.value.boundingBox.x + selectedDefect.value.boundingBox.width, y: selectedDefect.value.boundingBox.y + selectedDefect.value.boundingBox.height },
]
: [],
color: selectedDefect.value.severity === 'high' ? '#ff4d4f' : '#faad14',
label: selectedDefect.value.defectType
label: selectedDefect.value.defectType,
}]
})
@ -102,7 +86,7 @@ export function useIndustrialImage() {
try {
loading.value = true
const response = await getProjectList({ page: 1, pageSize: 1000 })
// 处理不同的返回数据结构
let projects: ProjectInfo[] = []
if (Array.isArray(response.data)) {
@ -112,8 +96,8 @@ export function useIndustrialImage() {
} else {
projects = []
}
projectTreeData.value = projects.map(project => ({
projectTreeData.value = projects.map((project) => ({
id: project.projectId,
name: project.projectName,
type: 'project' as const,
@ -123,16 +107,16 @@ export function useIndustrialImage() {
expanded: false,
status: project.status,
createTime: project.createTime,
rawData: project
rawData: project,
}))
// 默认选中第一个项目
if (projectTreeData.value.length > 0) {
const firstProject = projectTreeData.value[0]
selectedKeys.value = [firstProject.id]
currentProjectId.value = firstProject.id
console.log('默认选中第一个项目:', firstProject.name)
// 加载图像列表
loadImageList()
}
@ -147,13 +131,13 @@ export function useIndustrialImage() {
// 懒加载子节点
const handleLoadMore = async (node: ProjectTreeNode) => {
if (!node || node.loaded) return
try {
if (node.type === 'project') {
const response = await getTurbineList({ projectId: node.id })
const turbines = response.data || []
node.children = turbines.map(turbine => ({
node.children = turbines.map((turbine) => ({
id: turbine.turbineId || turbine.projectId,
name: turbine.turbineName || turbine.turbineDesc || `机组${turbine.turbineId || turbine.projectId}`,
type: 'turbine' as const,
@ -164,16 +148,16 @@ export function useIndustrialImage() {
expanded: false,
status: turbine.status,
createTime: turbine.createTime,
rawData: turbine
rawData: turbine,
}))
} else if (node.type === 'turbine') {
const turbineData = node.rawData as TurbineInfo
const response = await getPartList({
turbineId: turbineData?.turbineId || node.id
const response = await getPartList({
turbineId: turbineData?.turbineId || node.id,
})
const parts = response.data || []
node.children = parts.map(part => ({
node.children = parts.map((part) => ({
id: part.partId,
name: part.partName || part.partType || `部件${part.partId}`,
type: 'part' as const,
@ -184,10 +168,10 @@ export function useIndustrialImage() {
expanded: false,
status: part.partType,
createTime: part.createTime,
rawData: part
rawData: part,
}))
}
node.loaded = true
} catch (error) {
console.error('加载子节点失败:', error)
@ -200,11 +184,11 @@ export function useIndustrialImage() {
if (keys.length > 0) {
selectedKeys.value = keys
currentProjectId.value = keys[0]
const selectedNode = findSelectedNode(projectTreeData.value, keys[0])
if (selectedNode) {
console.log('选中节点:', selectedNode)
// 如果选中的是机组节点,设置为当前选中的机组
if (selectedNode.type === 'turbine') {
selectedTurbineId.value = selectedNode.id
@ -212,7 +196,7 @@ export function useIndustrialImage() {
loadTurbineParts(selectedNode)
}
}
loadImageList()
}
}
@ -221,13 +205,13 @@ export function useIndustrialImage() {
const loadTurbineParts = async (turbineNode: ProjectTreeNode) => {
try {
const turbineData = turbineNode.rawData as TurbineInfo
const response = await getPartList({
turbineId: turbineData?.turbineId || turbineNode.id
const response = await getPartList({
turbineId: turbineData?.turbineId || turbineNode.id,
})
const parts = response.data || []
console.log('加载的部件信息:', parts)
currentTurbineParts.value = parts
console.log('设置部件信息完成,数量:', currentTurbineParts.value.length)
} catch (error) {
@ -241,13 +225,13 @@ export function useIndustrialImage() {
const loadImageList = async () => {
try {
loading.value = true
const selectedNode = findSelectedNode(projectTreeData.value, currentProjectId.value)
if (!selectedNode) {
imageList.value = []
return
}
// 构建查询参数
const params: {
imageTypes?: string[]
@ -255,12 +239,12 @@ export function useIndustrialImage() {
partId?: string
turbineId?: string
} = {}
// 添加关键字搜索
if (searchKeyword.value) {
params.keyword = searchKeyword.value
}
// 根据选中节点类型设置查询参数
if (selectedNode.type === 'project') {
// 选中项目时,不设置特定的过滤条件,查询所有图像
@ -275,7 +259,7 @@ export function useIndustrialImage() {
params.turbineId = turbineNode.id
}
}
console.log('查询图像列表参数:', params)
// 调用真实API
const response = await getImageList(params)
@ -288,15 +272,13 @@ export function useIndustrialImage() {
}
}
// 查找选中的节点
const findSelectedNode = (nodes: ProjectTreeNode[], nodeId: string): ProjectTreeNode | null => {
if (!nodes || !Array.isArray(nodes) || !nodeId) return null
for (const node of nodes) {
if (!node) continue
if (node.id === nodeId) {
return node
}
@ -311,14 +293,14 @@ export function useIndustrialImage() {
// 查找父节点
const findParentNode = (nodes: ProjectTreeNode[], childId: string): ProjectTreeNode | null => {
if (!nodes || !Array.isArray(nodes) || !childId) return null
for (const node of nodes) {
if (!node) continue
if (node.children && node.children.length > 0) {
const childFound = node.children.find(child => child && child.id === childId)
const childFound = node.children.find((child) => child && child.id === childId)
if (childFound) return node
const found = findParentNode(node.children, childId)
if (found) return found
}
@ -330,18 +312,18 @@ export function useIndustrialImage() {
const handleImageSelect = (image: any) => {
console.log('选中图像:', image)
selectedImage.value = image
// 尝试多种可能的图像ID字段名
const imageId = image.imageId || image.id || image.image_id || image.key || image.attachId
selectedImageId.value = imageId || ''
console.log('设置图像ID:', selectedImageId.value)
if (!selectedImageId.value) {
console.warn('图像ID为空图像数据结构:', image)
Message.warning('图像ID为空标注功能可能无法正常使用')
}
// 确保图像对象有必要的字段
if (!image.attachId && imageId) {
image.attachId = imageId
@ -349,7 +331,7 @@ export function useIndustrialImage() {
}
const handleImagePreview = (image: any) => {
console.log("image:",image);
console.log('image:', image)
previewImage.value = image
previewModalVisible.value = true
}
@ -372,7 +354,7 @@ export function useIndustrialImage() {
console.error('删除图像失败:', error)
Message.error('删除图像失败')
}
}
},
})
}
@ -397,7 +379,7 @@ export function useIndustrialImage() {
Message.warning('请先选择一个项目或机组')
return
}
if (selectedNode.type === 'turbine') {
// 如果选中的是机组,直接打开向导
importWizardVisible.value = true
@ -415,13 +397,13 @@ export function useIndustrialImage() {
Message.warning('请先选择一张图像')
return
}
// 切换到自动识别模式
isAutoRecognitionMode.value = true
// 清空之前的识别结果
recognitionResults.value = []
Message.info('已切换到自动识别模式')
}
@ -430,13 +412,13 @@ export function useIndustrialImage() {
Message.warning('请先选择一张图像')
return
}
// 切换到手动标注模式
isManualAnnotationMode.value = true
// 加载对应的缺陷列表
loadDefectList()
Message.info('已切换到手动标注模式')
}
@ -446,14 +428,14 @@ export function useIndustrialImage() {
Message.warning('请先选择一个项目、机组或部件')
return
}
// 设置报告单位数据
reportUnitData.value = {
id: selectedNode.id,
name: selectedNode.name,
type: selectedNode.type
type: selectedNode.type,
}
// 显示报告生成对话框
reportGenerationVisible.value = true
}
@ -476,7 +458,7 @@ export function useIndustrialImage() {
// 向导导入成功处理
const handleWizardImportSuccess = async (data: any) => {
console.log('向导导入成功:', data)
try {
// 构建上传参数
const uploadParams = {
@ -489,22 +471,22 @@ export function useIndustrialImage() {
temperatureMax: data.imageInfo.maxTemperature,
temperatureMin: data.imageInfo.minTemperature,
weather: data.imageInfo.weather,
windLevel: data.imageInfo.windPower
windLevel: data.imageInfo.windPower,
}
// 使用新的API接口上传图像
const response = await uploadImageToPartV2(
'default', // 图像源
data.part.partId, // 部件ID
data.images, // 文件列表
uploadParams
uploadParams,
)
console.log('批量上传响应:', response)
// 重新加载图像列表
loadImageList()
Message.success(`成功导入${data.images.length}张图像到${data.part.name}`)
} catch (error) {
console.error('批量上传失败:', error)
@ -529,14 +511,14 @@ export function useIndustrialImage() {
if (selectedKeys.value && selectedKeys.value.length > 0) {
const selectedNodeId = selectedKeys.value[0]
const selectedNode = findSelectedNode(projectTreeData.value, selectedNodeId)
if (!selectedNode) {
console.warn('找不到选中的节点:', selectedNodeId)
} else if (selectedNode.type === 'turbine') {
params.turbineId = selectedNode.id
} else if (selectedNode.type === 'part') {
params.partId = selectedNode.id
// 添加机组ID
const turbineNode = findParentNode(projectTreeData.value, selectedNode.id)
if (turbineNode) {
@ -547,30 +529,30 @@ export function useIndustrialImage() {
console.log('加载缺陷列表参数:', params)
const response = await getDefectList(params)
if (response.data && response.data.code === 0) {
// 检查返回数据结构
const resultData = response.data.data
// 将结果转换为DefectInfo[]类型
let defects: DefectInfo[] = []
if (resultData) {
if ('list' in resultData && Array.isArray(resultData.list)) {
// 分页结构
defects = resultData.list.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
...item
...item,
}))
} else if (Array.isArray(resultData)) {
// 直接数组结构
defects = resultData.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
...item
...item,
}))
}
}
console.log('处理后的缺陷列表数据:', defects)
defectList.value = defects
} else {
@ -590,7 +572,7 @@ export function useIndustrialImage() {
selectedDefectAnnotations.value = []
return
}
try {
// 尝试解析labelInfo
const labelInfo = JSON.parse(selectedDefect.value.labelInfo)
@ -604,7 +586,7 @@ export function useIndustrialImage() {
selectedDefectAnnotations.value = []
}
}
// 选择缺陷
const handleDefectSelect = (defect: DefectInfo) => {
selectedDefect.value = defect
@ -629,7 +611,7 @@ export function useIndustrialImage() {
repairIdea: defectForm.repairIdea,
imageId: selectedImage.value?.imageId || '',
detectionDate: new Date().toISOString(),
source: 'manual'
source: 'manual',
}
// 根据节点类型设置相关ID
@ -643,7 +625,7 @@ export function useIndustrialImage() {
}
const response = await addDefect(defectData)
if (response.data.success) {
Message.success('缺陷添加成功')
loadDefectList() // 重新加载缺陷列表
@ -659,7 +641,7 @@ export function useIndustrialImage() {
const handleEditDefect = async (defect: DefectDetectionResult) => {
try {
const response = await updateDefect(defect.defectId, defect)
if (response.data.success) {
Message.success('缺陷更新成功')
loadDefectList() // 重新加载缺陷列表
@ -675,7 +657,7 @@ export function useIndustrialImage() {
const handleDeleteDefectById = async (defectId: string) => {
try {
const response = await deleteDefect(defectId)
if (response.data.success) {
Message.success('缺陷删除成功')
loadDefectList() // 重新加载缺陷列表
@ -696,19 +678,19 @@ export function useIndustrialImage() {
Message.error('请先选择一张图像')
return
}
// 获取有效的图像ID
const imageId = selectedImageId.value || selectedImage.value?.imageId || selectedImage.value?.id
if (!imageId) {
console.error('无法获取图像ID选中的图像数据:', selectedImage.value)
Message.error('无法获取图像ID请重新选择图像')
return
}
console.log('添加标注 - 图像ID:', imageId)
console.log('添加标注 - 标注数据:', annotation)
// 构造符合API要求的缺陷数据
const defectData: ManualDefectAddRequest = {
attachId: selectedImage.value?.attachId || '',
@ -723,29 +705,31 @@ export function useIndustrialImage() {
detectionDate: new Date().toISOString(),
labelInfo: JSON.stringify(annotation),
markInfo: {
bbox: annotation.type === 'rectangle' ? [
Math.min(annotation.points[0].x, annotation.points[1].x),
Math.min(annotation.points[0].y, annotation.points[1].y),
Math.abs(annotation.points[1].x - annotation.points[0].x),
Math.abs(annotation.points[1].y - annotation.points[0].y)
] : [],
bbox: annotation.type === 'rectangle'
? [
Math.min(annotation.points[0].x, annotation.points[1].x),
Math.min(annotation.points[0].y, annotation.points[1].y),
Math.abs(annotation.points[1].x - annotation.points[0].x),
Math.abs(annotation.points[1].y - annotation.points[0].y),
]
: [],
clsId: 1,
confidence: 1.0,
label: annotation.defectType || '手动标注'
label: annotation.defectType || '手动标注',
},
repairIdea: '',
repairStatus: 'PENDING',
source: 'MANUAL'
source: 'MANUAL',
}
console.log('发送给后端的缺陷数据:', defectData)
// 调用API添加缺陷传递两个参数缺陷数据和图像ID
await addManualDefect(defectData, imageId)
// 重新加载缺陷列表
await loadDefectList()
Message.success('标注添加成功')
} catch (error) {
console.error('添加标注失败:', error)
@ -780,10 +764,10 @@ export function useIndustrialImage() {
const handleReportGenerated = async (reportData: any) => {
try {
console.log('生成报告数据:', reportData)
// 这里可以调用后端API生成报告
// await generateReport(reportData)
Message.success('报告生成成功')
reportGenerationVisible.value = false
} catch (error) {
@ -818,20 +802,20 @@ export function useIndustrialImage() {
try {
isRecognizing.value = true
// 调用真实的缺陷检测API
const detectParams: DefectDetectionRequest = {
confThreshold: settings.confidence / 100, // 转换为0-1的置信度
defectTypeList: settings.defectTypes,
imageId: selectedImage.value.imageId,
modelId: settings.algorithm // 使用算法作为模型ID
modelId: settings.algorithm, // 使用算法作为模型ID
}
const response = await detectDefects(detectParams)
// 检查返回结构适配API实际返回的数据格式
let detectedDefects: DefectDetectionResult[] = []
if (response && response.data) {
if (Array.isArray(response.data)) {
// 直接使用返回的数组
@ -841,29 +825,28 @@ export function useIndustrialImage() {
detectedDefects = response.data.data
}
}
if (detectedDefects.length > 0) {
recognitionResults.value = detectedDefects
// 检查是否有attachPath如果有则更新当前显示的图片路径
const firstResult = detectedDefects[0]
if (firstResult.attachPath && firstResult.attachPath !== selectedImage.value.imagePath) {
// 更新当前选中图像的路径为识别后返回的attachPath
selectedImage.value = {
...selectedImage.value,
imagePath: firstResult.attachPath
imagePath: firstResult.attachPath,
}
console.log('图片路径已更新为识别后的路径:', firstResult.attachPath)
}
Message.success(`识别完成,发现${detectedDefects.length}个缺陷`)
} else {
recognitionResults.value = []
Message.info('未发现缺陷')
}
// 不再需要类型映射因为RecognitionResults组件已经处理了自定义类型
} catch (error) {
console.error('识别失败:', error)
Message.error('识别失败')
@ -896,7 +879,7 @@ export function useIndustrialImage() {
try {
console.log('开始保存识别结果:', results)
let successCount = 0
let failCount = 0
const errors: string[] = []
@ -904,7 +887,7 @@ export function useIndustrialImage() {
// 为每个识别结果创建缺陷记录
for (let i = 0; i < results.length; i++) {
const result = results[i]
try {
// 构建缺陷数据,转换识别结果为手动添加缺陷的格式
const defectData: ManualDefectAddRequest = {
@ -924,16 +907,16 @@ export function useIndustrialImage() {
bbox: [0, 0, 0, 0],
clsId: 0,
confidence: 0.8,
label: result.defectType || '自动识别'
label: result.defectType || '自动识别',
},
repairIdea: result.repairIdea || '建议进一步检查确认',
repairStatus: result.repairStatus || 'pending',
source: 'auto' // 标记为自动识别来源
source: 'auto', // 标记为自动识别来源
}
// 调用手动添加缺陷接口
const response = await addManualDefect(defectData,selectedImage.value.imageId)
const response = await addManualDefect(defectData, selectedImage.value.imageId)
if (response.data.success) {
successCount++
console.log(`缺陷 ${i + 1} 保存成功:`, response.data.data)
@ -967,7 +950,6 @@ export function useIndustrialImage() {
if (successCount > 0) {
loadDefectList()
}
} catch (error) {
console.error('保存识别结果失败:', error)
Message.error('保存识别结果失败')
@ -1005,27 +987,27 @@ export function useIndustrialImage() {
annotationModalVisible,
loading,
currentImageId,
// 向导相关状态
importWizardVisible,
selectedTurbineId,
currentTurbineParts,
// 自动识别相关状态
isAutoRecognitionMode,
recognitionResults,
isRecognizing,
// 手动标注相关状态
isManualAnnotationMode,
defectList,
selectedDefect,
selectedDefectAnnotations,
// 报告生成相关状态
reportGenerationVisible,
reportUnitData,
// 方法
loadProjectTree,
handleLoadMore,
@ -1046,14 +1028,14 @@ export function useIndustrialImage() {
handleAnnotationSaved,
handleProcessSuccess,
handleWizardImportSuccess,
// 自动识别相关方法
handleCloseAutoRecognition,
handleStartRecognition,
handleRecognitionResultSelect,
handleSaveRecognitionResults,
handleExportRecognitionResults,
// 手动标注相关方法
handleCloseManualAnnotation,
loadDefectList,
@ -1061,15 +1043,15 @@ export function useIndustrialImage() {
handleAddDefect,
handleEditDefect,
handleDeleteDefectById,
// Canvas 标注相关方法
handleAnnotationAdd,
handleAnnotationUpdate,
handleAnnotationDelete,
// 报告生成相关方法
handleReportGenerated,
init
init,
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="industrial-image-container">
<!-- 头部按钮栏 -->
<HeaderToolbar
<HeaderToolbar
:current-image-id="currentImageId"
@start="handleStart"
@auto-annotate="handleAutoAnnotate"
@ -12,11 +12,13 @@
<!-- 主体内容区域 -->
<div class="main-content">
<!-- 上部区域 -->
<div class="top-section" :class="{
'auto-recognition-layout': isAutoRecognitionMode,
'manual-annotation-layout': isManualAnnotationMode,
'top-section-expanded': isImageListCollapsed || isAutoRecognitionMode || isManualAnnotationMode
}">
<div
class="top-section" :class="{
'auto-recognition-layout': isAutoRecognitionMode,
'manual-annotation-layout': isManualAnnotationMode,
'top-section-expanded': isImageListCollapsed || isAutoRecognitionMode || isManualAnnotationMode,
}"
>
<!-- 自动识别模式三列布局 -->
<template v-if="isAutoRecognitionMode">
<!-- 左侧自动识别设置面板 -->
@ -27,17 +29,17 @@
@start-recognition="handleStartRecognition"
/>
</div>
<!-- 中间图像显示区域 -->
<div class="auto-center-panel">
<ImagePreview
<ImagePreview
:selected-image="selectedImage"
/>
</div>
<!-- 右侧识别结果区域 -->
<div class="auto-right-panel">
<RecognitionResults
<RecognitionResults
:results="recognitionResults"
:is-processing="isRecognizing"
@result-select="handleRecognitionResultSelect"
@ -46,7 +48,7 @@
/>
</div>
</template>
<!-- 手动标注模式三列布局 -->
<template v-else-if="isManualAnnotationMode">
<!-- 左侧缺陷列表面板 -->
@ -64,10 +66,10 @@
@close="handleCloseManualAnnotation"
/>
</div>
<!-- 中间图像标注区域 -->
<div class="manual-center-panel">
<ImageCanvas
<ImageCanvas
:selected-image="selectedImage"
:annotations="selectedDefectAnnotations"
@annotation-finish="handleAnnotationFinish"
@ -75,7 +77,7 @@
@annotation-delete="handleAnnotationDelete"
/>
</div>
<!-- 右侧缺陷详情编辑区域 -->
<div class="manual-right-panel">
<DefectDetailsForm
@ -85,18 +87,18 @@
@submit="handleDefectFormSubmit"
/>
<div v-else class="no-annotation-prompt">
<icon-bug class="prompt-icon" />
<IconBug class="prompt-icon" />
<p>请在图像上绘制矩形标注缺陷</p>
</div>
</div>
</template>
<!-- 普通模式双列布局 -->
<template v-else>
<!-- 左侧面板 -->
<div class="left-panel">
<!-- 项目管理 -->
<ProjectTree
<ProjectTree
v-if="!isManualAnnotationMode"
:project-tree-data="projectTreeData"
:selected-keys="selectedKeys"
@ -104,7 +106,7 @@
@load-more="handleLoadMore"
/>
<!-- 手动标注 -->
<ManualAnnotation
<ManualAnnotation
v-if="isManualAnnotationMode"
:tree-data="projectTreeData"
:selected-keys="selectedKeys"
@ -121,7 +123,7 @@
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 图像预览区域 -->
<ImagePreview
<ImagePreview
:selected-image="selectedImage"
/>
</div>
@ -129,8 +131,8 @@
</div>
<!-- 下部工业图像列表区域 - 仅在非自动识别和非手动标注模式下显示 -->
<div v-if="!isAutoRecognitionMode && !isManualAnnotationMode" class="bottom-section" :class="{ 'collapsed': isImageListCollapsed }">
<IndustrialImageList
<div v-if="!isAutoRecognitionMode && !isManualAnnotationMode" class="bottom-section" :class="{ collapsed: isImageListCollapsed }">
<IndustrialImageList
:image-list="imageList"
:selected-image-id="selectedImageId"
@import-images="handleImportImages"
@ -146,25 +148,23 @@
<!-- 模态框 -->
<ImageModals
:preview-modal-visible="previewModalVisible"
:preview-image="previewImage"
:process-image="processImage"
:selected-image="selectedImage"
@update:preview-modal-visible="previewModalVisible = $event"
@save="handleSaveImageSuccess"
:preview-modal-visible="previewModalVisible"
:preview-image="previewImage"
:process-image="processImage"
:selected-image="selectedImage"
@update:preview-modal-visible="previewModalVisible = $event"
@save="handleSaveImageSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { IconBug } from '@arco-design/web-vue/es/icon'
import { Message } from '@arco-design/web-vue'
import HeaderToolbar from './components/HeaderToolbar.vue'
import ProjectTree from './components/ProjectTree.vue'
import ImagePreview from './components/ImagePreview.vue'
import ImageCanvas from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import IndustrialImageList from '@/components/IndustrialImageList/index.vue'
import ImageModals from './components/ImageModals.vue'
import RecognitionResults from './components/RecognitionResults.vue'
import DefectListPanel from './components/DefectListPanel.vue'
@ -172,9 +172,10 @@ import DefectDetailsForm from './components/DefectDetailsForm.vue'
import AutoRecognitionSettings from './components/AutoRecognitionSettings.vue'
import { useIndustrialImage } from './hooks/useIndustrialImage'
import type { TreeNode } from './components/DefectListPanel.vue'
import IndustrialImageList from '@/components/IndustrialImageList/index.vue'
import ImageCanvas from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import type { Annotation } from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import type { ManualDefectAddRequest, DefectInfo, AttachInfoData } from '@/apis/industrial-image/defect'
import { addManualDefect, getDefectList, uploadAnnotatedImage, getDefectTypes, getDefectLevels, type DefectType, type DefectLevelType } from '@/apis/industrial-image/defect'
import type { DefectInfo, type DefectLevelType, type DefectType, ManualDefectAddRequest, addManualDefect, getDefectLevels, getDefectList, getDefectTypes, uploadAnnotatedImage } from '@/apis/industrial-image/defect'
defineOptions({ name: 'IndustrialImageProcessing' })
@ -251,27 +252,27 @@ const {
annotationModalVisible,
loading,
currentImageId,
//
importWizardVisible,
selectedTurbineId,
currentTurbineParts,
//
isAutoRecognitionMode,
recognitionResults,
isRecognizing,
//
isManualAnnotationMode,
defectList,
selectedDefect,
selectedDefectAnnotations,
//
reportGenerationVisible,
reportUnitData,
//
loadProjectTree,
handleLoadMore,
@ -291,36 +292,36 @@ const {
handleAnnotationSaved,
handleProcessSuccess,
handleWizardImportSuccess,
//
handleCloseAutoRecognition,
handleStartRecognition,
handleRecognitionResultSelect,
handleSaveRecognitionResults,
handleExportRecognitionResults,
//
handleCloseManualAnnotation,
handleDefectSelect,
handleAddDefect,
handleEditDefect,
handleDeleteDefectById,
// Canvas
handleAnnotationAdd,
handleAnnotationUpdate,
handleAnnotationDelete,
//
handleReportGenerated,
init
init,
} = useIndustrialImage()
//
const treeData = computed(() => {
console.log('计算treeData, projectTreeData:', projectTreeData.value)
if (!projectTreeData.value || !Array.isArray(projectTreeData.value)) {
console.log('projectTreeData为空或不是数组返回测试数据')
//
@ -341,60 +342,60 @@ const treeData = computed(() => {
key: 'test-part-1',
title: '测试部件1',
type: 'part',
defectCount: 0
}
]
}
]
}
defectCount: 0,
},
],
},
],
},
] as TreeNode[]
}
const result = projectTreeData.value.map(project => {
const result = projectTreeData.value.map((project) => {
if (!project) {
console.warn('发现空的project对象')
return null
}
const projectNode = {
key: project.id || `project-${Math.random().toString(36).substr(2, 9)}`,
title: project.name || '未命名项目',
type: 'project' as const,
defectCount: 0,
children: project.children?.map(turbine => {
children: project.children?.map((turbine) => {
if (!turbine) {
console.warn('发现空的turbine对象')
return null
}
const turbineNode = {
key: turbine.id || `turbine-${Math.random().toString(36).substr(2, 9)}`,
title: turbine.name || '未命名机组',
type: 'turbine' as const,
defectCount: 0,
children: turbine.children?.map(part => {
children: turbine.children?.map((part) => {
if (!part) {
console.warn('发现空的part对象')
return null
}
return {
key: part.id || `part-${Math.random().toString(36).substr(2, 9)}`,
title: part.name || '未命名部件',
type: 'part' as const,
defectCount: 0
defectCount: 0,
}
}).filter(Boolean) || []
}).filter(Boolean) || [],
}
return turbineNode
}).filter(Boolean) || []
}).filter(Boolean) || [],
}
console.log('创建的project节点:', projectNode)
return projectNode
}).filter(item => item !== null) as TreeNode[]
}).filter((item) => item !== null) as TreeNode[]
console.log('最终的treeData:', result)
return result
})
@ -412,19 +413,19 @@ const handleAnnotationFinish = async (annotations: Annotation[], imageBlob: Blob
Message.error('请先选择一张图像')
return
}
//
const fileName = `annotated_${selectedImage.value?.imageName || 'image'}_${Date.now()}.png`
const uploadResponse = await uploadAnnotatedImage(imageBlob, fileName)
if ( !uploadResponse.data) {
if (!uploadResponse.data) {
Message.error('上传标注图片失败')
return
}
const attachInfo = uploadResponse.data
console.log('上传成功,附件信息:', attachInfo)
//
const combinedAnnotation: Annotation = {
id: `multi-annotation-${Date.now()}`,
@ -437,14 +438,13 @@ const handleAnnotationFinish = async (annotations: Annotation[], imageBlob: Blob
attachId: attachInfo,
// attachPath: attachInfo.attachPath,
// attachInfo: attachInfo,
allAnnotations: annotations
}
allAnnotations: annotations,
},
}
//
currentAnnotation.value = combinedAnnotation
Message.success(`成功绘制${annotations.length}个区域,请填写缺陷详情`)
} catch (error) {
console.error('处理标注完成失败:', error)
Message.error('处理标注失败')
@ -463,25 +463,25 @@ const handleAddDefectFromPanel = () => {
Message.warning('请先选择一个项目节点')
return
}
//
if (!selectedImage.value) {
Message.warning('请先选择一张图像')
return
}
//
const virtualAnnotation: Annotation = {
id: `virtual-${Date.now()}`,
type: 'rectangle',
points: [
{ x: 100, y: 100 },
{ x: 200, y: 200 }
{ x: 200, y: 200 },
],
color: '#ff4d4f',
label: '手动添加的缺陷'
label: '手动添加的缺陷',
}
//
currentAnnotation.value = virtualAnnotation
Message.info('请在右侧表单中填写缺陷详情')
@ -491,35 +491,35 @@ const handleAddDefectFromPanel = () => {
const handleTurbineSelect = async (turbineId: string) => {
try {
console.log('选中机组:', turbineId, '开始查询缺陷列表')
// API使loadDefectList
const response = await getDefectList({ turbineId })
console.log('API响应:', response)
if (response.data && response.data.code === 0 && response.data.data) {
//
const resultData = response.data.data
// DefectInfo[]
let defects: DefectInfo[] = []
if ('list' in resultData && Array.isArray(resultData.list)) {
//
defects = resultData.list.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
turbineId: turbineId,
...item
turbineId,
...item,
}))
} else if (Array.isArray(resultData)) {
//
defects = resultData.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
turbineId: turbineId,
...item
turbineId,
...item,
}))
}
//
defectList.value = defects
console.log('查询到缺陷列表:', defects.length, '条记录')
@ -542,39 +542,39 @@ const loadDefectList = async () => {
console.warn('未选择图像,无法加载缺陷列表')
return
}
// ID
const imageId = selectedImageId.value || selectedImage.value?.imageId || selectedImage.value?.id
if (!imageId) {
console.error('无法获取图像ID选中的图像数据:', selectedImage.value)
return
}
// API
const response = await getDefectList({ imageId })
if (response.data && response.data.code === 0 && response.data.data) {
//
const resultData = response.data.data
// DefectInfo[]
let defects: DefectInfo[] = []
if ('list' in resultData && Array.isArray(resultData.list)) {
//
defects = resultData.list.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
...item
...item,
}))
} else if (Array.isArray(resultData)) {
//
defects = resultData.map((item: any) => ({
id: item.id || item.defectId || `defect-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
...item
...item,
}))
}
//
defectList.value = defects
} else {
@ -596,24 +596,24 @@ const handleDefectFormSubmit = async (formData: any, annotation: Annotation) =>
Message.error('请先选择一张图像')
return
}
// ID
const imageId = selectedImageId.value || selectedImage.value?.imageId || selectedImage.value?.id
if (!imageId) {
console.error('无法获取图像ID选中的图像数据:', selectedImage.value)
Message.error('无法获取图像ID请重新选择图像')
return
}
//
const isMultiAnnotation = annotation.metadata?.isMultiAnnotation
const attachId = annotation.metadata?.attachId || ''
// const attachPath = annotation.metadata?.attachPath || ''
// API
const defectData = {
attachId: attachId,
attachId,
attachPath: '',
axial: formData.axialDimension,
chordwise: formData.chordDimension,
@ -627,61 +627,62 @@ const handleDefectFormSubmit = async (formData: any, annotation: Annotation) =>
defectTypeLabel: formData.defectTypeLabel || getDefectTypeLabel(formData.defectType),
description: formData.description,
detectionDate: new Date().toISOString().split('T')[0],
imageId: imageId,
imageId,
labelInfo: '',
// labelInfo: isMultiAnnotation ?
// JSON.stringify(annotation.metadata?.allAnnotations || []) :
// labelInfo: isMultiAnnotation ?
// JSON.stringify(annotation.metadata?.allAnnotations || []) :
// JSON.stringify(annotation),
markInfo: {
bbox: isMultiAnnotation ?
bbox: isMultiAnnotation
// bbox
(annotation.metadata?.allAnnotations || []).map(ann => [
Math.min(ann.points[0].x, ann.points[1].x),
Math.min(ann.points[0].y, ann.points[1].y),
Math.abs(ann.points[1].x - ann.points[0].x),
Math.abs(ann.points[1].y - ann.points[0].y)
]) :
? (annotation.metadata?.allAnnotations || []).map((ann) => [
Math.min(ann.points[0].x, ann.points[1].x),
Math.min(ann.points[0].y, ann.points[1].y),
Math.abs(ann.points[1].x - ann.points[0].x),
Math.abs(ann.points[1].y - ann.points[0].y),
])
//
annotation.type === 'rectangle' ? [
Math.min(annotation.points[0].x, annotation.points[1].x),
Math.min(annotation.points[0].y, annotation.points[1].y),
Math.abs(annotation.points[1].x - annotation.points[0].x),
Math.abs(annotation.points[1].y - annotation.points[0].y)
] : [],
: annotation.type === 'rectangle'
? [
Math.min(annotation.points[0].x, annotation.points[1].x),
Math.min(annotation.points[0].y, annotation.points[1].y),
Math.abs(annotation.points[1].x - annotation.points[0].x),
Math.abs(annotation.points[1].y - annotation.points[0].y),
]
: [],
clsId: 1,
confidence: 1.0,
label: formData.defectType
label: formData.defectType,
},
repairIdea: formData.repairIdea,
repairStatus: 'PENDING',
repairStatusLabel: '待处理',
source: 'MANUAL',
sourceLabel: '手动标注'
sourceLabel: '手动标注',
}
console.log('发送给后端的缺陷数据:', defectData)
// API
await addManualDefect(defectData as ManualDefectAddRequest, imageId)
// loading
Message.clear()
//
await loadDefectList()
//
currentAnnotation.value = null
//
const annotationCount = annotation.metadata?.allAnnotations?.length || 1
Message.success({
content: isMultiAnnotation ?
`成功保存包含${annotationCount}个标注区域的缺陷信息!` :
'缺陷信息保存成功!',
duration: 3000
content: isMultiAnnotation
? `成功保存包含${annotationCount}个标注区域的缺陷信息!`
: '缺陷信息保存成功!',
duration: 3000,
})
} catch (error) {
console.error('添加缺陷失败:', error)
Message.clear()
@ -692,10 +693,10 @@ const handleDefectFormSubmit = async (formData: any, annotation: Annotation) =>
//
const calculateBoundingBox = (annotations: Annotation[]): number[] => {
if (!annotations || annotations.length === 0) return [0, 0, 0, 0]
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
annotations.forEach(annotation => {
let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity
annotations.forEach((annotation) => {
if (annotation.points && annotation.points.length >= 2) {
const [p1, p2] = annotation.points
minX = Math.min(minX, p1.x, p2.x)
@ -704,19 +705,19 @@ const calculateBoundingBox = (annotations: Annotation[]): number[] => {
maxY = Math.max(maxY, p1.y, p2.y)
}
})
return [minX, minY, maxX - minX, maxY - minY]
}
//
const getDefectLevelLabel = (levelCode: string): string => {
const level = defectLevels.value.find(l => l.code === levelCode)
const level = defectLevels.value.find((l) => l.code === levelCode)
return level ? level.name : levelCode
}
//
const getDefectTypeLabel = (typeCode: string): string => {
const type = defectTypes.value.find(t => t.code === typeCode)
const type = defectTypes.value.find((t) => t.code === typeCode)
return type ? type.name : typeCode
}
@ -774,7 +775,7 @@ onMounted(() => {
.top-section.auto-recognition-layout {
display: flex;
height: 100%; /* 自动识别模式占据全部高度 */
.auto-left-panel {
width: 280px;
border-right: 1px solid #e5e7eb;
@ -784,7 +785,7 @@ onMounted(() => {
background: #f8f9fa;
flex-shrink: 0;
}
.auto-center-panel {
flex: 1;
border-right: 1px solid #e5e7eb;
@ -794,12 +795,12 @@ onMounted(() => {
background: #ffffff;
position: relative;
min-width: 0; /* 确保flex项目可以收缩 */
/* 让图像区域居中显示 */
align-items: center;
justify-content: center;
}
.auto-right-panel {
width: 320px;
overflow: hidden;
@ -814,7 +815,7 @@ onMounted(() => {
.top-section.manual-annotation-layout {
display: flex;
height: 100%; /* 手动标注模式占据全部高度 */
.manual-left-panel {
width: 280px;
border-right: 1px solid #e5e7eb;
@ -824,7 +825,7 @@ onMounted(() => {
background: #f8f9fa;
flex-shrink: 0;
}
.manual-center-panel {
flex: 1;
border-right: 1px solid #e5e7eb;
@ -834,12 +835,12 @@ onMounted(() => {
background: #ffffff;
position: relative;
min-width: 0; /* 确保flex项目可以收缩 */
/* 让图像区域居中显示 */
align-items: center;
justify-content: center;
}
.manual-right-panel {
width: 320px;
overflow: hidden;
@ -856,7 +857,7 @@ onMounted(() => {
flex-direction: column;
overflow: hidden;
transition: flex 0.3s ease, height 0.3s ease;
&.collapsed {
flex: 0 0 0;
min-height: 0;
@ -875,17 +876,17 @@ onMounted(() => {
margin: 20px;
border-radius: 8px;
border: 2px dashed #d1d5db;
.prompt-icon {
font-size: 48px;
margin-bottom: 16px;
color: #9ca3af;
}
p {
margin: 0;
font-size: 14px;
text-align: center;
}
}
</style>
</style>

View File

@ -6,11 +6,11 @@
<div class="operation-bar">
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
<template #icon><IconPlus /></template>
新建配置
</a-button>
<a-button @click="refreshList">
<template #icon><icon-refresh /></template>
<template #icon><IconRefresh /></template>
刷新
</a-button>
</a-space>
@ -31,9 +31,9 @@
:data="tableData"
:loading="loading"
:pagination="pagination"
row-key="modelId"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
row-key="modelId"
>
<template #columns>
<!-- <a-table-column title="模型ID" data-index="modelId" /> -->
@ -53,11 +53,11 @@
<template #cell="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleEdit(record)">
<template #icon><icon-edit /></template>
<template #icon><IconEdit /></template>
编辑
</a-button>
<a-button type="text" size="small" @click="handleView(record)">
<template #icon><icon-eye /></template>
<template #icon><IconEye /></template>
查看
</a-button>
<a-popconfirm
@ -65,7 +65,7 @@
@ok="handleDelete(record)"
>
<a-button type="text" status="danger" size="small">
<template #icon><icon-delete /></template>
<template #icon><IconDelete /></template>
删除
</a-button>
</a-popconfirm>
@ -81,9 +81,9 @@
<a-modal
v-model:visible="formVisible"
:title="isEdit ? '编辑模型配置' : '新增模型配置'"
unmount-on-close
@cancel="closeForm"
@before-ok="handleSubmit"
unmount-on-close
>
<a-form
ref="formRef"
@ -125,24 +125,24 @@
:custom-request="uploadModelFile"
>
<a-button type="primary">
<template #icon><icon-upload /></template>
<template #icon><IconUpload /></template>
上传模型文件
</a-button>
</a-upload>
<a-button
v-if="formData.attachId"
status="danger"
<a-button
v-if="formData.attachId"
status="danger"
@click="formData.attachId = ''"
>
<template #icon><icon-delete /></template>
<template #icon><IconDelete /></template>
清除
</a-button>
</a-space>
<div class="upload-tip" v-if="uploadingFile">
<div v-if="uploadingFile" class="upload-tip">
<a-spin /> 上传中...{{ uploadProgress }}%
</div>
<div class="upload-tip success" v-if="uploadedFileName && !uploadingFile">
<icon-check-circle /> 已上传: {{ uploadedFileName }}
<div v-if="uploadedFileName && !uploadingFile" class="upload-tip success">
<IconCheckCircle /> 已上传: {{ uploadedFileName }}
</div>
</a-form-item>
<a-form-item
@ -151,13 +151,13 @@
:validate-trigger="['change', 'blur']"
:rules="[
{ required: true, message: '请输入置信度阈值' },
{
{
validator: (value, cb) => {
if (value < 0 || value > 1) {
cb('置信度阈值必须在0-1之间');
}
}
}
},
},
]"
>
<a-input-number
@ -176,13 +176,13 @@
:validate-trigger="['change', 'blur']"
:rules="[
{ required: true, message: '请输入NMS阈值' },
{
{
validator: (value, cb) => {
if (value < 0 || value > 1) {
cb('NMS阈值必须在0-1之间');
}
}
}
},
},
]"
>
<a-input-number
@ -202,9 +202,9 @@
<a-modal
v-model:visible="detailVisible"
title="模型配置详情"
@cancel="closeDetail"
:footer="false"
unmount-on-close
@cancel="closeDetail"
>
<a-descriptions
:data="detailData"
@ -217,33 +217,33 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { Message } from '@arco-design/web-vue';
import {
IconPlus,
IconRefresh,
IconEdit,
IconEye,
import { onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconCheckCircle,
IconDelete,
IconEdit,
IconEye,
IconPlus,
IconRefresh,
IconUpload,
IconCheckCircle
} from '@arco-design/web-vue/es/icon';
import {
getModelConfigList,
getModelConfigDetail,
createModelConfig,
updateModelConfig,
deleteModelConfig
} from '@/apis/model-config';
import { addAttachment } from '@/apis/attach-info';
import type { ModelConfigRequest, ModelConfigResponse } from '@/apis/model-config/type';
} from '@arco-design/web-vue/es/icon'
import {
createModelConfig,
deleteModelConfig,
getModelConfigDetail,
getModelConfigList,
updateModelConfig,
} from '@/apis/model-config'
import { addAttachment } from '@/apis/attach-info'
import type { ModelConfigRequest, ModelConfigResponse } from '@/apis/model-config/type'
defineOptions({ name: 'ModelConfig' });
defineOptions({ name: 'ModelConfig' })
//
const tableData = ref<any[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
const tableData = ref<any[]>([])
const loading = ref(false)
const searchKeyword = ref('')
//
const pagination = reactive({
@ -252,117 +252,117 @@ const pagination = reactive({
total: 0,
showTotal: true,
showJumper: true,
});
})
//
const formRef = ref();
const formVisible = ref(false);
const isEdit = ref(false);
const formRef = ref()
const formVisible = ref(false)
const isEdit = ref(false)
const formData = reactive<ModelConfigRequest>({
attachId: '',
confThreshold: 0,
modelId: '',
modelName: '',
nmsThreshold: 0,
});
})
//
const detailVisible = ref(false);
const detailData = ref<{ label: string; value: any }[]>([]);
const detailVisible = ref(false)
const detailData = ref<{ label: string, value: any }[]>([])
//
const uploadingFile = ref(false);
const uploadProgress = ref(0);
const uploadedFileName = ref('');
const uploadingFile = ref(false)
const uploadProgress = ref(0)
const uploadedFileName = ref('')
//
const fetchModelConfigList = async () => {
loading.value = true;
loading.value = true
try {
const res = await getModelConfigList({
keyword: searchKeyword.value,
page: pagination.current,
pageSize: pagination.pageSize,
});
})
if (res.data) {
tableData.value = Array.isArray(res.data) ? res.data : [];
pagination.total = Array.isArray(res.data) ? res.data.length : 0;
tableData.value = Array.isArray(res.data) ? res.data : []
pagination.total = Array.isArray(res.data) ? res.data.length : 0
} else {
tableData.value = [];
pagination.total = 0;
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取模型配置列表失败:', error);
Message.error('获取模型配置列表失败');
console.error('获取模型配置列表失败:', error)
Message.error('获取模型配置列表失败')
} finally {
loading.value = false;
loading.value = false
}
};
}
//
onMounted(() => {
fetchModelConfigList();
});
fetchModelConfigList()
})
//
const refreshList = () => {
fetchModelConfigList();
};
fetchModelConfigList()
}
//
const handleSearch = () => {
pagination.current = 1;
fetchModelConfigList();
};
pagination.current = 1
fetchModelConfigList()
}
//
const onPageChange = (page: number) => {
pagination.current = page;
fetchModelConfigList();
};
pagination.current = page
fetchModelConfigList()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize;
fetchModelConfigList();
};
pagination.pageSize = pageSize
fetchModelConfigList()
}
//
const handleAdd = () => {
isEdit.value = false;
resetForm();
formVisible.value = true;
};
isEdit.value = false
resetForm()
formVisible.value = true
}
//
const handleEdit = async (record: ModelConfigResponse) => {
isEdit.value = true;
resetForm();
isEdit.value = true
resetForm()
try {
const res = await getModelConfigDetail(record.modelId);
const res = await getModelConfigDetail(record.modelId)
if (res.data) {
const modelData = res.data.data;
formData.modelId = modelData.modelId;
formData.modelName = modelData.modelName;
formData.attachId = modelData.attachId;
formData.confThreshold = modelData.confThreshold;
formData.nmsThreshold = modelData.nmsThreshold;
formVisible.value = true;
const modelData = res.data.data
formData.modelId = modelData.modelId
formData.modelName = modelData.modelName
formData.attachId = modelData.attachId
formData.confThreshold = modelData.confThreshold
formData.nmsThreshold = modelData.nmsThreshold
formVisible.value = true
}
} catch (error) {
console.error('获取详情失败:', error);
Message.error('获取详情失败');
console.error('获取详情失败:', error)
Message.error('获取详情失败')
}
};
}
//
const handleView = async (record: ModelConfigResponse) => {
try {
const res = await getModelConfigDetail(record.modelId);
const res = await getModelConfigDetail(record.modelId)
if (res.data) {
const modelData = res.data.data;
const modelData = res.data.data
detailData.value = [
// { label: 'ID', value: modelData.modelId },
{ label: '模型名称', value: modelData.modelName },
@ -370,111 +370,111 @@ const handleView = async (record: ModelConfigResponse) => {
{ label: '模型附件ID', value: modelData.attachId || '-' },
{ label: '置信度阈值', value: modelData.confThreshold ? modelData.confThreshold.toFixed(2) : '-' },
{ label: 'NMS阈值', value: modelData.nmsThreshold ? modelData.nmsThreshold.toFixed(2) : '-' },
];
detailVisible.value = true;
]
detailVisible.value = true
}
} catch (error) {
console.error('获取详情失败:', error);
Message.error('获取详情失败');
console.error('获取详情失败:', error)
Message.error('获取详情失败')
}
};
}
//
const handleDelete = async (record: ModelConfigResponse) => {
try {
await deleteModelConfig(record.modelId);
Message.success('删除成功');
fetchModelConfigList();
await deleteModelConfig(record.modelId)
Message.success('删除成功')
fetchModelConfigList()
} catch (error) {
console.error('删除失败:', error);
Message.error('删除失败');
console.error('删除失败:', error)
Message.error('删除失败')
}
};
}
//
const handleSubmit = async () => {
if (!formRef.value) return false;
if (!formRef.value) return false
try {
await formRef.value.validate();
await formRef.value.validate()
const submitData = {
modelId: formData.modelId,
modelName: formData.modelName,
attachId: formData.attachId,
confThreshold: formData.confThreshold,
nmsThreshold: formData.nmsThreshold,
};
if (isEdit.value) {
await updateModelConfig(submitData);
Message.success('更新成功');
} else {
await createModelConfig(submitData);
Message.success('创建成功');
}
closeForm();
fetchModelConfigList();
return true;
if (isEdit.value) {
await updateModelConfig(submitData)
Message.success('更新成功')
} else {
await createModelConfig(submitData)
Message.success('创建成功')
}
closeForm()
fetchModelConfigList()
return true
} catch (error) {
console.error('提交失败:', error);
Message.error('提交失败: ' + (error as any)?.msg || '未知错误');
return false;
console.error('提交失败:', error)
Message.error(`提交失败: ${(error as any)?.msg}` || '未知错误')
return false
}
};
}
//
const resetForm = () => {
// formData.modelId = '';
formData.modelName = '';
formData.attachId = '';
formData.confThreshold = 0;
formData.nmsThreshold = 0;
};
formData.modelName = ''
formData.attachId = ''
formData.confThreshold = 0
formData.nmsThreshold = 0
}
//
const closeForm = () => {
formVisible.value = false;
resetForm();
};
formVisible.value = false
resetForm()
}
//
const closeDetail = () => {
detailVisible.value = false;
detailData.value = [];
};
detailVisible.value = false
detailData.value = []
}
//
const uploadModelFile = async (options: any) => {
const {onProgress, onError, onSuccess, fileItem, name} = options;
const { onProgress, onError, onSuccess, fileItem, name } = options
try {
// FormData
const uploadFormData = new FormData();
uploadFormData.append(name || 'file', fileItem.file);
const uploadFormData = new FormData()
uploadFormData.append(name || 'file', fileItem.file)
// API /attach-info/{businessType}
const res = await addAttachment(uploadFormData);
const res = await addAttachment(uploadFormData)
if (res && res.data) {
// ID
const attachId = res.data;
formData.attachId = attachId;
uploadedFileName.value = fileItem.file.name;
Message.success('模型文件上传成功');
onSuccess(res);
const attachId = res.data
formData.attachId = attachId
uploadedFileName.value = fileItem.file.name
Message.success('模型文件上传成功')
onSuccess(res)
} else {
Message.error('模型文件上传失败');
onError(new Error('上传失败'));
Message.error('模型文件上传失败')
onError(new Error('上传失败'))
}
} catch (error) {
console.error('上传失败:', error);
Message.error('上传失败: ' + (error as any)?.msg || '未知错误');
onError(error);
console.error('上传失败:', error)
Message.error(`上传失败: ${(error as any)?.msg}` || '未知错误')
onError(error)
} finally {
uploadingFile.value = false;
uploadingFile.value = false
}
};
}
</script>
<style scoped lang="scss">
@ -489,10 +489,10 @@ const uploadModelFile = async (options: any) => {
margin-top: 8px;
color: #86909c;
font-size: 14px;
&.success {
color: #00b42a;
}
}
}
</style>
</style>

View File

@ -1,484 +1,481 @@
<template>
<div class="app-container">
<a-card class="general-card" title="应用使用数据" :bordered="false">
<a-row :gutter="16">
<a-col :span="6" v-for="(stat, index) in appStatistics" :key="index">
<a-card class="stat-card">
<a-statistic
:title="stat.title"
:value="stat.value"
:precision="stat.precision || 0"
:suffix="stat.suffix || ''"
>
<template #prefix>
<component :is="stat.icon" />
<div class="app-container">
<a-card class="general-card" title="应用使用数据" :bordered="false">
<a-row :gutter="16">
<a-col v-for="(stat, index) in appStatistics" :key="index" :span="6">
<a-card class="stat-card">
<a-statistic
:title="stat.title"
:value="stat.value"
:precision="stat.precision || 0"
:suffix="stat.suffix || ''"
>
<template #prefix>
<component :is="stat.icon" />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="应用访问量趋势" :bordered="false">
<div ref="appVisitChart" style="height: 350px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="应用使用时长分布" :bordered="false">
<div ref="appTimeChart" style="height: 350px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="应用使用详情" :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="Web端">
<a-table :columns="appColumns" :data-source="webAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="应用访问量趋势" :bordered="false">
<div ref="appVisitChart" style="height: 350px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="应用使用时长分布" :bordered="false">
<div ref="appTimeChart" style="height: 350px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="应用使用详情" :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="Web端">
<a-table :columns="appColumns" :data-source="webAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<icon-up v-if="record.trend > 0" style="color: #52c41a" />
<icon-down v-if="record.trend < 0" style="color: #ff4d4f" />
<icon-minus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<IconUp v-if="record.trend > 0" style="color: #52c41a" />
<IconDown v-if="record.trend < 0" style="color: #ff4d4f" />
<IconMinus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="2" tab="移动端">
<a-table :columns="appColumns" :data-source="mobileAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<icon-up v-if="record.trend > 0" style="color: #52c41a" />
<icon-down v-if="record.trend < 0" style="color: #ff4d4f" />
<icon-minus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="2" tab="移动端">
<a-table :columns="appColumns" :data-source="mobileAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="3" tab="微信小程序">
<a-table :columns="appColumns" :data-source="miniAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<icon-up v-if="record.trend > 0" style="color: #52c41a" />
<icon-down v-if="record.trend < 0" style="color: #ff4d4f" />
<icon-minus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<IconUp v-if="record.trend > 0" style="color: #52c41a" />
<IconDown v-if="record.trend < 0" style="color: #ff4d4f" />
<IconMinus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-card>
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="终端设备分布" :bordered="false">
<div ref="deviceDistributionChart" style="height: 400px"></div>
</a-card>
</a-col>
</a-row>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="3" tab="微信小程序">
<a-table :columns="appColumns" :data-source="miniAppData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<IconUp v-if="record.trend > 0" style="color: #52c41a" />
<IconDown v-if="record.trend < 0" style="color: #ff4d4f" />
<IconMinus v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive, nextTick } from 'vue'
import { Statistic } from '@arco-design/web-vue'
import * as echarts from 'echarts'
import {
IconApps,
IconMobile,
IconUser,
IconClockCircle,
IconUp,
IconDown,
IconMinus
} from '@arco-design/web-vue/es/icon'
const appVisitChart = ref(null)
const appTimeChart = ref(null)
const deviceDistributionChart = ref(null)
//
const appStatistics = reactive([
{
title: '应用总数',
value: 12,
icon: IconApps
},
{
title: '活跃应用',
value: 8,
icon: IconApps
},
{
title: '日均访问量',
value: 1258,
icon: IconUser
},
{
title: '日均使用时长',
value: 3.5,
precision: 1,
suffix: '小时',
icon: IconClockCircle
}
])
// 使
const appColumns = [
{
title: '应用名称',
dataIndex: 'appName',
key: 'appName',
},
{
title: '访问量',
dataIndex: 'visitCount',
key: 'visitCount',
sorter: (a, b) => a.visitCount - b.visitCount,
},
{
title: '用户数',
dataIndex: 'userCount',
key: 'userCount',
sorter: (a, b) => a.userCount - b.userCount,
},
{
title: '使用率',
dataIndex: 'usageRate',
key: 'usageRate',
sorter: (a, b) => a.usageRate - b.usageRate,
},
{
title: '平均使用时长',
dataIndex: 'averageTime',
key: 'averageTime',
},
{
title: '环比上月',
dataIndex: 'trend',
key: 'trend',
sorter: (a, b) => a.trend - b.trend,
}
]
// Web
const webAppData = [
{
key: '1',
appName: '企业管理后台',
visitCount: 3560,
userCount: 320,
usageRate: 95,
averageTime: '2.5小时',
trend: 8.5
},
{
key: '2',
appName: '项目管理系统',
visitCount: 2980,
userCount: 285,
usageRate: 90,
averageTime: '3小时',
trend: 5.2
},
{
key: '3',
appName: '数据分析平台',
visitCount: 2450,
userCount: 210,
usageRate: 85,
averageTime: '2小时',
trend: 3.8
},
{
key: '4',
appName: '客户管理系统',
visitCount: 1980,
userCount: 180,
usageRate: 75,
averageTime: '1.5小时',
trend: -2.1
},
{
key: '5',
appName: '知识库系统',
visitCount: 1560,
userCount: 150,
usageRate: 65,
averageTime: '1小时',
trend: 0
}
]
//
const mobileAppData = [
{
key: '1',
appName: '移动办公APP',
visitCount: 4250,
userCount: 350,
usageRate: 98,
averageTime: '3.5小时',
trend: 12.5
},
{
key: '2',
appName: '外勤管理APP',
visitCount: 3680,
userCount: 320,
usageRate: 92,
averageTime: '4小时',
trend: 9.8
},
{
key: '3',
appName: '项目跟踪APP',
visitCount: 2850,
userCount: 260,
usageRate: 88,
averageTime: '3小时',
trend: 7.5
},
{
key: '4',
appName: '客户拜访APP',
visitCount: 2120,
userCount: 180,
usageRate: 72,
averageTime: '2小时',
trend: -1.5
}
]
//
const miniAppData = [
{
key: '1',
appName: '企业服务小程序',
visitCount: 5680,
userCount: 420,
usageRate: 96,
averageTime: '1.5小时',
trend: 15.8
},
{
key: '2',
appName: '客户自助小程序',
visitCount: 4850,
userCount: 380,
usageRate: 90,
averageTime: '1小时',
trend: 10.5
},
{
key: '3',
appName: '员工工具小程序',
visitCount: 3920,
userCount: 345,
usageRate: 85,
averageTime: '0.8小时',
trend: 8.2
}
]
//
const getUsageRateColor = (rate) => {
if (rate >= 90) return '#52c41a'
if (rate >= 70) return '#1890ff'
return '#faad14'
}
const getTrendColor = (trend) => {
if (trend > 0) return '#52c41a'
if (trend < 0) return '#ff4d4f'
return '#1890ff'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="终端设备分布" :bordered="false">
<div ref="deviceDistributionChart" style="height: 400px"></div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import * as echarts from 'echarts'
import {
IconApps,
IconClockCircle,
IconDown,
IconMinus,
IconUp,
IconUser,
} from '@arco-design/web-vue/es/icon'
const appVisitChart = ref(null)
const appTimeChart = ref(null)
const deviceDistributionChart = ref(null)
//
const appStatistics = reactive([
{
title: '应用总数',
value: 12,
icon: IconApps,
},
{
title: '活跃应用',
value: 8,
icon: IconApps,
},
{
title: '日均访问量',
value: 1258,
icon: IconUser,
},
{
title: '日均使用时长',
value: 3.5,
precision: 1,
suffix: '小时',
icon: IconClockCircle,
},
])
// 使
const appColumns = [
{
title: '应用名称',
dataIndex: 'appName',
key: 'appName',
},
{
title: '访问量',
dataIndex: 'visitCount',
key: 'visitCount',
sorter: (a, b) => a.visitCount - b.visitCount,
},
{
title: '用户数',
dataIndex: 'userCount',
key: 'userCount',
sorter: (a, b) => a.userCount - b.userCount,
},
{
title: '使用率',
dataIndex: 'usageRate',
key: 'usageRate',
sorter: (a, b) => a.usageRate - b.usageRate,
},
{
title: '平均使用时长',
dataIndex: 'averageTime',
key: 'averageTime',
},
{
title: '环比上月',
dataIndex: 'trend',
key: 'trend',
sorter: (a, b) => a.trend - b.trend,
},
]
// Web
const webAppData = [
{
key: '1',
appName: '企业管理后台',
visitCount: 3560,
userCount: 320,
usageRate: 95,
averageTime: '2.5小时',
trend: 8.5,
},
{
key: '2',
appName: '项目管理系统',
visitCount: 2980,
userCount: 285,
usageRate: 90,
averageTime: '3小时',
trend: 5.2,
},
{
key: '3',
appName: '数据分析平台',
visitCount: 2450,
userCount: 210,
usageRate: 85,
averageTime: '2小时',
trend: 3.8,
},
{
key: '4',
appName: '客户管理系统',
visitCount: 1980,
userCount: 180,
usageRate: 75,
averageTime: '1.5小时',
trend: -2.1,
},
{
key: '5',
appName: '知识库系统',
visitCount: 1560,
userCount: 150,
usageRate: 65,
averageTime: '1小时',
trend: 0,
},
]
//
const mobileAppData = [
{
key: '1',
appName: '移动办公APP',
visitCount: 4250,
userCount: 350,
usageRate: 98,
averageTime: '3.5小时',
trend: 12.5,
},
{
key: '2',
appName: '外勤管理APP',
visitCount: 3680,
userCount: 320,
usageRate: 92,
averageTime: '4小时',
trend: 9.8,
},
{
key: '3',
appName: '项目跟踪APP',
visitCount: 2850,
userCount: 260,
usageRate: 88,
averageTime: '3小时',
trend: 7.5,
},
{
key: '4',
appName: '客户拜访APP',
visitCount: 2120,
userCount: 180,
usageRate: 72,
averageTime: '2小时',
trend: -1.5,
},
]
//
const miniAppData = [
{
key: '1',
appName: '企业服务小程序',
visitCount: 5680,
userCount: 420,
usageRate: 96,
averageTime: '1.5小时',
trend: 15.8,
},
{
key: '2',
appName: '客户自助小程序',
visitCount: 4850,
userCount: 380,
usageRate: 90,
averageTime: '1小时',
trend: 10.5,
},
{
key: '3',
appName: '员工工具小程序',
visitCount: 3920,
userCount: 345,
usageRate: 85,
averageTime: '0.8小时',
trend: 8.2,
},
]
//
const getUsageRateColor = (rate) => {
if (rate >= 90) return '#52c41a'
if (rate >= 70) return '#1890ff'
return '#faad14'
}
const getTrendColor = (trend) => {
if (trend > 0) return '#52c41a'
if (trend < 0) return '#ff4d4f'
return '#1890ff'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
// 访
if (appVisitChart.value) {
const visitChart = echarts.init(appVisitChart.value)
visitChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Web端', '移动端', '小程序']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'Web端',
type: 'line',
data: [2500, 2800, 3200, 3100, 2950, 1800, 1200]
if (appVisitChart.value) {
const visitChart = echarts.init(appVisitChart.value)
visitChart.setOption({
tooltip: {
trigger: 'axis',
},
{
name: '移动端',
type: 'line',
data: [3200, 3500, 3800, 3600, 3400, 2800, 2500]
legend: {
data: ['Web端', '移动端', '小程序'],
},
{
name: '小程序',
type: 'line',
data: [4500, 4800, 5200, 4900, 4700, 3900, 3500]
}
]
})
}
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
name: 'Web端',
type: 'line',
data: [2500, 2800, 3200, 3100, 2950, 1800, 1200],
},
{
name: '移动端',
type: 'line',
data: [3200, 3500, 3800, 3600, 3400, 2800, 2500],
},
{
name: '小程序',
type: 'line',
data: [4500, 4800, 5200, 4900, 4700, 3900, 3500],
},
],
})
}
// 使
if (appTimeChart.value) {
const timeChart = echarts.init(appTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '使用时长分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: 'Web端' },
{ value: 45, name: '移动端' },
{ value: 20, name: '小程序' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}
//
if (deviceDistributionChart.value) {
const deviceChart = echarts.init(deviceDistributionChart.value)
deviceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['Windows PC', 'Mac', 'iOS', 'Android', '微信']
},
series: [
{
name: '访问量',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [5200, 3800, 6500, 8200, 9500]
if (appTimeChart.value) {
const timeChart = echarts.init(appTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item',
},
{
name: '用户数',
type: 'bar',
stack: 'total',
label: {
show: true
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '使用时长分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: 'Web端' },
{ value: 45, name: '移动端' },
{ value: 20, name: '小程序' },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
emphasis: {
focus: 'series'
],
})
}
//
if (deviceDistributionChart.value) {
const deviceChart = echarts.init(deviceDistributionChart.value)
deviceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
data: [280, 220, 320, 380, 420]
}
]
})
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
data: ['Windows PC', 'Mac', 'iOS', 'Android', '微信'],
},
series: [
{
name: '访问量',
type: 'bar',
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series',
},
data: [5200, 3800, 6500, 8200, 9500],
},
{
name: '用户数',
type: 'bar',
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series',
},
data: [280, 220, 320, 380, 420],
},
],
})
}
//
window.addEventListener('resize', () => {
if (appVisitChart.value) {
const visitChart = echarts.getInstanceByDom(appVisitChart.value)
visitChart?.resize()
}
if (appTimeChart.value) {
const timeChart = echarts.getInstanceByDom(appTimeChart.value)
timeChart?.resize()
}
if (deviceDistributionChart.value) {
const deviceChart = echarts.getInstanceByDom(deviceDistributionChart.value)
deviceChart?.resize()
}
})
if (appVisitChart.value) {
const visitChart = echarts.getInstanceByDom(appVisitChart.value)
visitChart?.resize()
}
if (appTimeChart.value) {
const timeChart = echarts.getInstanceByDom(appTimeChart.value)
timeChart?.resize()
}
if (deviceDistributionChart.value) {
const deviceChart = echarts.getInstanceByDom(deviceDistributionChart.value)
deviceChart?.resize()
}
})
})
</script>
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
.stat-card {
margin-bottom: 20px;
text-align: center;
}
</style>
</style>

View File

@ -1,375 +1,375 @@
<template>
<div class="app-container">
<a-card class="general-card" title="功能使用情况" :bordered="false">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="功能模块使用频率" :bordered="false">
<div ref="moduleUsageChart" style="height: 400px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="各部门功能使用分布" :bordered="false">
<div ref="departmentUsageChart" style="height: 350px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="功能使用时长占比" :bordered="false">
<div ref="usageTimeChart" style="height: 350px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="功能使用详情" :bordered="false">
<a-table :columns="functionColumns" :data-source="functionData" :pagination="{ pageSize: 10 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<arrow-up-outlined v-if="record.trend > 0" style="color: #52c41a" />
<arrow-down-outlined v-if="record.trend < 0" style="color: #ff4d4f" />
<minus-outlined v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</template>
</a-table>
</a-card>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
// @ant-design/icons-vue
// import { ArrowUpOutlined, ArrowDownOutlined, MinusOutlined } from '@ant-design/icons-vue'
<div class="app-container">
<a-card class="general-card" title="功能使用情况" :bordered="false">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="功能模块使用频率" :bordered="false">
<div ref="moduleUsageChart" style="height: 400px"></div>
</a-card>
</a-col>
</a-row>
const moduleUsageChart = ref(null)
const departmentUsageChart = ref(null)
const usageTimeChart = ref(null)
// 使
const functionColumns = [
{
title: '功能模块',
dataIndex: 'module',
key: 'module',
},
{
title: '子功能',
dataIndex: 'function',
key: 'function',
},
{
title: '使用次数',
dataIndex: 'usageCount',
key: 'usageCount',
sorter: (a, b) => a.usageCount - b.usageCount,
},
{
title: '使用率',
dataIndex: 'usageRate',
key: 'usageRate',
sorter: (a, b) => a.usageRate - b.usageRate,
},
{
title: '平均使用时长',
dataIndex: 'averageTime',
key: 'averageTime',
sorter: (a, b) => a.averageTime - b.averageTime,
},
{
title: '环比上月',
dataIndex: 'trend',
key: 'trend',
sorter: (a, b) => a.trend - b.trend,
}
]
const functionData = [
{
key: '1',
module: '组织架构',
function: '人员管理',
usageCount: 1245,
usageRate: 92,
averageTime: '15分钟',
trend: 5.2
},
{
key: '2',
module: '组织架构',
function: '角色管理',
usageCount: 865,
usageRate: 78,
averageTime: '12分钟',
trend: 3.8
},
{
key: '3',
module: '资产管理',
function: '设备管理',
usageCount: 1056,
usageRate: 85,
averageTime: '18分钟',
trend: 7.5
},
{
key: '4',
module: '资产管理',
function: '库存管理',
usageCount: 932,
usageRate: 80,
averageTime: '14分钟',
trend: -2.1
},
{
key: '5',
module: '产品与服务',
function: '产品管理',
usageCount: 1120,
usageRate: 88,
averageTime: '20分钟',
trend: 4.3
},
{
key: '6',
module: '产品与服务',
function: '服务管理',
usageCount: 986,
usageRate: 82,
averageTime: '16分钟',
trend: 0
},
{
key: '7',
module: '项目管理',
function: '项目模板',
usageCount: 1320,
usageRate: 95,
averageTime: '25分钟',
trend: 8.7
},
{
key: '8',
module: '项目管理',
function: '合同管理',
usageCount: 1150,
usageRate: 90,
averageTime: '22分钟',
trend: 6.2
},
{
key: '9',
module: '施工操作台',
function: '外业施工',
usageCount: 1280,
usageRate: 93,
averageTime: '30分钟',
trend: 9.5
},
{
key: '10',
module: '施工操作台',
function: '数据处理',
usageCount: 1180,
usageRate: 91,
averageTime: '28分钟',
trend: 5.8
},
{
key: '11',
module: '聊天平台',
function: '消息管理',
usageCount: 1420,
usageRate: 98,
averageTime: '35分钟',
trend: 12.3
},
{
key: '12',
module: '企业设置',
function: '企业信息',
usageCount: 720,
usageRate: 65,
averageTime: '10分钟',
trend: -1.5
}
]
//
const getUsageRateColor = (rate) => {
if (rate >= 90) return '#52c41a'
if (rate >= 70) return '#1890ff'
return '#faad14'
}
const getTrendColor = (trend) => {
if (trend > 0) return '#52c41a'
if (trend < 0) return '#ff4d4f'
return '#1890ff'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="各部门功能使用分布" :bordered="false">
<div ref="departmentUsageChart" style="height: 350px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="功能使用时长占比" :bordered="false">
<div ref="usageTimeChart" style="height: 350px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="功能使用详情" :bordered="false">
<a-table :columns="functionColumns" :data-source="functionData" :pagination="{ pageSize: 10 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'usageRate'">
<a-progress :percent="record.usageRate" :stroke-color="getUsageRateColor(record.usageRate)" />
</template>
<template v-if="column.dataIndex === 'trend'">
<span :style="{ color: getTrendColor(record.trend) }">
{{ record.trend >= 0 ? '+' : '' }}{{ record.trend }}%
<arrow-up-outlined v-if="record.trend > 0" style="color: #52c41a" />
<arrow-down-outlined v-if="record.trend < 0" style="color: #ff4d4f" />
<minus-outlined v-if="record.trend === 0" style="color: #1890ff" />
</span>
</template>
</template>
</a-table>
</a-card>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue'
import * as echarts from 'echarts'
// @ant-design/icons-vue
// import { ArrowUpOutlined, ArrowDownOutlined, MinusOutlined } from '@ant-design/icons-vue'
const moduleUsageChart = ref(null)
const departmentUsageChart = ref(null)
const usageTimeChart = ref(null)
// 使
const functionColumns = [
{
title: '功能模块',
dataIndex: 'module',
key: 'module',
},
{
title: '子功能',
dataIndex: 'function',
key: 'function',
},
{
title: '使用次数',
dataIndex: 'usageCount',
key: 'usageCount',
sorter: (a, b) => a.usageCount - b.usageCount,
},
{
title: '使用率',
dataIndex: 'usageRate',
key: 'usageRate',
sorter: (a, b) => a.usageRate - b.usageRate,
},
{
title: '平均使用时长',
dataIndex: 'averageTime',
key: 'averageTime',
sorter: (a, b) => a.averageTime - b.averageTime,
},
{
title: '环比上月',
dataIndex: 'trend',
key: 'trend',
sorter: (a, b) => a.trend - b.trend,
},
]
const functionData = [
{
key: '1',
module: '组织架构',
function: '人员管理',
usageCount: 1245,
usageRate: 92,
averageTime: '15分钟',
trend: 5.2,
},
{
key: '2',
module: '组织架构',
function: '角色管理',
usageCount: 865,
usageRate: 78,
averageTime: '12分钟',
trend: 3.8,
},
{
key: '3',
module: '资产管理',
function: '设备管理',
usageCount: 1056,
usageRate: 85,
averageTime: '18分钟',
trend: 7.5,
},
{
key: '4',
module: '资产管理',
function: '库存管理',
usageCount: 932,
usageRate: 80,
averageTime: '14分钟',
trend: -2.1,
},
{
key: '5',
module: '产品与服务',
function: '产品管理',
usageCount: 1120,
usageRate: 88,
averageTime: '20分钟',
trend: 4.3,
},
{
key: '6',
module: '产品与服务',
function: '服务管理',
usageCount: 986,
usageRate: 82,
averageTime: '16分钟',
trend: 0,
},
{
key: '7',
module: '项目管理',
function: '项目模板',
usageCount: 1320,
usageRate: 95,
averageTime: '25分钟',
trend: 8.7,
},
{
key: '8',
module: '项目管理',
function: '合同管理',
usageCount: 1150,
usageRate: 90,
averageTime: '22分钟',
trend: 6.2,
},
{
key: '9',
module: '施工操作台',
function: '外业施工',
usageCount: 1280,
usageRate: 93,
averageTime: '30分钟',
trend: 9.5,
},
{
key: '10',
module: '施工操作台',
function: '数据处理',
usageCount: 1180,
usageRate: 91,
averageTime: '28分钟',
trend: 5.8,
},
{
key: '11',
module: '聊天平台',
function: '消息管理',
usageCount: 1420,
usageRate: 98,
averageTime: '35分钟',
trend: 12.3,
},
{
key: '12',
module: '企业设置',
function: '企业信息',
usageCount: 720,
usageRate: 65,
averageTime: '10分钟',
trend: -1.5,
},
]
//
const getUsageRateColor = (rate) => {
if (rate >= 90) return '#52c41a'
if (rate >= 70) return '#1890ff'
return '#faad14'
}
const getTrendColor = (trend) => {
if (trend > 0) return '#52c41a'
if (trend < 0) return '#ff4d4f'
return '#1890ff'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
// 使
if (moduleUsageChart.value) {
const moduleChart = echarts.init(moduleUsageChart.value)
moduleChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['使用次数', '使用人数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['组织架构', '资产管理', '产品与服务', '项目管理', '施工操作台', '聊天平台', '企业设置', '系统资源管理']
},
series: [
{
name: '使用次数',
type: 'bar',
data: [2110, 1988, 2106, 2470, 2460, 1420, 720, 650]
if (moduleUsageChart.value) {
const moduleChart = echarts.init(moduleUsageChart.value)
moduleChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
{
name: '使用人数',
type: 'bar',
data: [320, 302, 315, 335, 340, 356, 120, 85]
}
]
})
}
legend: {
data: ['使用次数', '使用人数'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
data: ['组织架构', '资产管理', '产品与服务', '项目管理', '施工操作台', '聊天平台', '企业设置', '系统资源管理'],
},
series: [
{
name: '使用次数',
type: 'bar',
data: [2110, 1988, 2106, 2470, 2460, 1420, 720, 650],
},
{
name: '使用人数',
type: 'bar',
data: [320, 302, 315, 335, 340, 356, 120, 85],
},
],
})
}
// 使
if (departmentUsageChart.value) {
const departmentChart = echarts.init(departmentUsageChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '部门使用分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: '技术部' },
{ value: 25, name: '市场部' },
{ value: 20, name: '销售部' },
{ value: 10, name: '人事部' },
{ value: 10, name: '财务部' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}
if (departmentUsageChart.value) {
const departmentChart = echarts.init(departmentUsageChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '部门使用分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: '技术部' },
{ value: 25, name: '市场部' },
{ value: 20, name: '销售部' },
{ value: 10, name: '人事部' },
{ value: 10, name: '财务部' },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
})
}
// 使
if (usageTimeChart.value) {
const timeChart = echarts.init(usageTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '使用时长占比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
if (usageTimeChart.value) {
const timeChart = echarts.init(usageTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '使用时长占比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
fontSize: '16',
fontWeight: 'bold'
}
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 20, name: '组织架构' },
{ value: 15, name: '资产管理' },
{ value: 15, name: '产品与服务' },
{ value: 20, name: '项目管理' },
{ value: 20, name: '施工操作台' },
{ value: 5, name: '聊天平台' },
{ value: 3, name: '企业设置' },
{ value: 2, name: '系统资源管理' },
],
},
labelLine: {
show: false
},
data: [
{ value: 20, name: '组织架构' },
{ value: 15, name: '资产管理' },
{ value: 15, name: '产品与服务' },
{ value: 20, name: '项目管理' },
{ value: 20, name: '施工操作台' },
{ value: 5, name: '聊天平台' },
{ value: 3, name: '企业设置' },
{ value: 2, name: '系统资源管理' }
]
}
]
})
}
],
})
}
//
window.addEventListener('resize', () => {
if (moduleUsageChart.value) {
const moduleChart = echarts.getInstanceByDom(moduleUsageChart.value)
moduleChart?.resize()
}
if (departmentUsageChart.value) {
const departmentChart = echarts.getInstanceByDom(departmentUsageChart.value)
departmentChart?.resize()
}
if (usageTimeChart.value) {
const timeChart = echarts.getInstanceByDom(usageTimeChart.value)
timeChart?.resize()
}
})
if (moduleUsageChart.value) {
const moduleChart = echarts.getInstanceByDom(moduleUsageChart.value)
moduleChart?.resize()
}
if (departmentUsageChart.value) {
const departmentChart = echarts.getInstanceByDom(departmentUsageChart.value)
departmentChart?.resize()
}
if (usageTimeChart.value) {
const timeChart = echarts.getInstanceByDom(usageTimeChart.value)
timeChart?.resize()
}
})
})
</script>
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
</style>
</style>

View File

@ -1,494 +1,494 @@
<template>
<div class="app-container">
<a-card class="general-card" title="成员活跃数据" :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="活跃度分析">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="部门活跃度对比" :bordered="false">
<div ref="departmentActivityChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="每日活跃用户数" :bordered="false">
<div ref="dailyActiveUsersChart" style="height: 300px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="平均在线时长" :bordered="false">
<div ref="onlineTimeChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="2" tab="成员排行榜">
<a-card title="本月活跃度排名" :bordered="false">
<a-table :columns="rankColumns" :data-source="rankData" :pagination="{ pageSize: 10 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'rank'">
<a-tag :color="getRankColor(record.rank)">{{ record.rank }}</a-tag>
</template>
<template v-if="column.dataIndex === 'activityScore'">
<a-progress :percent="record.activityScore" :stroke-color="getScoreColor(record.activityScore)" />
</template>
<div class="app-container">
<a-card class="general-card" title="成员活跃数据" :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="活跃度分析">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="部门活跃度对比" :bordered="false">
<div ref="departmentActivityChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="每日活跃用户数" :bordered="false">
<div ref="dailyActiveUsersChart" style="height: 300px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="平均在线时长" :bordered="false">
<div ref="onlineTimeChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="2" tab="成员排行榜">
<a-card title="本月活跃度排名" :bordered="false">
<a-table :columns="rankColumns" :data-source="rankData" :pagination="{ pageSize: 10 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'rank'">
<a-tag :color="getRankColor(record.rank)">{{ record.rank }}</a-tag>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" tab="考勤数据">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="部门考勤统计" :bordered="false">
<a-table :columns="attendanceColumns" :data-source="attendanceData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'attendanceRate'">
<a-progress :percent="record.attendanceRate" :stroke-color="getAttendanceColor(record.attendanceRate)" />
</template>
<template v-if="column.dataIndex === 'lateCount'">
<a-tag :color="getLateCountColor(record.lateCount)">{{ record.lateCount }}</a-tag>
</template>
<template v-if="column.dataIndex === 'activityScore'">
<a-progress :percent="record.activityScore" :stroke-color="getScoreColor(record.activityScore)" />
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" tab="考勤数据">
<a-row :gutter="16">
<a-col :span="24">
<a-card title="部门考勤统计" :bordered="false">
<a-table :columns="attendanceColumns" :data-source="attendanceData" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'attendanceRate'">
<a-progress :percent="record.attendanceRate" :stroke-color="getAttendanceColor(record.attendanceRate)" />
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="考勤趋势" :bordered="false">
<div ref="attendanceTrendChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
const departmentActivityChart = ref(null)
const dailyActiveUsersChart = ref(null)
const onlineTimeChart = ref(null)
const attendanceTrendChart = ref(null)
//
const rankColumns = [
{
title: '排名',
dataIndex: 'rank',
key: 'rank',
width: 80,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '部门',
dataIndex: 'department',
key: 'department',
},
{
title: '活跃度',
dataIndex: 'activityScore',
key: 'activityScore',
},
{
title: '登录次数',
dataIndex: 'loginCount',
key: 'loginCount',
},
{
title: '操作次数',
dataIndex: 'operationCount',
key: 'operationCount',
}
]
const rankData = [
{
key: '1',
rank: 1,
name: '张三',
department: '技术部',
activityScore: 98,
loginCount: 45,
operationCount: 532
},
{
key: '2',
rank: 2,
name: '李四',
department: '市场部',
activityScore: 95,
loginCount: 42,
operationCount: 498
},
{
key: '3',
rank: 3,
name: '王五',
department: '销售部',
activityScore: 92,
loginCount: 40,
operationCount: 475
},
{
key: '4',
rank: 4,
name: '赵六',
department: '人事部',
activityScore: 88,
loginCount: 38,
operationCount: 450
},
{
key: '5',
rank: 5,
name: '钱七',
department: '财务部',
activityScore: 85,
loginCount: 36,
operationCount: 420
},
{
key: '6',
rank: 6,
name: '孙八',
department: '技术部',
activityScore: 82,
loginCount: 34,
operationCount: 405
},
{
key: '7',
rank: 7,
name: '周九',
department: '市场部',
activityScore: 79,
loginCount: 32,
operationCount: 380
},
{
key: '8',
rank: 8,
name: '吴十',
department: '销售部',
activityScore: 76,
loginCount: 30,
operationCount: 365
},
{
key: '9',
rank: 9,
name: '郑十一',
department: '人事部',
activityScore: 73,
loginCount: 28,
operationCount: 350
},
{
key: '10',
rank: 10,
name: '王十二',
department: '财务部',
activityScore: 70,
loginCount: 26,
operationCount: 335
}
]
//
const attendanceColumns = [
{
title: '部门',
dataIndex: 'department',
key: 'department',
},
{
title: '人数',
dataIndex: 'memberCount',
key: 'memberCount',
},
{
title: '出勤率',
dataIndex: 'attendanceRate',
key: 'attendanceRate',
},
{
title: '迟到次数',
dataIndex: 'lateCount',
key: 'lateCount',
},
{
title: '早退次数',
dataIndex: 'earlyLeaveCount',
key: 'earlyLeaveCount',
},
{
title: '缺勤次数',
dataIndex: 'absentCount',
key: 'absentCount',
}
]
const attendanceData = [
{
key: '1',
department: '技术部',
memberCount: 45,
attendanceRate: 98,
lateCount: 3,
earlyLeaveCount: 1,
absentCount: 0
},
{
key: '2',
department: '市场部',
memberCount: 32,
attendanceRate: 96,
lateCount: 5,
earlyLeaveCount: 2,
absentCount: 1
},
{
key: '3',
department: '销售部',
memberCount: 38,
attendanceRate: 95,
lateCount: 6,
earlyLeaveCount: 3,
absentCount: 1
},
{
key: '4',
department: '人事部',
memberCount: 15,
attendanceRate: 97,
lateCount: 2,
earlyLeaveCount: 1,
absentCount: 0
},
{
key: '5',
department: '财务部',
memberCount: 12,
attendanceRate: 99,
lateCount: 1,
earlyLeaveCount: 0,
absentCount: 0
}
]
//
const getRankColor = (rank) => {
if (rank <= 3) return '#f50'
if (rank <= 10) return '#2db7f5'
return '#87d068'
}
const getScoreColor = (score) => {
if (score >= 90) return '#52c41a'
if (score >= 70) return '#1890ff'
return '#faad14'
}
const getAttendanceColor = (rate) => {
if (rate >= 95) return '#52c41a'
if (rate >= 90) return '#1890ff'
return '#faad14'
}
const getLateCountColor = (count) => {
if (count <= 2) return 'green'
if (count <= 5) return 'orange'
return 'red'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
<template v-if="column.dataIndex === 'lateCount'">
<a-tag :color="getLateCountColor(record.lateCount)">{{ record.lateCount }}</a-tag>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="考勤趋势" :bordered="false">
<div ref="attendanceTrendChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue'
import * as echarts from 'echarts'
const departmentActivityChart = ref(null)
const dailyActiveUsersChart = ref(null)
const onlineTimeChart = ref(null)
const attendanceTrendChart = ref(null)
//
const rankColumns = [
{
title: '排名',
dataIndex: 'rank',
key: 'rank',
width: 80,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '部门',
dataIndex: 'department',
key: 'department',
},
{
title: '活跃度',
dataIndex: 'activityScore',
key: 'activityScore',
},
{
title: '登录次数',
dataIndex: 'loginCount',
key: 'loginCount',
},
{
title: '操作次数',
dataIndex: 'operationCount',
key: 'operationCount',
},
]
const rankData = [
{
key: '1',
rank: 1,
name: '张三',
department: '技术部',
activityScore: 98,
loginCount: 45,
operationCount: 532,
},
{
key: '2',
rank: 2,
name: '李四',
department: '市场部',
activityScore: 95,
loginCount: 42,
operationCount: 498,
},
{
key: '3',
rank: 3,
name: '王五',
department: '销售部',
activityScore: 92,
loginCount: 40,
operationCount: 475,
},
{
key: '4',
rank: 4,
name: '赵六',
department: '人事部',
activityScore: 88,
loginCount: 38,
operationCount: 450,
},
{
key: '5',
rank: 5,
name: '钱七',
department: '财务部',
activityScore: 85,
loginCount: 36,
operationCount: 420,
},
{
key: '6',
rank: 6,
name: '孙八',
department: '技术部',
activityScore: 82,
loginCount: 34,
operationCount: 405,
},
{
key: '7',
rank: 7,
name: '周九',
department: '市场部',
activityScore: 79,
loginCount: 32,
operationCount: 380,
},
{
key: '8',
rank: 8,
name: '吴十',
department: '销售部',
activityScore: 76,
loginCount: 30,
operationCount: 365,
},
{
key: '9',
rank: 9,
name: '郑十一',
department: '人事部',
activityScore: 73,
loginCount: 28,
operationCount: 350,
},
{
key: '10',
rank: 10,
name: '王十二',
department: '财务部',
activityScore: 70,
loginCount: 26,
operationCount: 335,
},
]
//
const attendanceColumns = [
{
title: '部门',
dataIndex: 'department',
key: 'department',
},
{
title: '人数',
dataIndex: 'memberCount',
key: 'memberCount',
},
{
title: '出勤率',
dataIndex: 'attendanceRate',
key: 'attendanceRate',
},
{
title: '迟到次数',
dataIndex: 'lateCount',
key: 'lateCount',
},
{
title: '早退次数',
dataIndex: 'earlyLeaveCount',
key: 'earlyLeaveCount',
},
{
title: '缺勤次数',
dataIndex: 'absentCount',
key: 'absentCount',
},
]
const attendanceData = [
{
key: '1',
department: '技术部',
memberCount: 45,
attendanceRate: 98,
lateCount: 3,
earlyLeaveCount: 1,
absentCount: 0,
},
{
key: '2',
department: '市场部',
memberCount: 32,
attendanceRate: 96,
lateCount: 5,
earlyLeaveCount: 2,
absentCount: 1,
},
{
key: '3',
department: '销售部',
memberCount: 38,
attendanceRate: 95,
lateCount: 6,
earlyLeaveCount: 3,
absentCount: 1,
},
{
key: '4',
department: '人事部',
memberCount: 15,
attendanceRate: 97,
lateCount: 2,
earlyLeaveCount: 1,
absentCount: 0,
},
{
key: '5',
department: '财务部',
memberCount: 12,
attendanceRate: 99,
lateCount: 1,
earlyLeaveCount: 0,
absentCount: 0,
},
]
//
const getRankColor = (rank) => {
if (rank <= 3) return '#f50'
if (rank <= 10) return '#2db7f5'
return '#87d068'
}
const getScoreColor = (score) => {
if (score >= 90) return '#52c41a'
if (score >= 70) return '#1890ff'
return '#faad14'
}
const getAttendanceColor = (rate) => {
if (rate >= 95) return '#52c41a'
if (rate >= 90) return '#1890ff'
return '#faad14'
}
const getLateCountColor = (count) => {
if (count <= 2) return 'green'
if (count <= 5) return 'orange'
return 'red'
}
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
//
if (departmentActivityChart.value) {
const departmentChart = echarts.init(departmentActivityChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value'
},
series: [
{
name: '活跃度',
type: 'bar',
data: [92, 85, 88, 79, 82]
if (departmentActivityChart.value) {
const departmentChart = echarts.init(departmentActivityChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
{
name: '登录次数',
type: 'bar',
data: [320, 280, 310, 240, 260]
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
{
name: '操作次数',
type: 'bar',
data: [2800, 2100, 2400, 1800, 2000]
}
]
})
}
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '活跃度',
type: 'bar',
data: [92, 85, 88, 79, 82],
},
{
name: '登录次数',
type: 'bar',
data: [320, 280, 310, 240, 260],
},
{
name: '操作次数',
type: 'bar',
data: [2800, 2100, 2400, 1800, 2000],
},
],
})
}
//
if (dailyActiveUsersChart.value) {
const dailyActiveChart = echarts.init(dailyActiveUsersChart.value)
dailyActiveChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 132, 145, 135, 128, 68, 42],
type: 'line',
areaStyle: {}
}
]
})
}
if (dailyActiveUsersChart.value) {
const dailyActiveChart = echarts.init(dailyActiveUsersChart.value)
dailyActiveChart.setOption({
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [120, 132, 145, 135, 128, 68, 42],
type: 'line',
areaStyle: {},
},
],
})
}
// 线
if (onlineTimeChart.value) {
const onlineChart = echarts.init(onlineTimeChart.value)
onlineChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value} 小时'
}
},
series: [
{
name: '平均在线时长',
type: 'bar',
data: [7.5, 6.8, 7.2, 6.5, 6.9]
}
]
})
}
if (onlineTimeChart.value) {
const onlineChart = echarts.init(onlineTimeChart.value)
onlineChart.setOption({
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部'],
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value} 小时',
},
},
series: [
{
name: '平均在线时长',
type: 'bar',
data: [7.5, 6.8, 7.2, 6.5, 6.9],
},
],
})
}
//
if (attendanceTrendChart.value) {
const attendanceChart = echarts.init(attendanceTrendChart.value)
attendanceChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['出勤率', '迟到率', '早退率']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%'
}
},
series: [
{
name: '出勤率',
type: 'line',
data: [96.2, 97.1, 96.8, 97.5, 98.2, 97.8]
if (attendanceTrendChart.value) {
const attendanceChart = echarts.init(attendanceTrendChart.value)
attendanceChart.setOption({
tooltip: {
trigger: 'axis',
},
{
name: '迟到率',
type: 'line',
data: [2.8, 2.2, 2.5, 1.8, 1.2, 1.5]
legend: {
data: ['出勤率', '迟到率', '早退率'],
},
{
name: '早退率',
type: 'line',
data: [1.0, 0.7, 0.7, 0.7, 0.6, 0.7]
}
]
})
}
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%',
},
},
series: [
{
name: '出勤率',
type: 'line',
data: [96.2, 97.1, 96.8, 97.5, 98.2, 97.8],
},
{
name: '迟到率',
type: 'line',
data: [2.8, 2.2, 2.5, 1.8, 1.2, 1.5],
},
{
name: '早退率',
type: 'line',
data: [1.0, 0.7, 0.7, 0.7, 0.6, 0.7],
},
],
})
}
//
window.addEventListener('resize', () => {
if (departmentActivityChart.value) {
const departmentChart = echarts.getInstanceByDom(departmentActivityChart.value)
departmentChart?.resize()
}
if (dailyActiveUsersChart.value) {
const dailyActiveChart = echarts.getInstanceByDom(dailyActiveUsersChart.value)
dailyActiveChart?.resize()
}
if (onlineTimeChart.value) {
const onlineChart = echarts.getInstanceByDom(onlineTimeChart.value)
onlineChart?.resize()
}
if (attendanceTrendChart.value) {
const attendanceChart = echarts.getInstanceByDom(attendanceTrendChart.value)
attendanceChart?.resize()
}
})
if (departmentActivityChart.value) {
const departmentChart = echarts.getInstanceByDom(departmentActivityChart.value)
departmentChart?.resize()
}
if (dailyActiveUsersChart.value) {
const dailyActiveChart = echarts.getInstanceByDom(dailyActiveUsersChart.value)
dailyActiveChart?.resize()
}
if (onlineTimeChart.value) {
const onlineChart = echarts.getInstanceByDom(onlineTimeChart.value)
onlineChart?.resize()
}
if (attendanceTrendChart.value) {
const attendanceChart = echarts.getInstanceByDom(attendanceTrendChart.value)
attendanceChart?.resize()
}
})
})
</script>
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
</style>
</style>

View File

@ -1,267 +1,266 @@
<template>
<div class="app-container">
<a-card class="general-card" title="企业数据概览" :bordered="false">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="项目总数"
:value="statistics.projectCount"
:precision="0"
style="margin-right: 50px"
>
<template #prefix>
<icon-file />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="成员总数"
:value="statistics.memberCount"
:precision="0"
>
<template #prefix>
<icon-user-group />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="设备总数"
:value="statistics.deviceCount"
:precision="0"
>
<template #prefix>
<icon-computer />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="本月完成项目"
:value="statistics.completedProjectCount"
:precision="0"
>
<template #prefix>
<icon-check-circle />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="项目进度统计" :bordered="false">
<div ref="projectProgressChart" style="height: 300px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="资源使用情况" :bordered="false">
<div ref="resourceUsageChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="近6个月业务趋势" :bordered="false">
<div ref="businessTrendChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive, nextTick } from 'vue'
import { Statistic } from '@arco-design/web-vue'
import { IconFile, IconUserGroup, IconComputer, IconCheckCircle } from '@arco-design/web-vue/es/icon'
import * as echarts from 'echarts'
const projectProgressChart = ref(null)
const resourceUsageChart = ref(null)
const businessTrendChart = ref(null)
const statistics = reactive({
projectCount: 128,
memberCount: 356,
deviceCount: 243,
completedProjectCount: 15
})
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
<div class="app-container">
<a-card class="general-card" title="企业数据概览" :bordered="false">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="项目总数"
:value="statistics.projectCount"
:precision="0"
style="margin-right: 50px"
>
<template #prefix>
<IconFile />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="成员总数"
:value="statistics.memberCount"
:precision="0"
>
<template #prefix>
<IconUserGroup />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="设备总数"
:value="statistics.deviceCount"
:precision="0"
>
<template #prefix>
<IconComputer />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<a-statistic
title="本月完成项目"
:value="statistics.completedProjectCount"
:precision="0"
>
<template #prefix>
<IconCheckCircle />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="12">
<a-card title="项目进度统计" :bordered="false">
<div ref="projectProgressChart" style="height: 300px"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="资源使用情况" :bordered="false">
<div ref="resourceUsageChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-row :gutter="16">
<a-col :span="24">
<a-card title="近6个月业务趋势" :bordered="false">
<div ref="businessTrendChart" style="height: 300px"></div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref } from 'vue'
import { IconCheckCircle, IconComputer, IconFile, IconUserGroup } from '@arco-design/web-vue/es/icon'
import * as echarts from 'echarts'
const projectProgressChart = ref(null)
const resourceUsageChart = ref(null)
const businessTrendChart = ref(null)
const statistics = reactive({
projectCount: 128,
memberCount: 356,
deviceCount: 243,
completedProjectCount: 15,
})
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
//
if (projectProgressChart.value) {
const projectChart = echarts.init(projectProgressChart.value)
projectChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '项目状态',
type: 'pie',
radius: '70%',
data: [
{ value: 48, name: '进行中' },
{ value: 65, name: '已完成' },
{ value: 12, name: '已暂停' },
{ value: 3, name: '已取消' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}
if (projectProgressChart.value) {
const projectChart = echarts.init(projectProgressChart.value)
projectChart.setOption({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '项目状态',
type: 'pie',
radius: '70%',
data: [
{ value: 48, name: '进行中' },
{ value: 65, name: '已完成' },
{ value: 12, name: '已暂停' },
{ value: 3, name: '已取消' },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
})
}
// 使
if (resourceUsageChart.value) {
const resourceChart = echarts.init(resourceUsageChart.value)
resourceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['服务器', '存储空间', '带宽', '设备使用率', '人力资源']
},
series: [
{
name: '已使用',
type: 'bar',
stack: 'total',
label: {
show: true
if (resourceUsageChart.value) {
const resourceChart = echarts.init(resourceUsageChart.value)
resourceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
emphasis: {
focus: 'series'
},
data: [65, 72, 58, 80, 75]
},
{
name: '剩余',
type: 'bar',
stack: 'total',
label: {
show: true
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
data: ['服务器', '存储空间', '带宽', '设备使用率', '人力资源'],
},
series: [
{
name: '已使用',
type: 'bar',
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series',
},
data: [65, 72, 58, 80, 75],
},
emphasis: {
focus: 'series'
{
name: '剩余',
type: 'bar',
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series',
},
data: [35, 28, 42, 20, 25],
},
data: [35, 28, 42, 20, 25]
}
]
})
}
],
})
}
//
if (businessTrendChart.value) {
const businessChart = echarts.init(businessTrendChart.value)
businessChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['项目数量', '营业收入', '新增客户']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '项目数量',
type: 'line',
data: [10, 12, 15, 18, 22, 24]
if (businessTrendChart.value) {
const businessChart = echarts.init(businessTrendChart.value)
businessChart.setOption({
tooltip: {
trigger: 'axis',
},
{
name: '营业收入',
type: 'line',
data: [120, 132, 145, 160, 178, 190]
legend: {
data: ['项目数量', '营业收入', '新增客户'],
},
{
name: '新增客户',
type: 'line',
data: [5, 7, 8, 10, 12, 15]
}
]
})
}
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '项目数量',
type: 'line',
data: [10, 12, 15, 18, 22, 24],
},
{
name: '营业收入',
type: 'line',
data: [120, 132, 145, 160, 178, 190],
},
{
name: '新增客户',
type: 'line',
data: [5, 7, 8, 10, 12, 15],
},
],
})
}
//
window.addEventListener('resize', () => {
if (projectProgressChart.value) {
const projectChart = echarts.getInstanceByDom(projectProgressChart.value)
projectChart?.resize()
}
if (resourceUsageChart.value) {
const resourceChart = echarts.getInstanceByDom(resourceUsageChart.value)
resourceChart?.resize()
}
if (businessTrendChart.value) {
const businessChart = echarts.getInstanceByDom(businessTrendChart.value)
businessChart?.resize()
}
})
if (projectProgressChart.value) {
const projectChart = echarts.getInstanceByDom(projectProgressChart.value)
projectChart?.resize()
}
if (resourceUsageChart.value) {
const resourceChart = echarts.getInstanceByDom(resourceUsageChart.value)
resourceChart?.resize()
}
if (businessTrendChart.value) {
const businessChart = echarts.getInstanceByDom(businessTrendChart.value)
businessChart?.resize()
}
})
})
</script>
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
.stat-card {
margin-bottom: 20px;
text-align: center;
}
</style>
</style>

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