平台修改

This commit is contained in:
wxy 2025-07-14 11:11:33 +08:00
parent e6ad6ad6fb
commit ba041b3f3a
150 changed files with 29585 additions and 508 deletions

BIN
.DS_Store vendored

Binary file not shown.

21
.env.development Normal file
View File

@ -0,0 +1,21 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 接口前缀
VITE_API_PREFIX = '/dev-api'
# 接口地址
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'
# 地址前缀
VITE_BASE = '/'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
# 应用配置面板
VITE_APP_SETTING = true
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

17
.env.production Normal file
View File

@ -0,0 +1,17 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = false
# 接口地址
VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
# 地址前缀
VITE_BASE = '/'
# 应用配置面板
VITE_APP_SETTING = true
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

22
.env.test Normal file
View File

@ -0,0 +1,22 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = true
# 接口前缀
VITE_API_PREFIX = '/test-api'
# 接口地址
VITE_API_BASE_URL = 'http://localhost:8000'
# 地址前缀
VITE_BASE = '/test'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = true
# 应用配置面板
VITE_APP_SETTING = false
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/I3M-Web.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

10
.idea/misc.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenRunner">
<option name="jreName" value="1.8" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="corretto-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/I3M-Web.iml" filepath="$PROJECT_DIR$/.idea/I3M-Web.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

7
.idea/vcs.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Industrial-image-management-system---web" vcs="Git" />
</component>
</project>

95
AUTO_RECOGNITION_GUIDE.md Normal file
View File

@ -0,0 +1,95 @@
# 自动识别功能使用说明
## 功能概述
工业图像自动识别功能允许用户使用AI算法自动检测图像中的各种缺陷类型包括裂纹、腐蚀、磨损、变形等。
## 使用步骤
### 1. 准备工作
1. 在左侧项目管理树中选择项目、机组或部件
2. 在下方图像列表中选择一张需要识别的图像
3. 确保图像已在右上角预览区域显示
### 2. 启动自动识别
1. 点击顶部工具栏中的"自动标注"按钮
2. 界面会自动切换到识别模式:
- 左侧显示"自动识别设置"面板
- 右侧显示"识别结果"面板
### 3. 配置识别参数
在左侧设置面板中:
- **识别算法**: 选择YOLOv5、YOLOv8或R-CNN
- **置信度**: 调整识别置信度阈值0-100%
- **缺陷类型**: 选择要识别的缺陷类型
- 裂纹 (Crack)
- 划痕 (Scratch)
- 腐蚀 (Corrosion)
- 变形 (Deformation)
- 孔洞 (Hole)
- 污垢 (Dirt)
### 4. 开始识别
1. 配置完参数后,点击"开始识别"按钮
2. 系统会显示识别进度
3. 识别完成后,结果会在右侧面板显示
### 5. 查看识别结果
右侧识别结果面板显示:
- **统计信息**: 各类缺陷的数量和平均置信度
- **详细列表**: 每个缺陷的具体信息
- 缺陷类型和颜色标识
- 置信度百分比
- 位置坐标
- 尺寸大小
- 修复建议
### 6. 结果操作
- **选择结果**: 点击结果项可在图像上高亮显示
- **保存结果**: 将识别结果保存到系统
- **导出结果**: 导出识别结果为文件
### 7. 退出识别模式
点击左上角的关闭按钮(×)可退出识别模式,返回正常的项目管理界面。
## 缺陷类型说明
| 缺陷类型 | 描述 | 修复建议 |
|---------|------|----------|
| 裂纹 | 表面或内部的线状断裂 | 立即维修,防止扩散 |
| 腐蚀 | 金属表面的化学腐蚀 | 清洁并涂保护层 |
| 磨损 | 表面材料的磨损消失 | 定期监测,必要时更换 |
| 变形 | 结构形状的改变 | 检查结构完整性 |
| 划痕 | 表面的轻微划伤 | 轻微处理即可 |
| 孔洞 | 表面的洞穴或孔隙 | 立即修补 |
| 污垢 | 表面的污染物 | 清洁处理 |
## 注意事项
1. **图像质量**: 确保图像清晰度足够便于AI识别
2. **光照条件**: 良好的光照有助于提高识别准确度
3. **置信度设置**: 根据实际需要调整置信度阈值
4. **人工复核**: 置信度低于60%的结果建议人工复核
5. **数据保存**: 及时保存重要的识别结果
## 技术参数
- **支持格式**: JPG, PNG, BMP等常见图像格式
- **识别算法**: YOLOv5, YOLOv8, R-CNN
- **置信度范围**: 0-100%
- **处理时间**: 通常3-10秒根据图像大小
- **准确率**: 一般在85-95%之间
## 故障排除
### 常见问题
1. **无法启动识别**: 检查是否选择了图像
2. **识别结果为空**: 调整置信度阈值或更换图像
3. **识别时间过长**: 检查网络连接或联系技术支持
4. **结果不准确**: 调整算法参数或使用不同的识别算法
### 性能优化建议
- 使用适当的图像分辨率建议800x600以上
- 确保图像对比度清晰
- 避免过度曝光或过暗的图像
- 定期清理缓存数据

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,96 @@
import http from '@/utils/http'
const { request } = http
import type { AttachInfoData, BusinessTypeResult } from './type'
/**
*
* @param businessType
* @param files
*/
export function batchAddAttachment(businessType: string, formData: FormData) {
return request<AttachInfoData[]>({
url: `/attach-info/batch/${businessType}`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
* @param businessType
* @param formData
*/
export function addAttachment(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/model`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
* @param businessType
* @param formData
*/
export function addAttachmentByDefectMarkPic(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/defect_mark_pic`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
* @param businessType
* @param formData
*/
export function addAttachInsurance(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/insurance_file`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
*/
export function getAttachBusinessTypes() {
return request<BusinessTypeResult>({
url: '/common/list/attach-business_type',
method: 'get'
})
}
/**
*
* @param businessType
*/
export function getAttachmentList(businessType: string) {
return request<AttachInfoData[]>({
url: `/attach-info/list/${businessType}`,
method: 'get'
})
}
/**
*
* @param id ID
*/
export function deleteAttachment(id: string | number) {
return request<boolean>({
url: `/attach-info/${id}`,
method: 'delete'
})
}

View File

@ -0,0 +1,34 @@
/**
*
*/
export interface AttachInfoData {
id: string | number
fileName: string
fileUrl: string
fileType: string
fileSize: number
createTime: string
businessType: string
remark?: string
userDefinedPath?: string
}
/**
*
*/
export interface BusinessTypeResult {
code: number
data: BusinessType[]
msg: string
status: number
}
/**
*
*/
export interface BusinessType {
id: number
name: string
code: string
description?: string
}

View File

@ -45,3 +45,8 @@ export function listMenuType() {
export function uploadFile(data: FormData) {
return http.post(`${BASE_URL}/file`, data)
}
/** @desc 查询缺陷类型列表 */
export function listDefectType() {
return http.get<import('./type').DefectTypeResp[]>(`${BASE_URL}/list/defect-type`)
}

View File

@ -1,3 +1,4 @@
export * from './common'
export * from './captcha'
export * from './dashboard'
export * from './type'

View File

@ -58,3 +58,17 @@ export interface CheckBehaviorCaptchaResp {
repCode: string
repMsg: string
}
/** 缺陷类型响应类型 - 根据实际API返回结构 */
export interface DefectTypeResp {
[key: string]: string // 键是缺陷代码,值是缺陷名称
}
/** 缺陷类型选项类型 - 用于前端组件 */
export interface DefectTypeOption {
code: string;
label: string;
value: string;
name?: string; // 兼容性字段
sort?: number; // 兼容性字段
}

View File

@ -0,0 +1,77 @@
import http from '@/utils/http'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo } from './type'
const { request } = http
// 导出类型定义
export type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo }
// 新增人员资质
export function createCertification(data: CertificationInfo) {
return request({
url: '/certification',
method: 'post',
data
})
}
// 查询人员资质信息列表
export function getCertificationList(params: CertificationListParams) {
return request<CertificationListResponse>({
url: '/certification/list',
method: 'get',
params
})
}
// 查询人员资质详情
export function getCertificationDetail(certificationId: string) {
return request<CertificationInfo>({
url: `/certification/detail/${certificationId}`,
method: 'get'
})
}
// 修改人员资质信息
export function updateCertification(certificationId: string, data: CertificationInfo) {
return request({
url: `/certification/${certificationId}`,
method: 'put',
data
})
}
// 删除人员资质
export function deleteCertification(certificationId: string) {
return request({
url: `/certification/${certificationId}`,
method: 'delete'
})
}
// 批量删除人员资质
export function batchDeleteCertification(ids: string[]) {
return request({
url: '/certification/batch',
method: 'delete',
data: { ids }
})
}
// 导出人员资质
export function exportCertification(params: CertificationListParams) {
return request({
url: '/certification/export',
method: 'get',
params,
responseType: 'blob'
})
}
// 获取用户列表(用于下拉选择)
export function getUserList() {
return request<SimpleUserInfo[]>({
url: '/user/list',
method: 'get'
})
}

38
src/apis/employee/type.ts Normal file
View File

@ -0,0 +1,38 @@
/** 人员资质信息接口 */
export interface CertificationInfo {
certificationId?: string
certificationCode: string
certificationImage: string
certificationName: string
certificationType: string
userId: string
validityDateBegin: string
validityDateEnd: string
createTime?: string
updateTime?: string
}
/** 人员资质列表查询参数 */
export interface CertificationListParams {
certificationName?: string
certificationType?: string
userName?: string
current?: number
size?: number
}
/** 人员资质列表响应 */
export interface CertificationListResponse {
records: CertificationInfo[]
total: number
current: number
size: number
}
/** 用户信息简化版 */
export interface SimpleUserInfo {
userId: string
userName: string
account: string
name: string
}

View File

@ -0,0 +1,136 @@
import http from '@/utils/http'
const { request } = http
export interface HealthRecord {
id?: string
employeeName: string
employeeId: string
checkupDate: string
hospital: string
checkType: string
result: string
nextCheckupDate: string
summary?: string
suggestions?: string
remarks?: string
reportFiles?: any[]
}
export interface HealthRecordListParams {
employeeName?: string
employeeId?: string
hospital?: string
result?: string
checkupDateStart?: string
checkupDateEnd?: string
current?: number
size?: number
}
export interface HealthRecordListResponse {
records: HealthRecord[]
total: number
current: number
size: number
}
// 新增健康记录
export function createHealthRecord(data: HealthRecord) {
return request({
url: '/health-record',
method: 'post',
data
})
}
// 查询健康记录列表
export function getHealthRecordList(params: HealthRecordListParams) {
return request<HealthRecordListResponse>({
url: '/health-record/list',
method: 'get',
params
})
}
// 查询健康记录详情
export function getHealthRecordDetail(id: string) {
return request<HealthRecord>({
url: `/health-record/detail/${id}`,
method: 'get'
})
}
// 编辑健康记录
export function updateHealthRecord(id: string, data: HealthRecord) {
return request({
url: `/health-record/${id}`,
method: 'put',
data
})
}
// 删除健康记录
export function deleteHealthRecord(id: string) {
return request({
url: `/health-record/${id}`,
method: 'delete'
})
}
// 上传体检报告
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'
}
})
}
// 下载体检报告
export function downloadHealthReport(fileId: string) {
return request({
url: `/health-record/download-report/${fileId}`,
method: 'get',
responseType: 'blob'
})
}
// 获取员工健康记录历史
export function getEmployeeHealthHistory(employeeId: string) {
return request<HealthRecord[]>({
url: `/health-record/employee/${employeeId}`,
method: 'get'
})
}
// 导出健康记录
export function exportHealthRecords(params: HealthRecordListParams) {
return request({
url: '/health-record/export',
method: 'get',
params,
responseType: 'blob'
})
}
// 安排体检
export function scheduleHealthCheck(data: {
employeeIds: string[]
checkupDate: string
hospital: string
checkType: string
}) {
return request({
url: '/health-record/schedule',
method: 'post',
data
})
}

View File

@ -7,6 +7,15 @@ export * from './code'
export * from './schedule'
export * from './project'
export * from './project/task'
export * from './attach-info'
export * from './model-config'
// 保险相关模块
export * as InsuranceAPI from './insurance'
export * as InsuranceCompanyAPI from './insurance-company'
export * as InsuranceTypeAPI from './insurance-type'
export * as HealthRecordAPI from './health-record'
export * as InsuranceFileAPI from './insurance-file'
export * as EmployeeAPI from './employee'
export * from './area/type'
export * from './auth/type'
@ -16,3 +25,5 @@ export * from './system/type'
export * from './code/type'
export * from './schedule/type'
export * from './project/type'
export * from './attach-info/type'
export * from './model-config/type'

View File

@ -0,0 +1,239 @@
import http from '@/utils/http'
// 缺陷检测请求参数
export interface DefectDetectionRequest {
confThreshold: number
defectTypeList: string[]
imageId: string
modelId: string
}
// 手动添加缺陷请求参数
export interface ManualDefectAddRequest {
attachId: string
axial: number
chordwise: number
defectCode: string
defectLevel: string
defectName: string
defectPosition: string
defectType: string
description: string
detectionDate: string
labelInfo: string
markInfo: {
bbox: number[]
clsId: number
confidence: number
label: string
}
repairIdea: string
repairStatus: string
source: string
}
// 缺陷检测结果
export interface DefectDetectionResult {
attachId: string
attachPath: string
axial: number
chordwise: number
defectCode: string
defectId: string
defectLevel: string
defectLevelLabel: string
defectName: string
defectPosition: string
defectType: string
defectTypeLabel: string
description: string
detectionDate: string
imageId: string
labelInfo: string
markInfo: {
bbox: number[]
clsId: number
confidence: number
label: string
}
repairIdea: string
repairStatus: string
repairStatusLabel: string
source: string
sourceLabel: string
}
// API响应结构
export interface DefectDetectionResponse {
code: number
data: DefectDetectionResult[]
msg: string
status: number
success: boolean
}
// 手动添加缺陷响应结构
export interface ManualDefectAddResponse {
code: number
data: DefectDetectionResult
msg: string
status: number
success: boolean
}
/** @desc 单图自动标注缺陷 */
export const detectDefects = (params: DefectDetectionRequest) => {
return http.post<DefectDetectionResponse>('/defect/detect', params)
}
/** @desc 手动添加缺陷记录 */
export const addManualDefect = (params: ManualDefectAddRequest,imageId:string) => {
return http.post<ManualDefectAddResponse>(`/defect/${imageId}`, params)
}
// 缺陷相关的其他接口
// 缺陷列表查询参数接口
export interface DefectListParams {
defectId?: string;
defectLevel?: string;
defectType?: string;
keyword?: string;
turbineId?: string;
imageId?: string; // 添加imageId参数用于按图像筛选缺陷
}
/** @desc 获取缺陷列表 */
export const getDefectList = (params: DefectListParams) => {
return http.get<{
code: number
data: DefectDetectionResult[]
msg: string
status: number
success: boolean
}>('/defect/list', params )
}
/** @desc 添加缺陷 */
export const addDefect = (params: Partial<DefectDetectionResult>) => {
return http.post<{
code: number
data: DefectDetectionResult
msg: string
status: number
success: boolean
}>('/defect/add', params)
}
/** @desc 更新缺陷 */
export const updateDefect = (defectId: string, params: Partial<DefectDetectionResult>) => {
return http.put<{
code: number
data: DefectDetectionResult
msg: string
status: number
success: boolean
}>(`/defect/${defectId}`, params)
}
/** @desc 删除缺陷 */
export const deleteDefect = (defectId: string) => {
return http.del<{
code: number
msg: string
status: number
success: boolean
}>(`/defect/${defectId}`)
}
/** @desc 上传标注后的图片作为附件 */
export const uploadAnnotatedImage = (imageBlob: Blob, fileName: string) => {
const formData = new FormData()
formData.append('file', imageBlob, fileName)
return http.post<{
code: number
data: AttachInfoData
msg: string
status: number
success: boolean
}>('/attach-info/defect_mark_pic', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 附件信息数据类型
export interface AttachInfoData {
attachId: string
attachPath: string
fileName: string
fileSize: number
contentType: string
uploadTime: string
[key: string]: any
}
// 缺陷信息接口
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;
markInfo?: {
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;
}
// 缺陷类型
export interface DefectType {
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;
}>('/common/list/defect-level')
}
// 获取缺陷类型列表
export const getDefectTypes = () => {
return http.get<{
code: number;
data: DefectType[];
msg: string;
status: number;
success: boolean;
}>('/common/list/defect-type')
}

View File

@ -0,0 +1,341 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
export * from './defect'
/** @desc 获取项目列表(分页) */
export const getProjectList = (params: T.ProjectQuery) => {
return http.get<T.ProjectInfo[] | T.PageResult<T.ProjectInfo>>('/project/list', params)
}
/** @desc 获取项目详情 */
export const getProjectDetail = (projectId: string) => {
return http.get<T.ProjectInfo>(`/project/detail/${projectId}`)
}
/** @desc 获取机组列表 */
export const getTurbineList = (params?: T.TurbineQuery) => {
return http.get<T.TurbineInfo[]>('/turbine/list', params)
}
/** @desc 获取机组详情 */
export const getTurbineDetail = (turbineId: string) => {
return http.get<T.TurbineInfo>(`/turbine/info/${turbineId}`)
}
/** @desc 获取部件列表 */
export const getPartList = (params?: T.PartQuery) => {
return http.get<T.PartInfo[]>('/part/list', params)
}
/** @desc 获取部件详情 */
export const getPartDetail = (partId: string) => {
return http.get<T.PartInfo>(`/part/detail/${partId}`)
}
/** @desc 获取项目树 */
export const getProjectTree = () => {
return http.get<T.ProjectTreeNode[]>(`/industrial-image/project-tree`)
}
/** @desc 获取图像列表 */
export const getImageList = (params?: {
imageTypes?: string[]
keyword?: string
partId?: string
turbineId?: string
}) => {
return http.get<{
code: number
data: Array<{
imageId: string
imageName: string
audioList?: any[]
cameraManufacturer?: string
cameraModel?: string
collectorName?: string
focalDistance?: string
gps?: string
humidness?: number
imageResolution?: string
imageType?: string
imageTypeLabel?: string
partId?: string
partName?: string
shootingDistance?: number
shootingMethod?: string
shootingMethodLabel?: string
shootingTime?: string
temperature?: string
weather?: string
weatherLabel?: string
windLevel?: number
filePath?: string
imagePath?: string
}>
msg: string
status: number
success: boolean
}>('/image/list', params)
}
/** @desc 获取图像详情 */
export function getImageDetail(imageId: string) {
return http.get<T.IndustrialImage>(`/industrial-image/image/${imageId}`)
}
/** @desc 搜索图像 */
export function searchImages(query: T.ImageQuery) {
return http.get<T.IndustrialImage[]>(`/industrial-image/images/search`, query)
}
/** @desc 查询通用图片来源 */
export const getImageSources = () => {
return http.get<Array<{
code: number
data: Array<{
id: string
name: string
code: string
}>
msg: string
status: number
}>>('/common/list/common-image-source')
}
/** @desc 上传单张图片 */
export const uploadSingleImage = (imageSource: string, file: File, params?: {
altitude?: string
latitude?: string
longitude?: string
partId?: string
uploadUser?: string
}) => {
const formData = new FormData()
formData.append('file', file)
// 构建查询参数
const queryParams = new URLSearchParams()
if (params?.altitude) queryParams.append('altitude', params.altitude)
if (params?.latitude) queryParams.append('latitude', params.latitude)
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() : ''}`
return http.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/** @desc 批量上传图片 */
export const batchUploadImages = (imageSource: string, files: File[], params?: {
altitude?: string
latitude?: string
longitude?: string
partId?: string
uploadUser?: string
}) => {
const formData = new FormData()
// 添加文件
files.forEach(file => {
formData.append('files', file)
})
// 构建查询参数
const queryParams = new URLSearchParams()
if (params?.altitude) queryParams.append('altitude', params.altitude)
if (params?.latitude) queryParams.append('latitude', params.latitude)
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() : ''}`
return http.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/** @desc 单图自动标注缺陷 */
export const detectDefects = (params: {
confThreshold: number
defectTypeList: string[]
imageId: string
modelId: string
}) => {
return http.post('/defect/detect', params, {
headers: {
'Content-Type': 'application/json'
}
})
}
/** @desc 图像导入接口使用新的API规范 */
export const uploadImageToPartV2 = (
imageSource: string,
partId: string,
files: File[],
params: Partial<T.ImageUploadParams>
) => {
const formData = new FormData()
// 添加文件
files.forEach(file => {
formData.append('files', file)
})
// 添加其他参数
if (params.collectorId) formData.append('collectorId', params.collectorId)
if (params.collectorName) formData.append('collectorName', params.collectorName)
if (params.humidness !== undefined) formData.append('humidness', params.humidness.toString())
if (params.gps) formData.append('imageList[0].GPS', params.gps)
if (params.xResolution) formData.append('imageList[0].XResolution', params.xResolution)
if (params.yResolution) formData.append('imageList[0].YResolution', params.yResolution)
if (params.altitude) formData.append('imageList[0].altitude', params.altitude)
if (params.cameraManufacturer) formData.append('imageList[0].cameraManufacturer', params.cameraManufacturer)
if (params.cameraModel) formData.append('imageList[0].cameraModel', params.cameraModel)
if (params.focalDistance) formData.append('imageList[0].focalDistance', params.focalDistance)
if (params.focalDistance35) formData.append('imageList[0].focalDistance35', params.focalDistance35)
if (params.imageHeight) formData.append('imageList[0].imageHeight', params.imageHeight)
if (params.imageId) formData.append('imageList[0].imageId', params.imageId)
if (params.imageName) formData.append('imageList[0].imageName', params.imageName)
if (params.imagePath) formData.append('imageList[0].imagePath', params.imagePath)
if (params.imageResolution) formData.append('imageList[0].imageResolution', params.imageResolution)
if (params.imageSize) formData.append('imageList[0].imageSize', params.imageSize)
if (params.imageWidth) formData.append('imageList[0].imageWidth', params.imageWidth)
if (params.latitude) formData.append('imageList[0].latitude', params.latitude)
if (params.longitude) formData.append('imageList[0].longitude', params.longitude)
if (params.resolutionUnits) formData.append('imageList[0].resolutionUnits', params.resolutionUnits)
if (params.shootingTime) formData.append('imageList[0].shootingTime', params.shootingTime)
if (params.shootingDistance !== undefined) formData.append('shootingDistance', params.shootingDistance.toString())
if (params.shootingMethod) formData.append('shootingMethod', params.shootingMethod)
if (params.shootingTimeBegin) formData.append('shootingTimeBegin', params.shootingTimeBegin)
if (params.shootingTimeEnd) formData.append('shootingTimeEnd', params.shootingTimeEnd)
if (params.temperatureMax !== undefined) formData.append('temperatureMax', params.temperatureMax.toString())
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'
}
})
}
/** @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 => {
// 如果需要自动标注
if (params.autoAnnotate && params.annotationTypes && params.annotationTypes.length > 0) {
// 这里可以添加自动标注逻辑
return response
}
return response
})
}
/** @desc 自动标注接口(更新为使用真实接口) */
export const autoAnnotateImage = (params: T.AutoAnnotationParams) => {
return detectDefects({
confThreshold: params.confidenceThreshold || 0.5,
defectTypeList: params.annotationTypes,
imageId: params.imageId,
modelId: params.params?.modelId || 'default'
})
}
/** @desc 获取图像标注信息 */
export const getImageAnnotations = (imageId: string) => {
return http.get<T.AnnotationResult>(`/industrial-image/annotations/${imageId}`)
}
/** @desc 删除标注 */
export const deleteAnnotation = (imageId: string, annotationId: string) => {
return http.del(`/industrial-image/annotations/${imageId}/${annotationId}`)
}
/** @desc 确认标注 */
export const confirmAnnotation = (imageId: string, annotationId: string) => {
return http.post(`/industrial-image/annotations/${imageId}/${annotationId}/confirm`)
}
/** @desc 上传图像(保留旧接口兼容性) */
export const uploadImage = (file: File, params: { projectId: string; componentId?: string }) => {
return uploadSingleImage('default', file, {
partId: params.componentId,
uploadUser: 'current-user'
})
}
/** @desc 删除图像 */
export const deleteImage = (imageId: string) => {
return http.del(`/industrial-image/images/${imageId}`)
}
/** @desc 批量删除图像 */
export function batchDeleteImages(imageIds: string[]) {
return http.del(`/industrial-image/images`, { ids: imageIds })
}
/** @desc 开始图像处理 */
export const processImage = (params: T.ImageProcessParams) => {
return http.post<T.ImageProcessResult>(`/industrial-image/process`, params)
}
/** @desc 获取处理结果 */
export const getProcessResult = (processId: string) => {
return http.get<T.ImageProcessResult>(`/industrial-image/process/${processId}`)
}
/** @desc 获取图像的所有处理结果 */
export function getImageProcessResults(imageId: string) {
return http.get<T.ImageProcessResult[]>(`/industrial-image/image/${imageId}/results`)
}
/** @desc 取消图像处理 */
export function cancelImageProcess(processId: string) {
return http.post(`/industrial-image/process/${processId}/cancel`)
}
/** @desc 重新处理图像 */
export function reprocessImage(params: T.ImageProcessParams) {
return http.post<T.ImageProcessResult>(`/industrial-image/process/reprocess`, params)
}
/** @desc 批量处理 */
export const batchProcessImages = (imageIds: string[], processType: string) => {
return http.post<T.ImageProcessResult[]>(`/industrial-image/batch-process`, {
imageIds,
processType
})
}
/** @desc 导出处理结果 */
export function exportProcessResults(query: T.ImageQuery) {
return http.get(`/industrial-image/export/results`, query, {
responseType: 'blob'
})
}
/** @desc 生成检测报告 */
export function generateReport(projectId: string) {
return http.post(`/industrial-image/report/generate`, { projectId }, {
responseType: 'blob'
})
}

View File

@ -0,0 +1,415 @@
/** 工业图像处理相关类型定义 */
/** 项目实体接口定义 */
export interface ProjectInfo {
projectId: string // 项目id
projectName: string // 项目名称
scale?: string // 项目规模
turbineModel?: string // 风机型号
status?: number // 状态0待施工1施工中2已完工3已验收4已验收
statusLabel?: string // 项目状态文本
coverUrl?: string // 项目封面
client?: string // 委托单位
clientContact?: string // 委托单位联系人
clientPhone?: string // 委托单位联系电话
farmName?: string // 风场名称
farmAddress?: string // 风场地址
inspectionUnit?: string // 检查单位
inspectionContact?: string // 检查单位联系人
inspectionPhone?: string // 检查单位联系电话
projectManagerId?: string // 项目经理id
projectManagerName?: string // 项目经理名称
qualityOfficerId?: string // 质量员id
auditorId?: string // 安全员id
constructionPersonnel?: string // 施工人员
constructionPersonnelId?: string // 施工人员id
constructionTeamLeaderId?: string // 施工组长id
constructorIds?: string // 施工人员id
constructorName?: string // 施工人员名称
workType?: string // 项目工作类型 可能有多项,逗号分隔
job?: string // 项目工作内容 可能有多项json对象保存
discloseContent?: string // 交底内容
technicalContent?: string // 技术方案内容
safetyContent?: string // 安全措施内容
createBy?: string // 创建者
createTime?: string // 创建时间
updateBy?: string // 更新者
idList?: string[] // id集合
}
/** 机组信息接口 */
export interface TurbineInfo {
turbineId?: string // 机组ID
projectId: string // 项目ID
turbineName: string // 机组名称
turbineDesc?: string // 机组描述
turbineManufacturer?: string // 机组厂商
turbineModel?: string // 机组型号
power?: string // 功率
status?: number // 状态
createTime?: string // 创建时间
updateTime?: string // 更新时间
parts?: PartInfo[] // 部件列表
}
/** 部件信息接口 */
export interface PartInfo {
partId: string // 部件ID
partCode?: string // 部件编码
partDesc?: string // 部件描述
partManufacturer?: string // 部件厂商
partModel?: string // 部件型号
partName: string // 部件名称
partType: string // 部件类型
turbineId: string // 机组ID
createTime?: string // 创建时间
updateTime?: string // 更新时间
}
/** 项目查询参数 */
export interface ProjectQuery {
projectName?: string
status?: number
page?: number
pageSize?: number
}
/** 机组查询参数 */
export interface TurbineQuery {
projectId?: string
turbineName?: string
turbineDesc?: string
turbineManufacturer?: string
turbineModel?: string
}
/** 部件查询参数 */
export interface PartQuery {
turbineId?: string
partName?: string
partType?: string
partManufacturer?: string
partModel?: string
}
/** 分页结果接口 */
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
/** 项目节点类型 */
export interface ProjectTreeNode {
id: string
name: string
type: 'project' | 'turbine' | 'part'
parentId?: string
children?: ProjectTreeNode[]
/** 图像数量 */
imageCount?: number
/** 状态 */
status?: string | number
/** 创建时间 */
createTime?: string
/** 是否为叶子节点 */
isLeaf?: boolean
/** 是否已加载子节点 */
loaded?: boolean
/** 是否展开 */
expanded?: boolean
/** 原始数据 */
rawData?: ProjectInfo | TurbineInfo | PartInfo
}
/** 缺陷标注信息 */
export interface DefectAnnotation {
id: string
/** 缺陷类型 */
type: string
/** 缺陷名称 */
name: string
/** 置信度 */
confidence: number
/** 边界框 [x, y, width, height] */
bbox: [number, number, number, number]
/** 缺陷描述 */
description?: string
/** 标注方式 */
annotationType: 'auto' | 'manual'
/** 标注人员 */
annotator?: string
/** 标注时间 */
annotationTime?: string
/** 是否已确认 */
confirmed?: boolean
}
/** 工业图像信息 */
export interface IndustrialImage {
id: string
/** 图像名称 */
name: string
/** 图像路径 */
path: string
/** 图像路径API返回字段*/
imagePath?: string
/** 缩略图路径 */
thumbnailPath?: string
/** 图像类型 */
type: string
/** 文件大小 */
size: number
/** 图像宽度 */
width?: number
/** 图像高度 */
height?: number
/** 所属项目ID */
projectId: string
/** 所属组件ID */
componentId?: string
/** 拍摄时间 */
captureTime?: string
/** 处理状态 */
processStatus?: 'pending' | 'processing' | 'completed' | 'failed'
/** 缺陷数量 */
defectCount?: number
/** 缺陷标注列表 */
annotations?: DefectAnnotation[]
/** 是否已标注 */
isAnnotated?: boolean
/** 标注状态 */
annotationStatus?: 'none' | 'auto' | 'manual' | 'verified'
/** 创建时间 */
createTime?: string
/** 更新时间 */
updateTime?: string
// 扩展字段 - 来自真实API
/** 部件名称 */
partName?: string
/** 相机制造商 */
cameraManufacturer?: string
/** 相机型号 */
cameraModel?: string
/** 采集员姓名 */
collectorName?: string
/** 焦距 */
focalDistance?: string
/** GPS坐标 */
gps?: string
/** 湿度 */
humidness?: number
/** 拍摄距离 */
shootingDistance?: number
/** 拍摄方式 */
shootingMethod?: string
/** 拍摄方式标签 */
shootingMethodLabel?: string
/** 温度 */
temperature?: string
/** 天气 */
weather?: string
/** 天气标签 */
weatherLabel?: string
/** 风力等级 */
windLevel?: number
}
/** 图像查询参数 */
export interface ImageQuery {
/** 项目ID */
projectId?: string
/** 组件ID */
componentId?: string
/** 图像名称搜索 */
keyword?: string
/** 图像类型 */
type?: string
/** 处理状态 */
processStatus?: string
/** 标注状态 */
annotationStatus?: string
/** 页码 */
page?: number
/** 每页数量 */
size?: number
}
/** 图像导入参数 */
export interface ImageImportParams {
/** 项目ID */
projectId: string
/** 组件ID */
componentId?: string
/** 图像来源 */
imageSource?: string
/** 上传用户 */
uploadUser?: string
/** 海拔 */
altitude?: string
/** 纬度 */
latitude?: string
/** 经度 */
longitude?: string
/** 部件ID */
partId?: string
/** 是否自动标注 */
autoAnnotate?: boolean
/** 标注类型 */
annotationTypes?: string[]
}
/** 图像上传参数基于新API */
export interface ImageUploadParams {
/** 图像源 */
imageSource: string
/** 部件ID */
partId: string
/** 采集员ID */
collectorId?: string
/** 采集员姓名 */
collectorName?: string
/** 湿度(百分比) */
humidness?: number
/** GPS坐标 */
gps?: string
/** X分辨率 */
xResolution?: string
/** Y分辨率 */
yResolution?: string
/** 海拔 */
altitude?: string
/** 相机制造商 */
cameraManufacturer?: string
/** 相机型号 */
cameraModel?: string
/** 焦距 */
focalDistance?: string
/** 35毫米焦距 */
focalDistance35?: string
/** 图像高度 */
imageHeight?: string
/** 图像ID */
imageId?: string
/** 图像名称 */
imageName?: string
/** 图像路径 */
imagePath?: string
/** 图像分辨率 */
imageResolution?: string
/** 图像大小 */
imageSize?: string
/** 图像宽度 */
imageWidth?: string
/** 纬度 */
latitude?: string
/** 经度 */
longitude?: string
/** 焦平面分辨率单位 */
resolutionUnits?: string
/** 拍摄时间 */
shootingTime?: string
/** 拍摄距离 */
shootingDistance?: number
/** 拍摄方式 */
shootingMethod?: string
/** 拍摄时间开始 */
shootingTimeBegin?: string
/** 拍摄时间结束 */
shootingTimeEnd?: string
/** 温度最高 */
temperatureMax?: number
/** 温度最低 */
temperatureMin?: number
/** 天气 */
weather?: string
/** 风力等级 */
windLevel?: number
}
/** 自动标注参数 */
export interface AutoAnnotationParams {
/** 图像ID */
imageId: string
/** 标注类型 */
annotationTypes: string[]
/** 置信度阈值 */
confidenceThreshold?: number
/** 其他参数 */
params?: Record<string, any>
}
/** 手动标注参数 */
export interface ManualAnnotationParams {
/** 图像ID */
imageId: string
/** 标注列表 */
annotations: Omit<DefectAnnotation, 'id' | 'annotationTime'>[]
}
/** 标注结果 */
export interface AnnotationResult {
/** 图像ID */
imageId: string
/** 标注列表 */
annotations: DefectAnnotation[]
/** 标注数量 */
annotationCount: number
/** 处理时间 */
processTime?: number
/** 标注状态 */
status: 'success' | 'failed'
/** 错误信息 */
error?: string
}
/** 图像处理参数 */
export interface ImageProcessParams {
/** 图像ID */
imageId: string
/** 处理类型 */
processType: 'defect_detection' | 'quality_analysis' | 'measurement'
/** 处理参数 */
params?: Record<string, any>
}
/** 图像处理结果 */
export interface ImageProcessResult {
/** 处理ID */
processId: string
/** 图像ID */
imageId: string
/** 处理类型 */
processType: string
/** 处理状态 */
status: 'pending' | 'processing' | 'completed' | 'failed'
/** 处理结果 */
result?: {
/** 检测到的缺陷 */
defects?: Array<{
id: string
type: string
confidence: number
bbox: [number, number, number, number]
description?: string
}>
/** 质量分析结果 */
qualityScore?: number
/** 测量结果 */
measurements?: Array<{
type: string
value: number
unit: string
}>
}
/** 处理耗时(毫秒) */
processTime?: number
/** 错误信息 */
error?: string
/** 创建时间 */
createTime?: string
/** 完成时间 */
completeTime?: string
}

View File

@ -0,0 +1,91 @@
import http from '@/utils/http'
const { request } = http
export interface InsuranceCompany {
id?: string
contact: string
contactPhone: string
insuranceCompanyName: string
status: string
email?: string
address?: string
startDate?: string
}
export interface InsuranceCompanyListParams {
contact?: string
contactPhone?: string
insuranceCompanyName?: string
status?: string
current?: number
size?: number
}
export interface InsuranceCompanyListResponse {
records: InsuranceCompany[]
total: number
current: number
size: number
}
// 新增保险公司信息
export function createInsuranceCompany(data: InsuranceCompany) {
return request({
url: '/insurance-company',
method: 'post',
data
})
}
// 查询保险公司信息列表
export function getInsuranceCompanyList(params: InsuranceCompanyListParams) {
return request<InsuranceCompanyListResponse>({
url: '/insurance-company/list',
method: 'get',
params
})
}
// 查询保险公司详情
export function getInsuranceCompanyDetail(id: string) {
return request<InsuranceCompany>({
url: `/insurance-company/detail/${id}`,
method: 'get'
})
}
// 编辑保险公司信息
export function updateInsuranceCompany(id: string, data: InsuranceCompany) {
return request({
url: `/insurance-company/${id}`,
method: 'put',
data
})
}
// 删除保险公司
export function deleteInsuranceCompany(id: string) {
return request({
url: `/insurance-company/${id}`,
method: 'delete'
})
}
// 终止合作
export function terminateCooperation(id: string) {
return request({
url: `/insurance-company/terminate/${id}`,
method: 'post'
})
}
// 恢复合作
export function resumeCooperation(id: string) {
return request({
url: `/insurance-company/resume/${id}`,
method: 'post'
})
}
// 获取所有有效保险公司(用于下拉选择)

View File

@ -0,0 +1,168 @@
import http from '@/utils/http'
const { request } = http
export interface InsuranceFile {
id?: string
employeeName: string
employeeId: string
fileName: string
fileType: string
fileSize?: number
filePath?: string
uploadDate?: string
description?: string
remarks?: string
}
export interface InsuranceFileListParams {
employeeName?: string
employeeId?: string
fileType?: string
uploadDateStart?: string
uploadDateEnd?: string
current?: number
size?: number
}
export interface InsuranceFileListResponse {
records: InsuranceFile[]
total: number
current: number
size: number
}
export interface UploadInsuranceFileParams {
employeeId: string
fileType: string
description?: string
file: File
}
// 上传保单文件
export function uploadInsuranceFile(data: UploadInsuranceFileParams) {
const formData = new FormData()
formData.append('file', data.file)
formData.append('employeeId', data.employeeId)
formData.append('fileType', data.fileType)
if (data.description) {
formData.append('description', data.description)
}
return request({
url: '/insurance-file/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 查询保单文件列表
export function getInsuranceFileList(params: InsuranceFileListParams) {
return request<InsuranceFileListResponse>({
url: '/insurance-file/list',
method: 'get',
params
})
}
// 查询文件详情
export function getInsuranceFileDetail(id: string) {
return request<InsuranceFile>({
url: `/insurance-file/detail/${id}`,
method: 'get'
})
}
// 更新文件信息
export function updateInsuranceFile(id: string, data: Partial<InsuranceFile>) {
return request({
url: `/insurance-file/${id}`,
method: 'put',
data
})
}
// 删除文件
export function deleteInsuranceFile(id: string) {
return request({
url: `/insurance-file/${id}`,
method: 'delete'
})
}
// 批量删除文件
export function batchDeleteInsuranceFiles(ids: string[]) {
return request({
url: '/insurance-file/batch',
method: 'delete',
data: { ids }
})
}
// 下载文件
export function downloadInsuranceFile(id: string) {
return request({
url: `/insurance-file/download/${id}`,
method: 'get',
responseType: 'blob'
})
}
// 预览文件
export function previewInsuranceFile(id: string) {
return request({
url: `/insurance-file/preview/${id}`,
method: 'get',
responseType: 'blob'
})
}
// 获取员工所有文件
export function getEmployeeFiles(employeeId: string) {
return request<InsuranceFile[]>({
url: `/insurance-file/employee/${employeeId}`,
method: 'get'
})
}
// 按文件类型统计
export function getInsuranceFileStatistics() {
return request<{
fileType: string
count: number
totalSize: number
}[]>({
url: '/insurance-file/statistics',
method: 'get'
})
}
// 批量上传文件
export function batchUploadFiles(data: {
files: File[]
employeeId: string
fileType: string
description?: string
}) {
const formData = new FormData()
data.files.forEach((file, index) => {
formData.append(`files[${index}]`, file)
})
formData.append('employeeId', data.employeeId)
formData.append('fileType', data.fileType)
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'
}
})
}

View File

@ -0,0 +1,76 @@
import http from '@/utils/http'
const { request } = http
export interface InsuranceType {
id?: string
description: string
insuranceTypeName: string
coverage?: string
}
export interface InsuranceTypeListParams {
insuranceTypeName?: string
description?: string
current?: number
size?: number
}
export interface InsuranceTypeListResponse {
records: InsuranceType[]
total: number
current: number
size: number
}
// 新增保险类型
export function createInsuranceType(data: InsuranceType) {
return request({
url: '/insurance-type',
method: 'post',
data
})
}
// 查询保险类型列表
export function getInsuranceTypeList(params?: InsuranceTypeListParams) {
return request<InsuranceTypeListResponse>({
url: '/insurance-type/list',
method: 'get',
params
})
}
// 查询保险类型详情
export function getInsuranceTypeDetail(insuranceTypeId: string) {
return request<InsuranceType>({
url: `/insurance-type/detail/${insuranceTypeId}`,
method: 'get'
})
}
// 编辑保险类型
export function updateInsuranceType(id: string, data: InsuranceType) {
return request({
url: `/insurance-type/${id}`,
method: 'put',
data
})
}
// 删除保险类型
export function deleteInsuranceType(id: string) {
return request({
url: `/insurance-type/${id}`,
method: 'delete'
})
}
// 批量删除保险类型
export function batchDeleteInsuranceType(ids: string[]) {
return request({
url: '/insurance-type/batch',
method: 'delete',
data: { ids }
})
}

View File

@ -0,0 +1,78 @@
import http from '@/utils/http'
import type { InsuranceInfo, InsuranceListParams, InsuranceListResponse, RenewInsuranceParams } from './type'
const { request } = http
// 导出类型定义
export type { InsuranceInfo, InsuranceListParams, InsuranceListResponse, RenewInsuranceParams }
// 新增保险信息
export function createInsurance(data: InsuranceInfo) {
return request({
url: '/insurance-info',
method: 'post',
data
})
}
// 查询保险信息列表
export function getInsuranceList(params: InsuranceListParams) {
return request<InsuranceListResponse>({
url: '/insurance-info/list',
method: 'get',
params
})
}
// 查询保险信息详情
export function getInsuranceDetail(id: string) {
return request<InsuranceInfo>({
url: `/insurance-info/detail/${id}`,
method: 'get'
})
}
// 编辑保险信息
export function updateInsurance(id: string, data: InsuranceInfo) {
return request({
url: `/insurance-info/${id}`,
method: 'put',
data
})
}
// 删除保险信息
export function deleteInsurance(id: string) {
return request({
url: `/insurance-info/${id}`,
method: 'delete'
})
}
// 续保
export function renewInsurance(id: string, data: RenewInsuranceParams) {
return request({
url: `/insurance-info/renew/${id}`,
method: 'post',
data
})
}
// 批量删除保险信息
export function batchDeleteInsurance(ids: string[]) {
return request({
url: '/insurance-info/batch',
method: 'delete',
data: { ids }
})
}
// 导出保险信息
export function exportInsurance(params: InsuranceListParams) {
return request({
url: '/insurance-info/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@ -0,0 +1,41 @@
/** 保险信息接口 */
export interface InsuranceInfo {
id?: string
attachInfoId:string
insuranceCompanyId: string
insuranceTypeId: string
userId: string
insuranceBillCode: string
effectiveDate: string
expireDate: string
insuranceAmount: number
insurancePremium: number
beneficiary: string
remark?: string
status?: string
createTime?: string
updateTime?: string
}
/** 保险信息列表查询参数 */
export interface InsuranceListParams {
insuranceCompanyId?: string
insuranceTypeId?: string
userId?: string
current?: number
size?: number
}
/** 保险信息列表响应 */
export interface InsuranceListResponse {
records: InsuranceInfo[]
total: number
current: number
size: number
}
/** 续保参数 */
export interface RenewInsuranceParams {
expireDate: string
insurancePremium: number
}

View File

@ -0,0 +1,71 @@
import http from '@/utils/http'
import type { ModelConfigRequest, ModelConfigResponse, ModelConfigListResponse, ModelConfigDetailResponse } from './type'
const { request } = http
/**
*
* @param data
*/
export function createModelConfig(data: ModelConfigRequest) {
return request<ModelConfigResponse>({
url: '/model-config',
method: 'post',
data
})
}
/**
*
* @param data
*/
export function updateModelConfig(data: ModelConfigRequest) {
return request<ModelConfigResponse>({
url: '/model-config',
method: 'put',
data
})
}
/**
*
* @param params
*/
export function getModelConfigList(params?: {
confThreshold?: number
keyword?: string
modelId?: string
modelName?: string
modelPath?: string
nmsThreshold?: number
page?: number
pageSize?: number
}) {
return request<ModelConfigListResponse>({
url: '/model-config/list',
method: 'get',
params
})
}
/**
*
* @param modelId ID
*/
export function getModelConfigDetail(modelId: string) {
return request<ModelConfigDetailResponse>({
url: `/model-config/${modelId}`,
method: 'get'
})
}
/**
*
* @param modelId ID
*/
export function deleteModelConfig(modelId: string) {
return request<any>({
url: `/model-config/${modelId}`,
method: 'delete'
})
}

View File

@ -0,0 +1,52 @@
/**
*
*/
export interface ModelConfigRequest {
attachId: string
confThreshold: number
modelId: string
modelName: string
nmsThreshold: number
}
/**
*
*/
export interface ModelConfigResponse {
attachId: string
confThreshold: number
modelId: string
modelName: string
nmsThreshold: number
modelPath?: string
}
/**
*
*/
export interface ModelConfigListResponse {
code: number
data: {
confThreshold?: number
idList?: string[]
modelId: string
modelName: string
modelPath: string
nmsThreshold?: number
page: number
pageSize: number
msg: string
status: number
success: boolean
}
}
/**
*
*/
export interface ModelConfigDetailResponse {
code: number
data: ModelConfigResponse
msg: string
status: number
}

View File

@ -3,7 +3,7 @@
</template>
<script setup lang="ts">
import { registerMap } from 'echarts/core'
import { registerMap } from 'echarts'
import VCharts from 'vue-echarts'
import worldMap from './world.json'
import chinaMap from './china.json'
@ -29,13 +29,16 @@ defineProps({
},
})
registerMap('world', worldMap)
registerMap('china', chinaMap)
const chart = ref(null)
defineExpose({
chart,
})
// Register maps when component is mounted
onMounted(() => {
registerMap('world', worldMap as any)
registerMap('china', chinaMap as any)
})
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,692 @@
<template>
<div class="image-import">
<a-modal
v-model:visible="visible"
title="导入图像"
width="600px"
:confirm-loading="importing"
@ok="handleImport"
@cancel="handleCancel"
>
<div class="import-content">
<!-- 选择项目和组件 -->
<a-form :model="form" layout="vertical">
<a-form-item label="图像来源" required>
<a-select
v-model="form.imageSource"
:options="imageSourceOptions"
placeholder="请选择图像来源"
:loading="loadingImageSources"
/>
</a-form-item>
<a-form-item label="目标项目" required>
<a-tree-select
v-model="form.projectId"
:data="projectTree"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择项目"
tree-checkable
:tree-check-strictly="true"
@change="onProjectChange"
/>
</a-form-item>
<a-form-item label="目标组件">
<a-select
v-model="form.componentId"
:options="componentOptions"
placeholder="请选择组件(可选)"
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">
<a-input v-model="form.altitude" placeholder="海拔" />
</a-col>
<a-col :span="8">
<a-input v-model="form.latitude" placeholder="纬度" />
</a-col>
<a-col :span="8">
<a-input v-model="form.longitude" placeholder="经度" />
</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-select
v-model="form.annotationTypes"
:options="defectTypeOptions"
placeholder="选择要自动标注的缺陷类型"
multiple
/>
</a-form-item>
</a-form>
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
ref="uploadRef"
:custom-request="() => {}"
:show-file-list="false"
accept="image/*"
multiple
@change="handleFileChange"
>
<template #upload-button>
<div class="upload-area">
<div class="upload-drag-icon">
<icon-upload size="48" />
</div>
<div class="upload-text">
<p>点击或拖拽图像文件到此区域</p>
<p class="upload-hint">支持 JPGPNGJPEG 格式可同时选择多个文件</p>
</div>
</div>
</template>
</a-upload>
</div>
<!-- 文件列表 -->
<div class="file-list" v-if="fileList.length > 0">
<div class="list-header">
<h4>待导入文件 ({{ fileList.length }})</h4>
<a-button type="text" @click="clearFiles">
<template #icon>
<icon-delete />
</template>
清空
</a-button>
</div>
<div class="list-content">
<div
v-for="(file, index) in fileList"
:key="index"
class="file-item"
>
<div class="file-info">
<div class="file-preview">
<img :src="file.preview" alt="preview" />
</div>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ formatFileSize(file.size) }}</div>
</div>
</div>
<div class="file-actions">
<a-button
type="text"
size="small"
@click="removeFile(index)"
>
<template #icon>
<icon-close />
</template>
</a-button>
</div>
</div>
</div>
</div>
<!-- 导入进度 -->
<div class="import-progress" v-if="importing">
<a-progress
:percent="importProgress"
:status="importStatus"
size="large"
/>
<p class="progress-text">{{ progressText }}</p>
</div>
<!-- 导入结果 -->
<div class="import-result" v-if="importResult">
<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">
<h4>失败文件列表:</h4>
<div class="failed-list">
<div
v-for="failed in importResult.failed"
:key="failed.filename"
class="failed-item"
>
<span class="filename">{{ failed.filename }}</span>
<span class="error">{{ failed.error }}</span>
</div>
</div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconUpload,
IconDelete,
IconClose
} from '@arco-design/web-vue/es/icon'
import {
getProjectTree,
importImages,
getImageSources
} from '@/apis/industrial-image'
import type {
ProjectTreeNode,
IndustrialImage,
ImageImportParams
} from '@/apis/industrial-image/type'
interface Props {
visible: boolean
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'importSuccess', result: { success: IndustrialImage[], failed: any[] }): void
}
interface FileItem {
file: File
name: string
size: number
preview: string
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
//
const importing = ref(false)
const importProgress = ref(0)
const importStatus = ref<'normal' | 'success' | 'warning' | 'danger'>('normal')
const progressText = ref('')
const importResult = ref<{ success: IndustrialImage[], failed: any[] } | null>(null)
const form = ref({
imageSource: '',
projectId: '',
componentId: '',
uploadUser: '',
altitude: '',
latitude: '',
longitude: '',
settings: [] 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 loadingImageSources = ref(false)
//
const visible = computed({
get: () => props.visible,
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 => {
if (node.type === 'component' || node.type === 'blade' || node.type === 'tower') {
options.push({
label: node.name,
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 => ({
label: type.name,
value: type.id
}))
})
const imageSourceOptions = computed(() => {
return imageSources.value.map(source => ({
label: source.name,
value: source.code
}))
})
//
const loadProjectTree = async () => {
try {
const res = await getProjectTree()
projectTree.value = res.data
} catch (error) {
console.error('加载项目树失败:', error)
}
}
// const loadDefectTypes = async () => {
// try {
// const res = await getDefectTypes()
// defectTypes.value = res.data
// } catch (error) {
// console.error(':', error)
// }
// }
const loadImageSources = async () => {
loadingImageSources.value = true
try {
const res = await getImageSources()
if (res.data && res.data.length > 0 && res.data[0].data) {
imageSources.value = res.data[0].data
if (imageSources.value.length > 0) {
form.value.imageSource = imageSources.value[0].code
}
}
} catch (error) {
console.error('加载图像来源失败:', error)
} finally {
loadingImageSources.value = false
}
}
const onProjectChange = (value: string) => {
form.value.componentId = ''
}
const handleFileChange = (fileList: any) => {
const files = Array.from(fileList.target?.files || []) as 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
}
fileList.value.push(fileItem)
}
reader.readAsDataURL(file)
})
}
const removeFile = (index: number) => {
fileList.value.splice(index, 1)
}
const clearFiles = () => {
fileList.value = []
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
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]
}
const handleImport = async () => {
if (!form.value.imageSource) {
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 params: ImageImportParams = {
imageSource: form.value.imageSource,
projectId: form.value.projectId,
componentId: form.value.componentId || undefined,
uploadUser: form.value.uploadUser || undefined,
altitude: form.value.altitude || undefined,
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
}
//
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) => ({
id: `img_${Date.now()}_${index}`,
name: file.name,
path: `uploaded/${file.name}`,
size: file.size,
type: file.type,
projectId: form.value.projectId,
componentId: form.value.componentId,
createTime: new Date().toISOString()
})),
failed: []
}
importResult.value = mockResult
emit('importSuccess', mockResult)
Message.success(`成功导入 ${files.length} 个图像文件`)
} catch (error) {
console.error('导入失败:', error)
importProgress.value = 100
importStatus.value = 'danger'
progressText.value = '导入失败!'
Message.error('导入图像失败')
} finally {
importing.value = false
}
}
const handleCancel = () => {
if (importing.value) {
Message.warning('正在导入中,请稍后再试')
return
}
resetForm()
visible.value = false
}
const resetForm = () => {
form.value = {
imageSource: '',
projectId: '',
componentId: '',
uploadUser: '',
altitude: '',
latitude: '',
longitude: '',
settings: [],
annotationTypes: []
}
fileList.value = []
importResult.value = null
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = ''
}
const getResultTitle = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return `导入成功!共导入 ${success.length} 个图像文件`
} else {
return `导入完成!成功 ${success.length} 个,失败 ${failed.length}`
}
}
const getResultDescription = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return '所有图像文件都已成功导入到指定项目中'
} else {
return `部分文件导入失败,请检查失败原因并重新导入`
}
}
//
watch(visible, (newValue) => {
if (newValue) {
loadProjectTree()
// loadDefectTypes()
loadImageSources()
} else {
resetForm()
}
})
</script>
<style scoped lang="scss">
.image-import {
.import-content {
max-height: 70vh;
overflow-y: auto;
}
.upload-section {
margin: 20px 0;
}
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 120px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
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;
color: #999;
}
}
}
.file-list {
margin-top: 20px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
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;
margin-right: 12px;
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>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,529 @@
<template>
<div class="industrial-image-list" :class="{ 'collapsed': isCollapsed }">
<div class="header-actions" v-if="!isCollapsed">
<slot name="header-left">
<a-button v-if="showImportButton" type="primary" @click="handleImportImages">
<template #icon><icon-upload /></template>
导入图像
</a-button>
</slot>
<div class="search-bar" v-if="showSearch">
<a-input-search
v-model="searchKeyword"
placeholder="输入关键字搜索"
allow-clear
@search="handleSearch"
@clear="handleSearchClear"
/>
</div>
<slot name="header-right"></slot>
<div class="collapse-button">
<a-button
type="text"
@click="toggleCollapse"
>
<template #icon>
<icon-up />
</template>
收起
</a-button>
</div>
</div>
<div class="image-grid" v-show="!isCollapsed">
<div v-if="imageList.length === 0" class="empty-data">
<icon-image class="empty-icon" />
<p>{{ emptyText }}</p>
</div>
<div v-else class="image-thumbnails">
<div
v-for="image in imageList"
:key="image.imageId"
class="thumbnail-item"
:class="{ active: selectedImageId === image.imageId }"
@click="handleImageSelect(image)"
>
<div class="thumbnail-image">
<img
:src="getImageUrl(image.imagePath)"
:alt="image.imageName"
@error="handleImageError"
@load="handleImageLoad"
/>
<div class="image-placeholder" v-if="!image.imagePath">
<icon-image />
<span>暂无图像</span>
</div>
<div class="thumbnail-overlay">
<div class="image-info">
<p class="image-name">{{ image.imageName }}</p>
<p class="image-size">{{ formatFileSize(image.size || 0) }}</p>
</div>
<div class="image-actions">
<a-button v-if="showPreviewAction" type="text" size="small" @click.stop="handleImagePreview(image)">
<icon-eye />
</a-button>
<a-button v-if="showProcessAction" type="text" size="small" @click.stop="handleImageProcess(image)">
<icon-settings />
</a-button>
<a-button v-if="showDeleteAction" type="text" size="small" status="danger" @click.stop="handleImageDelete(image)">
<icon-delete />
</a-button>
<slot name="item-actions" :image="image"></slot>
</div>
</div>
</div>
<div class="thumbnail-info">
<p class="thumbnail-name">{{ image.imageName }}</p>
<div class="thumbnail-meta">
<span class="image-type">{{ image.imageType?.toUpperCase() || 'IMAGE' }}</span>
<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">
<span v-if="image.partName" class="part-name">{{ image.partName }}</span>
<span v-if="image.shootingTime" class="capture-time">{{ formatTime(image.shootingTime) }}</span>
</div>
<slot name="item-extra" :image="image"></slot>
</div>
</div>
</div>
</div>
<!-- 收起状态下的展开按钮 -->
<div v-if="isCollapsed" class="expand-button-container">
<a-button
type="primary"
@click="toggleCollapse"
>
<template #icon>
<icon-down />
</template>
展开图像列表
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
IconUpload,
IconImage,
IconEye,
IconSettings,
IconDelete,
IconUp,
IconDown
} from '@arco-design/web-vue/es/icon'
export interface IndustrialImage {
imageId: string
imageName: string
imagePath?: string
imageType?: string
size?: number
defectCount?: number
partName?: string
shootingTime?: string
[key: string]: any //
}
const props = defineProps({
imageList: {
type: Array as () => IndustrialImage[],
default: () => []
},
selectedImageId: {
type: String,
default: ''
},
baseUrl: {
type: String,
default: 'http://pms.dtyx.net:9158'
},
emptyText: {
type: String,
default: '暂无图像数据'
},
showImportButton: {
type: Boolean,
default: true
},
showSearch: {
type: Boolean,
default: true
},
showPreviewAction: {
type: Boolean,
default: true
},
showProcessAction: {
type: Boolean,
default: true
},
showDeleteAction: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
importImages: []
imageSelect: [image: IndustrialImage]
imagePreview: [image: IndustrialImage]
imageProcess: [image: IndustrialImage]
imageDelete: [image: IndustrialImage]
search: [keyword: string]
collapsedChange: [collapsed: boolean]
}>()
const searchKeyword = ref('')
const isCollapsed = ref(false)
const handleImportImages = () => {
emit('importImages')
}
const handleImageSelect = (image: IndustrialImage) => {
emit('imageSelect', image)
}
const handleImagePreview = (image: IndustrialImage) => {
emit('imagePreview', image)
}
const handleImageProcess = (image: IndustrialImage) => {
emit('imageProcess', image)
}
const handleImageDelete = (image: IndustrialImage) => {
emit('imageDelete', image)
}
const handleSearch = () => {
emit('search', searchKeyword.value)
}
const handleSearchClear = () => {
searchKeyword.value = ''
emit('search', '')
}
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
emit('collapsedChange', isCollapsed.value)
}
//
const handleImageLoad = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'block'
}
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
console.warn('图片加载失败:', img.src)
}
// URL
const getImageUrl = (imagePath?: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
return `${props.baseUrl}${imagePath}`
}
//
const formatFileSize = (size: number): string => {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
//
const formatTime = (timeString: string): string => {
if (!timeString) return ''
try {
const date = new Date(timeString)
return date.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
} catch {
return timeString
}
}
</script>
<style scoped lang="scss">
.industrial-image-list {
flex: 1;
display: flex;
flex-direction: column;
background: white;
height: 100%;
overflow: hidden;
transition: height 0.3s ease;
position: relative;
&.collapsed {
flex: 0 0 auto;
height: auto;
min-height: 0;
max-height: 0;
overflow: visible;
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
padding: 10px 16px;
justify-content: space-between;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
.search-bar {
width: 300px;
}
.collapse-button {
margin-left: auto;
}
}
/* 收起状态下的展开按钮容器 */
.expand-button-container {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
.image-grid {
flex: 1;
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;
}
}
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #6b7280;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
p {
margin: 0;
font-size: 14px;
}
}
.image-thumbnails {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
.thumbnail-item {
cursor: pointer;
border: 2px solid transparent;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 8px 25px -8px rgba(0, 0, 0, 0.1);
}
&.active {
border-color: #3b82f6;
background: #f0f9ff;
}
.thumbnail-image {
position: relative;
aspect-ratio: 4/3;
overflow: hidden;
border-radius: 6px;
background: #f8f9fa;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s ease;
}
.image-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8f9fa;
color: #6b7280;
font-size: 12px;
.arco-icon {
font-size: 24px;
margin-bottom: 8px;
}
}
.thumbnail-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.7) 100%);
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 12px;
color: white;
.image-info {
.image-name {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.image-size {
margin: 0;
font-size: 12px;
opacity: 0.9;
}
}
.image-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
.arco-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
backdrop-filter: blur(4px);
&:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
&.arco-btn-status-danger:hover {
background: rgba(239, 68, 68, 0.8);
}
}
}
}
&:hover .thumbnail-overlay {
opacity: 1;
}
}
.thumbnail-info {
padding: 8px;
.thumbnail-name {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 500;
color: #1f2937;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.thumbnail-meta {
display: flex;
gap: 8px;
align-items: center;
.image-type {
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
}
.defect-count {
font-size: 12px;
color: #dc2626;
background: #fee2e2;
padding: 2px 6px;
border-radius: 4px;
}
}
.thumbnail-extra {
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 2px;
.part-name {
font-size: 11px;
color: #059669;
background: #ecfdf5;
padding: 1px 4px;
border-radius: 3px;
align-self: flex-start;
}
.capture-time {
font-size: 10px;
color: #6b7280;
opacity: 0.8;
}
}
}
}
}
}
}
</style>

View File

@ -9,7 +9,7 @@ export const defaultSettings: App.AppSettings = {
menuAccordion: true,
menuDark: false,
copyrightDisplay: true,
layout: 'mix',
layout: 'left',
enableColorWeaknessMode: false,
enableMourningMode: false,
}

View File

@ -5,7 +5,7 @@
<Header></Header>
<Tabs></Tabs>
<Main></Main>
<GiFooter v-if="appStore.copyrightDisplay" />
<!-- <GiFooter v-if="appStore.copyrightDisplay" /> -->
</a-layout>
<!-- 公告弹窗 -->

View File

@ -5,7 +5,7 @@
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>
<Menu :menus="leftMenus" :menu-style="{ width: '220px', flex: 1 }"></Menu>
<Menu :menus="leftMenus" :menu-style="{ width: '300px', flex: 1 }"></Menu>
<WwAds class="ads" />
</section>
@ -86,7 +86,7 @@ const checkAndShowNotices = () => {
}
const getMenuIcon = (item: RouteRecordRaw) => {
return item.meta?.icon || item.children?.[0].meta?.icon
return item.meta?.icon || item.children?.[0].meta?.icon || ''
}
//
@ -177,7 +177,7 @@ onMounted(() => {
background-color: var(--color-bg-1);
display: flex;
flex-direction: column;
overflow: hidden;
overflow-x: scroll;
}
&-right {

View File

@ -4,14 +4,14 @@
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<a-layout-sider
class="menu" collapsible breakpoint="xl" hide-trigger :width="230"
class="menu" collapsible breakpoint="xl" hide-trigger :width="300"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>
<a-scrollbar outer-class="menu-scroll-view" style="height: 100%; overflow: auto">
<a-scrollbar outer-class="menu-scroll-view" style="height: 100%; overflow: auto" :hide-bar="false">
<Menu></Menu>
</a-scrollbar>
<WwAds class="ads" />
<!-- <WwAds class="ads" /> -->
</a-layout-sider>
</div>
</template>
@ -68,13 +68,22 @@ const handleCollapse = (isCollapsed: boolean) => {
.menu-scroll-view {
flex: 1;
overflow: hidden;
overflow: auto;
}
.menu {
flex: 1;
overflow: hidden;
overflow: auto;
background-color: inherit;
}
}
:deep(.arco-scrollbar) {
overflow-x: hidden;
overflow-y: auto;
}
:deep(.arco-scrollbar-container) {
overflow-x: hidden !important;
}
</style>

View File

@ -13,7 +13,8 @@ const props = withDefaults(defineProps<Props>(), {
collapsed: false,
})
const appStore = useAppStore()
const title = computed(() => appStore.getTitle())
// const title = computed(() => appStore.getTitle())
const title = "武汉迪特聚能有限公司管理平台"
const logo = computed(() => appStore.getLogo())
interface Props {
@ -31,9 +32,10 @@ const toHome = () => {
height: 56px;
padding: 0 12px;
color: var(--color-text-1);
font-size: 20px;
font-size: 16px;
line-height: 1;
display: flex;
font-weight: 600;
align-items: center;
flex-shrink: 0;
cursor: pointer;

View File

@ -15,7 +15,9 @@
:icon="onlyOneChild?.meta?.icon || item?.meta?.icon"
/>
</template>
<span>{{ onlyOneChild?.meta?.title }}</span>
<a-tooltip :content="onlyOneChild?.meta?.title" position="right">
<span class="menu-item-text">{{ onlyOneChild?.meta?.title }}</span>
</a-tooltip>
</a-menu-item>
<a-sub-menu v-else v-bind="attrs" :key="item.path" :title="item?.meta?.title">
@ -72,3 +74,20 @@ watchEffect(() => {
}
})
</script>
<style scoped>
.menu-item-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
:deep(.arco-menu-title) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
</style>

View File

@ -89,4 +89,29 @@ const onCollapse = (collapsed: boolean) => {
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
:deep(.arco-menu-inner) {
min-width: 100%;
width: fit-content;
}
:deep(.arco-menu-item) {
height: auto;
line-height: 1.5;
padding: 10px 16px;
min-height: 40px;
}
:deep(.arco-menu-inline-header) {
height: auto;
line-height: 1.5;
padding: 10px 16px;
min-height: 40px;
}
:deep(.arco-menu-vertical .arco-menu-item),
:deep(.arco-menu-vertical .arco-menu-inline-header) {
margin-bottom: 4px;
white-space: normal;
}
</style>

View File

@ -11,27 +11,27 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/login/index.vue'),
meta: { hidden: true },
},
{
path: '/company',
name: 'Company',
component: Layout,
redirect: '/company/overview',
meta: { title: '企业概览', icon: 'company', hidden: false, sort: 1 },
children: [
{
path: '/company/overview',
name: 'CompanyOverview',
component: () => import('@/views/company/overview/index.vue'),
meta: { title: '企业概览', icon: 'overview', hidden: false },
}
],
},
// {
// path: '/company',
// name: 'Company',
// component: Layout,
// redirect: '/company/overview',
// meta: { title: '企业概览', icon: 'company', hidden: false, sort: 1 },
// children: [
// {
// path: '/company/overview',
// name: 'CompanyOverview',
// component: () => import('@/views/company/overview/index.vue'),
// meta: { title: '企业概览', icon: 'dashboard', hidden: false },
// }
// ],
// },
{
path: '/organization',
name: 'Organization',
component: Layout,
redirect: '/organization/hr/member',
meta: { title: '组织架构', icon: 'organization', hidden: false, sort: 2 },
meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
children: [
{
path: '/organization/hr',
@ -73,14 +73,113 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/organization/hr/salary',
name: 'HRSalary',
component: () => import('@/views/hr/salary/index.vue'),
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('@/views/hr/salary/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',
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/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/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 },
meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
}
]
},
@ -89,12 +188,21 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'OrganizationRole',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role', hidden: false },
}
],
},
{
path: '/organization/log',
name: 'OrganizationLog',
component: () => import('@/views/monitor/log/index.vue'),
meta: { title: '操作日志', icon: 'log', hidden: false },
path: '/asset-management',
name: 'AssetManagement',
component: Layout,
redirect: '/asset-management/device/inventory',
meta: { title: '资产管理', icon: 'property-safety', hidden: false, sort: 3 },
children: [
{
path: '/asset-management/intellectual-property',
name: 'IntellectualProperty',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '其他资产', icon: 'copyright', hidden: false },
}
],
},
@ -103,7 +211,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'ProductsServices',
component: Layout,
redirect: '/products-services/products/hardware/tower-monitoring',
meta: { title: '产品与服务', icon: 'products', hidden: false, sort: 3 },
meta: { title: '产品与服务', icon: 'heart', hidden: false, sort: 4 },
children: [
{
path: '/products-services/products',
@ -156,7 +264,7 @@ export const systemRoutes: RouteRecordRaw[] = [
path: '/products-services/products/software/field-assistant',
name: 'FieldAssistant',
component: () => import('@/views/service/lightning-detection/index.vue'),
meta: { title: '风电外业智能助手(外业数据实时处理)', icon: 'assistant', hidden: false },
meta: { title: '风电外业智能助手', icon: 'assistant', hidden: false },
},
{
path: '/products-services/products/software/blade-report',
@ -179,7 +287,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'Services',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/products-services/services/lightning-detection',
meta: { title: '服务', icon: 'service', hidden: false },
meta: { title: '服务', icon: 'customer-service', hidden: false },
children: [
{
path: '/products-services/services/lightning-detection',
@ -243,52 +351,71 @@ export const systemRoutes: RouteRecordRaw[] = [
path: '/project-management',
name: 'ProjectManagement',
component: Layout,
redirect: '/project-management/bidding/tender-documents',
meta: { title: '项目管理', icon: 'project', hidden: false, sort: 4 },
redirect: '/project-management/project-template/tender-documents',
meta: { title: '项目管理', icon: 'apps', hidden: false, sort: 4 },
children: [
{
path: '/project-management/bidding',
name: 'ProjectBidding',
path: '/project-management/project-template',
name: 'ProjectTemplate',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/project-management/project-template/tender-documents',
meta: {
title: '项目投标',
icon: 'gavel'
title: '施工立项',
icon: 'file-protect',
hidden: false
},
children: [
{
path: '/project-management/bidding/tender-documents',
path: '/project-management/project-template/tender-documents',
name: 'TenderDocuments',
component: () => import('@/views/project-management/bidding/tender-documents/index.vue'),
meta: {
title: '招标文件管理',
icon: 'file-text'
title: '招标文件',
icon: 'file-text',
hidden: false
}
},
{
path: '/project-management/bidding/bid-documents',
path: '/project-management/project-template/bid-documents',
name: 'BidDocuments',
component: () => import('@/views/project-management/bidding/bid-documents/index.vue'),
meta: {
title: '投标文件管理',
icon: 'file-text'
title: '投标文件',
icon: 'file-text',
hidden: false
}
},
{
path: '/project-management/bidding/award-notice',
path: '/project-management/project-template/award-notice',
name: 'AwardNotice',
component: () => import('@/views/project-management/bidding/award-notice/index.vue'),
meta: {
title: '中标通知书管理',
icon: 'trophy'
title: '中标通知书',
icon: 'trophy',
hidden: false
}
},
{
path: '/project-management/projects/initiation',
name: 'ProjectInitiation',
component: () => import('@/views/project/index.vue'),
meta: {
title: '立项管理',
icon: 'plus-circle',
hidden: false
}
},
]
},
{
path: '/project-management/contract',
name: 'ProjectContract',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/project-management/contract/revenue-contract',
meta: {
title: '合同管理',
icon: 'file-text'
title: '市场商务管理',
icon: 'file-text',
hidden: false
},
children: [
{
@ -296,8 +423,9 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'RevenueContract',
component: () => import('@/views/project-management/contract/revenue-contract/index.vue'),
meta: {
title: '收入合同管理',
icon: 'dollar'
title: '收入合同',
icon: 'dollar',
hidden: false
}
},
{
@ -305,8 +433,9 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'ExpenseContract',
component: () => import('@/views/project-management/contract/expense-contract/index.vue'),
meta: {
title: '支出合同管理',
icon: 'credit-card'
title: '支出合同',
icon: 'credit-card',
hidden: false
}
},
{
@ -314,8 +443,9 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'CostManagement',
component: () => import('@/views/project-management/contract/cost-management/index.vue'),
meta: {
title: '成本管理',
icon: 'bar-chart'
title: '成本费用',
icon: 'bar-chart',
hidden: false
}
}
]
@ -323,27 +453,73 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/project-management/projects',
name: 'ProjectsManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/project-management/projects/progress',
meta: {
title: '项目管理',
icon: 'briefcase'
title: '组织实施管理',
icon: 'briefcase',
hidden: false
},
children: [
{
path: '/project-management/projects/initiation',
name: 'ProjectInitiation',
component: () => import('@/views/project-management/projects/initiation/index.vue'),
path: '/project-management/projects/progress',
name: 'ProjectProgress',
component: () => import('@/views/project-management/projects/progress/index.vue'),
meta: {
title: '立项管理',
icon: 'plus-circle'
title: '进度管理',
icon: 'schedule',
hidden: false
}
},
{
path: '/project-management/projects/management',
name: 'ProjectDetailManagement',
path: '/project-management/projects/budget',
name: 'ProjectBudget',
component: () => import('@/views/project-management/projects/management/index.vue'),
meta: {
title: '项目详细管理',
icon: 'settings'
title: '预算管理',
icon: 'fund',
hidden: false
}
},
{
path: '/project-management/projects/personnel-distribution',
name: 'PersonnelDistribution',
component: () => import('@/views/project-management/projects/personnel-distribution/index.vue'),
meta: {
title: '人员分布图',
icon: 'team',
hidden: false
}
},
{
path: '/project-management/projects/device',
name: 'DeviceManagement',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备管理',
icon: 'plus-circle',
hidden: false
}
},
{
path: '/project-management/projects/safety',
name: 'SafetyManagement',
component: () => import('@/views/project-management/projects/safety/index.vue'),
meta: {
title: '安全管理',
icon: 'safety',
hidden: false
}
},
{
path: '/project-management/projects/quality',
name: 'QualityManagement',
component: () => import('@/views/project-management/projects/quality/index.vue'),
meta: {
title: '质量管理',
icon: 'audit',
hidden: false
}
}
]
@ -351,57 +527,245 @@ export const systemRoutes: RouteRecordRaw[] = [
],
},
{
path: '/',
name: 'Project',
path: '/construction-operation-platform',
name: 'ConstructionOperationPlatform',
component: Layout,
redirect: '/project',
meta: { title: '项目管理(旧)', icon: 'project-old', hidden: true, sort: 5 },
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
meta: { title: '施工操作台', icon: 'tool', hidden: false, sort: 5 },
children: [
{
path: '/project',
path: '/construction-operation-platform/implementation-workflow',
name: 'ImplementationWorkflow',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
meta: { title: '项目实施工作流程', icon: 'fork', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/field-construction',
name: 'FieldConstruction',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/field-construction/project-list',
meta: { title: '外业施工', icon: 'construction', hidden: false },
children: [
{
path: '/project-management/projects/list',
name: 'ProjectList',
component: () => import('@/views/project/index.vue'),
meta: { title: '项目列表', icon: 'list', hidden: false },
component: () => import('@/views/project-management/projects/list/index.vue'),
meta: {
title: '项目列表',
icon: 'unordered-list',
hidden: false
}
},
{
path: '/project/detail/:id',
name: 'ProjectDetail',
component: () => import('@/views/project/detail/index.vue'),
meta: { title: '项目详情', icon: 'detail', hidden: true },
path: '/construction-operation-platform/implementation-workflow/field-construction/technology',
name: 'FieldConstructionTechnology',
component: () => import('@/views/project-operation-platform/implementation-workflow/field-construction/project-list/index.vue'),
meta: { title: '现场工艺', icon: 'tool', hidden: false },
}
]
},
{
path: '/project/task',
name: 'TaskBoard',
component: () => import('@/views/project/task/index.vue'),
meta: { title: '任务看板', icon: 'table', hidden: false },
path: '/construction-operation-platform/implementation-workflow/data-processing',
name: 'DataProcessing',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/data-storage',
meta: { title: '数据处理', icon: 'filter', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage',
name: 'DataStorage',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/raw-data',
meta: { title: '数据入库', icon: 'database', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/raw-data',
name: 'RawData',
component: () => import('@/views/operation-platform/data-processing/data-storage/index.vue'),
meta: { title: '原数据管理', icon: 'file', hidden: false },
},
{
path: '/project/kanban',
name: 'ProjectKanban',
component: () => import('@/views/project/kanban/index.vue'),
meta: { title: '项目进度', icon: 'kanban', hidden: false },
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data',
name: 'PreprocessedData',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '数据预处理', icon: 'filter', hidden: false },
},
{
path: '/project/budget',
name: 'ProjectBudget',
component: () => import('@/views/project/budget/index.vue'),
meta: { title: '项目预算', icon: 'budget', hidden: false },
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
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/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 },
},
]
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection',
name: 'IntelligentInspection',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm',
meta: { title: '智能巡检平台', icon: 'scan', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm',
name: 'DefectAlgorithm',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm/index.vue'),
meta: { title: '缺陷检测算法', icon: 'code', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/tree-visualization',
name: 'TreeVisualization',
component: () => import('@/views/project-operation-platform/data-processing/key-info-extraction/index.vue'),
meta: { title: '树状可视化管理', icon: 'cluster', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/standard-info',
name: 'StandardInfo',
component: () => import('@/views/project-operation-platform/data-processing/standard-info/index.vue'),
meta: { title: '标准信息库', icon: 'book', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/report-generation',
name: 'ReportGeneration',
component: () => import('@/views/project-operation-platform/data-processing/report-generation/index.vue'),
meta: { title: '报告生成', icon: 'file-add', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/lifecycle-management',
name: 'LifecycleManagement',
component: () => import('@/views/project-operation-platform/lifecycle-management/index.vue'),
meta: { title: '全生命周期管理', icon: 'reload', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/drone-services',
name: 'DroneServices',
component: () => import('@/views/project-operation-platform/route-planning/index.vue'),
meta: { title: '无人机云服务', icon: 'cloud-server', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/report-review',
name: 'ReportReview',
component: () => import('@/views/project-operation-platform/data-processing/report-review/index.vue'),
meta: { title: '报告修改审核', icon: 'audit', hidden: false },
}
]
}
]
},
{
path: '/construction-operation-platform/implementation-workflow/tower-monitoring-video',
name: 'TowerMonitoringVideo',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/tower-monitoring-video/clearance-distance',
meta: { title: '塔下监测预告', icon: 'video-camera', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/tower-monitoring-video/clearance-distance',
name: 'ClearanceDistance',
component: () => import('@/views/project-operation-platform/data-processing/clearance-detection/index.vue'),
meta: { title: '净空距离检测', icon: 'fullscreen', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/tower-monitoring-video/deformation-monitoring',
name: 'DeformationMonitoring',
component: () => import('@/views/project-operation-platform/data-processing/deformation-detection/index.vue'),
meta: { title: '形变检测', icon: 'line-chart', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/tower-monitoring-video/image-detection',
name: 'ImageDetection',
component: () => import('@/views/project-operation-platform/data-processing/wide-angle-video/index.vue'),
meta: { title: '图像检测', icon: 'picture', hidden: false },
}
]
},
{
path: '/construction-operation-platform/implementation-workflow/project-delivery',
name: 'ProjectDelivery',
component: () => import('@/views/project-operation-platform/quality-management/process-verification/index.vue'),
meta: { title: '项目交付', icon: 'check-circle', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/reliability-assessment',
name: 'ReliabilityAssessment',
component: () => import('@/views/project-operation-platform/quality-management/process-verification/index.vue'),
meta: { title: '可靠性评估', icon: 'safety-certificate', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-quality-assessment',
name: 'DataQualityAssessment',
component: () => import('@/views/project-operation-platform/data-processing/data-quality-assessment/index.vue'),
meta: { title: '数据质量评估', icon: 'audit', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/defect-storage',
name: 'DefectStorage',
component: () => import('@/views/project-operation-platform/quality-management/defect-storage/index.vue'),
meta: { title: '缺陷入库', icon: 'folder-add', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/other',
name: 'Other',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/other/route-planning',
meta: { title: '其他', icon: 'more', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/other/route-planning',
name: 'RoutesPlanningOther',
component: () => import('@/views/project-operation-platform/route-planning/index.vue'),
meta: { title: '航线规划', icon: 'compass', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/other/3d-model',
name: '3DModelOther',
component: () => import('@/views/project-operation-platform/data-processing/3d-model/index.vue'),
meta: { title: '三维模型', icon: 'cube', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/other/report-template',
name: 'ReportTemplateOther',
component: () => import('@/views/project-operation-platform/data-processing/report-template/index.vue'),
meta: { title: '报告模版库', icon: 'book', hidden: false },
}
]
}
]
}
],
},
{
path: '/pwdExpired',
component: () => import('@/views/login/pwdExpired/index.vue'),
meta: { hidden: true },
path: '/chat-platform',
name: 'ChatPlatform',
component: Layout,
redirect: '/chat-platform/options',
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
children: [
// {
// path: '/chat-platform/options',
// name: 'ChatOptions',
// component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换
// meta: {
// title: '二级选项1',
// icon: 'setting',
// hidden: false
// }
// }
]
},
{
path: '/enterprise-settings',
name: 'EnterpriseSettings',
meta: {
title: '企业设置',
icon: 'building'
},
component: Layout,
redirect: '/enterprise-settings/company-info',
meta: { title: '企业设置', icon: 'setting', hidden: false, sort: 7 },
children: [
{
path: '/enterprise-settings/company-info',
@ -409,7 +773,8 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/enterprise-settings/company-info/index.vue'),
meta: {
title: '企业信息',
icon: 'info-circle'
icon: 'info-circle',
hidden: false
}
},
{
@ -418,7 +783,8 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/enterprise-settings/admin-permissions/index.vue'),
meta: {
title: '管理员权限',
icon: 'user-switch'
icon: 'lock',
hidden: false
}
},
{
@ -427,7 +793,8 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/enterprise-settings/data-migration/index.vue'),
meta: {
title: '数据迁移',
icon: 'swap'
icon: 'database',
hidden: false
}
},
{
@ -436,7 +803,57 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/enterprise-settings/version-upgrade/index.vue'),
meta: {
title: '版本升级提醒',
icon: 'arrow-up'
icon: 'upgrade',
hidden: false
}
}
]
},
{
path: '/enterprise-dashboard',
name: 'EnterpriseDashboard',
component: Layout,
redirect: '/enterprise-dashboard/overview',
meta: { title: '企业看板', icon: 'dashboard', hidden: false, sort: 8 },
children: [
{
path: '/enterprise-dashboard/overview',
name: 'DashboardOverview',
component: () => import('@/views/enterprise-dashboard/overview/index.vue'),
meta: {
title: '数据概览',
icon: 'bar-chart',
hidden: false
}
},
{
path: '/enterprise-dashboard/member-data',
name: 'MemberData',
component: () => import('@/views/enterprise-dashboard/member-data/index.vue'),
meta: {
title: '成员活跃数据',
icon: 'team',
hidden: false
}
},
{
path: '/enterprise-dashboard/function-usage',
name: 'FunctionUsage',
component: () => import('@/views/enterprise-dashboard/function-usage/index.vue'),
meta: {
title: '功能使用情况',
icon: 'appstore',
hidden: false
}
},
{
path: '/enterprise-dashboard/application-data',
name: 'ApplicationData',
component: () => import('@/views/enterprise-dashboard/application-data/index.vue'),
meta: {
title: '应用使用数据',
icon: 'pie-chart',
hidden: false
}
}
]
@ -444,26 +861,73 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/system-resource',
name: 'SystemResource',
meta: {
title: '系统资源管理',
icon: 'cluster'
},
component: Layout,
redirect: '/system-resource/device-management/warehouse',
meta: { title: '关于平台', icon: 'server', hidden: false, sort: 9 },
children: [
{
path: '/system-resource/device-management',
name: 'DeviceManagement',
path: '/system-resource/device-management/warehouse',
name: 'DeviceWarehouse',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备管理',
icon: 'desktop'
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',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/system-resource/information-system/software-management',
meta: {
title: '信息化系统管理',
icon: 'code'
icon: 'code',
hidden: false
},
children: [
{
@ -472,7 +936,8 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: {
title: '软件管理',
icon: 'appstore'
icon: 'appstore',
hidden: false
}
},
{
@ -481,13 +946,35 @@ export const systemRoutes: RouteRecordRaw[] = [
component: () => import('@/views/system-resource/information-system/system-backup/index.vue'),
meta: {
title: '系统备份管理',
icon: 'save'
icon: 'save',
hidden: false
}
}
]
},
{
path: '/system-resource/about',
name: 'About',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
title: '关于我们',
icon: 'info-circle',
hidden: false
}
}
]
}
},
{
path: '/',
redirect: '/project-management/projects/initiation',
meta: { hidden: true },
},
{
path: '/pwdExpired',
component: () => import('@/views/login/pwdExpired/index.vue'),
meta: { hidden: true },
},
]
// 固定路由(默认路由)

View File

@ -49,6 +49,9 @@ declare module 'vue' {
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']

BIN
src/views/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,532 @@
<template>
<GiPageLayout>
<div class="data-preprocessing-container">
<!-- 步骤指示器 -->
<div class="steps-container">
<a-steps :current="currentStep" size="small" class="preprocessing-steps">
<a-step title="选择数据" />
<a-step title="预处理设置" />
<a-step title="执行预处理" />
<a-step title="完成" />
</a-steps>
</div>
<!-- 步骤内容 -->
<div class="step-content">
<!-- 第一步选择数据 -->
<div v-if="currentStep === 0" class="step-panel">
<div class="step-header">
<h3>选择要预处理的数据</h3>
</div>
<div class="data-selection">
<!-- 项目选择 -->
<div class="project-select">
<a-select
v-model="selectedProject"
placeholder="请选择项目"
allow-clear
style="width: 300px"
>
<a-option
v-for="project in projectList"
:key="project.id"
:value="project.id"
>
{{ project.name }}
</a-option>
</a-select>
</div>
<!-- 文件列表 -->
<div class="file-list">
<a-table
:data="fileList"
:columns="fileColumns"
:row-selection="{ type: 'checkbox' }"
@select="handleFileSelect"
@select-all="handleFileSelectAll"
row-key="id"
:pagination="false"
:scroll="{ y: 400 }"
>
<template #fileName="{ record }">
<div class="file-item">
<a-checkbox :checked="selectedFiles.includes(record.id)" />
<span class="file-name">{{ record.fileName }}</span>
</div>
</template>
<template #fileType="{ record }">
<a-tag :color="getFileTypeColor(record.fileType)">
{{ record.fileType }}
</a-tag>
</template>
<template #fileSize="{ record }">
<span>{{ record.fileSize }}</span>
</template>
<template #uploadTime="{ record }">
<span>{{ record.uploadTime }}</span>
</template>
</a-table>
</div>
</div>
<div class="step-actions">
<a-button
type="primary"
@click="nextStep"
:disabled="selectedFiles.length === 0"
>
下一步
</a-button>
</div>
</div>
<!-- 第二步预处理设置 -->
<div v-if="currentStep === 1" class="step-panel">
<div class="step-header">
<h3>预处理设置</h3>
</div>
<div class="preprocessing-settings">
<!-- 图像处理选项 -->
<div class="setting-group">
<h4>图像处理</h4>
<div class="setting-options">
<a-checkbox v-model="settings.image.denoise">去噪</a-checkbox>
<a-checkbox v-model="settings.image.enhance">增强</a-checkbox>
<a-checkbox v-model="settings.image.crop">裁剪</a-checkbox>
<a-checkbox v-model="settings.image.rotate">旋转校正</a-checkbox>
</div>
</div>
<!-- 视频处理选项 -->
<div class="setting-group">
<h4>视频处理</h4>
<div class="setting-options">
<a-checkbox v-model="settings.video.keyFrameExtraction">关键帧提取</a-checkbox>
<a-checkbox v-model="settings.video.stabilization">稳定化</a-checkbox>
<a-checkbox v-model="settings.video.split">分辨率调整</a-checkbox>
</div>
</div>
<!-- 输出格式设置 -->
<div class="setting-group">
<h4>输出格式</h4>
<a-form-item label="输出格式">
<a-select v-model="settings.output.format" style="width: 200px">
<a-option value="JPG">JPG</a-option>
<a-option value="PNG">PNG</a-option>
<a-option value="TIFF">TIFF</a-option>
<a-option value="MP4">MP4</a-option>
</a-select>
</a-form-item>
<a-form-item label="输出目录">
<a-input v-model="settings.output.directory" style="width: 300px" />
</a-form-item>
</div>
</div>
<div class="step-actions">
<a-button @click="prevStep">上一步</a-button>
<a-button type="primary" @click="nextStep">下一步</a-button>
</div>
</div>
<!-- 第三步执行预处理 -->
<div v-if="currentStep === 2" class="step-panel">
<div class="step-header">
<h3>执行预处理</h3>
</div>
<div class="processing-section">
<div class="processing-status">
<div class="status-text">
<span v-if="!isProcessing">准备预处理...</span>
<span v-else>正在处理中...</span>
</div>
<div class="progress-container">
<a-progress
:percent="processingProgress"
:status="isProcessing ? 'normal' : 'warning'"
/>
<span class="progress-text">{{ processingProgress }}%</span>
</div>
</div>
</div>
<div class="step-actions">
<a-button @click="prevStep" :disabled="isProcessing">上一步</a-button>
<a-button
v-if="!isProcessing"
type="primary"
@click="startProcessing"
>
开始预处理
</a-button>
<a-button
v-else
type="primary"
@click="nextStep"
:disabled="processingProgress < 100"
>
下一步
</a-button>
</div>
</div>
<!-- 第四步完成 -->
<div v-if="currentStep === 3" class="step-panel">
<div class="step-header">
<h3>完成</h3>
</div>
<div class="completion-section">
<div class="completion-icon">
<div class="success-icon">
<GiSvgIcon name="check-circle" size="48" />
</div>
</div>
<div class="completion-text">
<h4>预处理完成</h4>
<p>数据预处理已成功完成共处理了 {{ selectedFiles.length }} 个文件</p>
</div>
</div>
<div class="step-actions">
<a-button @click="viewResults">查看结果</a-button>
<a-button type="primary" @click="continueProcessing">继续处理</a-button>
</div>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
defineOptions({ name: 'DataPreprocessing' })
//
const currentStep = ref(0)
const isProcessing = ref(false)
const processingProgress = ref(0)
//
const projectList = ref([
{ id: 1, name: 'A风场2023年检查' },
{ id: 2, name: 'B风场维修项目' },
{ id: 3, name: 'C风场建设项目' }
])
const selectedProject = ref<number>()
const selectedFiles = ref<number[]>([])
//
const fileList = ref([
{
id: 1,
fileName: 'IMG_20231105_1430.jpg',
fileType: 'image',
fileSize: '3.2MB',
uploadTime: '2023-11-05 14:32'
},
{
id: 2,
fileName: 'VID_20231106_0915.mp4',
fileType: 'video',
fileSize: '45.6MB',
uploadTime: '2023-11-06 09:18'
}
])
//
const fileColumns: TableColumnData[] = [
{
title: '文件名',
dataIndex: 'fileName',
width: 300,
ellipsis: true,
tooltip: true
},
{
title: '类型',
dataIndex: 'fileType',
slotName: 'fileType',
width: 100,
align: 'center'
},
{
title: '大小',
dataIndex: 'fileSize',
slotName: 'fileSize',
width: 100,
align: 'center'
},
{
title: '上传时间',
dataIndex: 'uploadTime',
slotName: 'uploadTime',
width: 150,
align: 'center'
}
]
//
const settings = reactive({
image: {
denoise: false,
enhance: false,
crop: false,
rotate: false
},
video: {
keyFrameExtraction: false,
stabilization: false,
split: false
},
output: {
format: 'JPG',
directory: '/output/processed'
}
})
//
const getFileTypeColor = (type: string) => {
switch (type) {
case 'image':
return 'blue'
case 'video':
return 'green'
case 'audio':
return 'orange'
case 'document':
return 'purple'
default:
return 'gray'
}
}
//
const handleFileSelect = (selectedRowKeys: number[]) => {
selectedFiles.value = selectedRowKeys
}
const handleFileSelectAll = (checked: boolean) => {
if (checked) {
selectedFiles.value = fileList.value.map(file => file.id)
} else {
selectedFiles.value = []
}
}
//
const nextStep = () => {
if (currentStep.value < 3) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
//
const startProcessing = () => {
isProcessing.value = true
processingProgress.value = 0
//
const timer = setInterval(() => {
processingProgress.value += 10
if (processingProgress.value >= 100) {
clearInterval(timer)
isProcessing.value = false
Message.success('预处理完成!')
}
}, 500)
}
//
const viewResults = () => {
Message.info('跳转到结果页面')
}
//
const continueProcessing = () => {
currentStep.value = 0
selectedFiles.value = []
selectedProject.value = undefined
processingProgress.value = 0
isProcessing.value = false
Message.info('开始新的预处理任务')
}
onMounted(() => {
//
})
</script>
<style scoped lang="less">
.data-preprocessing-container {
padding: 20px;
}
.steps-container {
margin-bottom: 40px;
.preprocessing-steps {
max-width: 600px;
margin: 0 auto;
}
}
.step-content {
min-height: 500px;
}
.step-panel {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.step-header {
margin-bottom: 24px;
border-bottom: 1px solid #e8e8e8;
padding-bottom: 16px;
h3 {
margin: 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
}
.data-selection {
.project-select {
margin-bottom: 20px;
}
.file-list {
margin-bottom: 20px;
}
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
.file-name {
color: #333;
}
}
.preprocessing-settings {
.setting-group {
margin-bottom: 32px;
h4 {
margin-bottom: 16px;
color: #333;
font-size: 16px;
font-weight: 600;
}
.setting-options {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
}
}
.processing-section {
text-align: center;
padding: 40px;
.processing-status {
.status-text {
font-size: 16px;
color: #666;
margin-bottom: 24px;
}
.progress-container {
display: flex;
align-items: center;
gap: 16px;
max-width: 400px;
margin: 0 auto;
.progress-text {
font-weight: 600;
color: #333;
}
}
}
}
.completion-section {
text-align: center;
padding: 40px;
.completion-icon {
margin-bottom: 24px;
.success-icon {
display: inline-block;
color: #52c41a;
}
}
.completion-text {
h4 {
margin-bottom: 12px;
color: #333;
font-size: 20px;
font-weight: 600;
}
p {
color: #666;
font-size: 14px;
}
}
}
.step-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 40px;
padding-top: 24px;
border-top: 1px solid #e8e8e8;
}
:deep(.arco-steps-item-title) {
font-size: 14px;
font-weight: 500;
}
:deep(.arco-table-cell) {
padding: 8px 16px;
}
:deep(.arco-form-item) {
margin-bottom: 16px;
}
:deep(.arco-form-item-label) {
font-weight: 500;
}
</style>

View File

@ -0,0 +1,380 @@
<template>
<div class="attachment-management">
<a-space direction="vertical" :size="16" fill>
<a-row class="search-form">
<a-col :flex="1">
<a-form :model="filterForm" layout="inline">
<a-form-item field="businessType" label="业务类型">
<a-select
v-model="filterForm.businessType"
placeholder="请选择业务类型"
allow-clear
style="width: 200px"
>
<a-option v-for="type in businessTypes" :key="type.code" :value="type.code">{{ type.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item field="keyword" label="关键词">
<a-input
v-model="filterForm.keyword"
placeholder="请输入文件名或描述关键词"
allow-clear
/>
</a-form-item>
<a-form-item field="fileType" label="文件类型">
<a-select
v-model="filterForm.fileType"
placeholder="请选择文件类型"
allow-clear
style="width: 150px"
>
<a-option value="image">图片</a-option>
<a-option value="document">文档</a-option>
<a-option value="video">视频</a-option>
<a-option value="other">其他</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="resetSearch">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-card :bordered="false">
<template #title>附件列表</template>
<template #extra>
<a-space>
<a-button type="primary" size="small" :disabled="selectedRowKeys.length === 0">
<template #icon><icon-download /></template>
批量下载
</a-button>
<a-button
type="primary"
status="danger"
size="small"
:disabled="selectedRowKeys.length === 0"
@click="handleBatchDelete"
>
<template #icon><icon-delete /></template>
批量删除
</a-button>
</a-space>
</template>
<a-table
:loading="loading"
:data="tableData"
:pagination="pagination"
@page-change="onPageChange"
row-key="id"
:row-selection="{
type: 'checkbox',
showCheckedAll: true,
selectedRowKeys: selectedRowKeys,
onChange: onSelectionChange
}"
>
<template #columns>
<a-table-column title="文件名" data-index="fileName">
<template #cell="{ record }">
<a-space>
<component :is="getFileIcon(record.fileType)" />
<a href="javascript:;" @click="previewFile(record)">
{{ record.fileName }}
</a>
</a-space>
</template>
</a-table-column>
<a-table-column title="文件类型" data-index="fileType">
<template #cell="{ record }">
<a-tag>{{ getFileTypeText(record.fileType) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="业务类型" data-index="businessType">
<template #cell="{ record }">
<a-tag>{{ getBusinessTypeName(record.businessType) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="文件大小" data-index="fileSize">
<template #cell="{ record }">
{{ formatFileSize(record.fileSize) }}
</template>
</a-table-column>
<a-table-column title="上传时间" data-index="createTime" />
<a-table-column title="描述" data-index="remark" />
<a-table-column title="操作" width="180">
<template #cell="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">
<template #icon><icon-eye /></template>
预览
</a-button>
<a-button size="small" @click="downloadFile(record)">
<template #icon><icon-download /></template>
下载
</a-button>
<a-popconfirm
content="确定要删除该文件吗?"
@ok="deleteFile(record)"
>
<a-button size="small" status="danger">
<template #icon><icon-delete /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table-column>
</template>
<template #empty>
<a-empty description="暂无数据" />
</template>
</a-table>
</a-card>
<a-modal
v-model:visible="previewVisible"
:title="currentPreviewFile?.fileName"
:footer="false"
style="width: 70%"
unmount-on-close
>
<FilePreview
v-if="previewVisible"
:file="currentPreviewFile"
style="width: 100%; height: 500px"
/>
</a-modal>
</a-space>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import FilePreview from '@/components/FilePreview/index.vue'
import {
IconSearch,
IconRefresh,
IconDownload,
IconDelete,
IconEye,
IconFile
} from '@arco-design/web-vue/es/icon'
import { getAttachBusinessTypes, getAttachmentList, deleteAttachment } from '@/apis/attach-info'
import type { AttachInfoData, BusinessType } from '@/apis/attach-info/type'
defineOptions({ name: 'AttachmentManagement' })
const loading = ref(false)
const previewVisible = ref(false)
const currentPreviewFile = ref<any>(null)
const businessTypes = ref<BusinessType[]>([])
const selectedRowKeys = ref<(string | number)[]>([])
const filterForm = reactive({
businessType: '',
keyword: '',
fileType: '',
})
const pagination = reactive({
total: 0,
current: 1,
pageSize: 10,
showTotal: true,
showJumper: true,
})
const tableData = ref<AttachInfoData[]>([])
//
const fetchBusinessTypes = async () => {
try {
const res = await getAttachBusinessTypes()
if (res.data) {
businessTypes.value = res.data
//
if (businessTypes.value.length > 0) {
filterForm.businessType = businessTypes.value[0].code
fetchAttachmentList()
}
}
} catch (error) {
console.error('获取业务类型失败:', error)
Message.error('获取业务类型失败')
}
}
//
const fetchAttachmentList = async () => {
if (!filterForm.businessType) {
Message.warning('请先选择业务类型')
return
}
loading.value = true
try {
const res = await getAttachmentList(filterForm.businessType)
if (res.data) {
tableData.value = res.data
pagination.total = res.data.length
} else {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取附件列表失败:', error)
Message.error('获取附件列表失败')
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
onMounted(() => {
fetchBusinessTypes()
})
const handleSearch = () => {
fetchAttachmentList()
}
const resetSearch = () => {
//
filterForm.keyword = ''
filterForm.fileType = ''
fetchAttachmentList()
}
const onPageChange = (page: number) => {
pagination.current = page
fetchAttachmentList()
}
const onSelectionChange = (rowKeys: (string | number)[]) => {
selectedRowKeys.value = rowKeys
}
const previewFile = (file: AttachInfoData) => {
currentPreviewFile.value = {
...file,
fileFormat: getFileFormat(file.fileName),
}
previewVisible.value = true
}
const downloadFile = (file: AttachInfoData) => {
if (file.fileUrl) {
const a = document.createElement('a')
a.href = file.fileUrl
a.download = file.fileName
a.click()
Message.success(`开始下载: ${file.fileName}`)
} else {
Message.error('文件链接不存在')
}
}
const deleteFile = async (file: AttachInfoData) => {
try {
const res = await deleteAttachment(file.id)
if (res) {
Message.success(`已删除: ${file.fileName}`)
//
tableData.value = tableData.value.filter(item => item.id !== file.id)
pagination.total = tableData.value.length
} else {
Message.error('删除文件失败')
}
} catch (error) {
console.error('删除文件失败:', error)
Message.error('删除文件失败')
}
}
const handleBatchDelete = () => {
if (selectedRowKeys.value.length === 0) {
return
}
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个文件吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const promises = selectedRowKeys.value.map(id => deleteAttachment(id))
await Promise.all(promises)
Message.success('批量删除成功')
fetchAttachmentList()
selectedRowKeys.value = []
} catch (error) {
console.error('批量删除失败:', error)
Message.error('批量删除失败')
}
}
})
}
const getFileTypeText = (type: string) => {
const typeMap = {
image: '图片',
document: '文档',
video: '视频',
other: '其他'
}
return typeMap[type] || '未知'
}
const getBusinessTypeName = (code: string) => {
const businessType = businessTypes.value.find(item => item.code === code)
return businessType ? businessType.name : code
}
const formatFileSize = (size: number) => {
if (!size) return '0 B'
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]}`
}
const getFileFormat = (fileName: string) => {
const ext = fileName.split('.').pop()?.toLowerCase() || ''
return ext
}
const getFileIcon = (fileType: string) => {
// 使
return IconFile
}
</script>
<style scoped lang="scss">
.attachment-management {
width: 100%;
.search-form {
margin-bottom: 16px;
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="attachment-upload">
<a-space direction="vertical" :size="16" style="width: 100%">
<a-form :model="formData" ref="formRef">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="businessType" label="业务类型" required>
<a-select v-model="formData.businessType" placeholder="请选择业务类型">
<a-option v-for="type in businessTypes" :key="type.code" :value="type.code">{{ type.name }}</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="fileType" label="文件类型" required>
<a-select v-model="formData.fileType" placeholder="请选择文件类型">
<a-option value="image">图片</a-option>
<a-option value="document">文档</a-option>
<a-option value="video">视频</a-option>
<a-option value="other">其他</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="文件描述">
<a-textarea
v-model="formData.remark"
placeholder="请输入文件描述"
:auto-size="{ minRows: 2, maxRows: 5 }"
/>
</a-form-item>
<a-form-item field="userDefinedPath" label="自定义路径">
<a-input
v-model="formData.userDefinedPath"
placeholder="请输入自定义路径(可选)"
allow-clear
/>
</a-form-item>
<a-form-item field="files" label="附件上传" required>
<a-upload
:default-file-list="fileList"
multiple
:action="null"
list-type="picture-card"
:custom-request="customRequest"
@change="handleChange"
>
<template #upload-button>
<div class="upload-trigger">
<div class="upload-trigger-box">
<div class="upload-trigger-text">
<IconPlus />
<div style="margin-top: 10px">上传文件</div>
</div>
<div class="upload-trigger-tip">
支持单个或批量上传每个文件不超过100MB
</div>
</div>
</div>
</template>
</a-upload>
</a-form-item>
<div class="form-actions">
<a-button type="primary" @click="handleSubmit" :loading="submitting">提交</a-button>
<a-button style="margin-left: 10px" @click="resetForm">重置</a-button>
</div>
</a-form>
</a-space>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } 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 type { BusinessType } from '@/apis/attach-info/type'
defineOptions({ name: 'AttachmentUpload' })
const formRef = ref()
const fileList = ref([])
const submitting = ref(false)
const businessTypes = ref<BusinessType[]>([])
interface FileItem {
file: File
status: string
uid: string | number
name: string
}
const formData = reactive({
businessType: '',
fileType: '',
remark: '',
userDefinedPath: '',
files: [] as FileItem[]
})
//
const fetchBusinessTypes = async () => {
try {
const res = await getAttachBusinessTypes()
if (res.data) {
businessTypes.value = res.data
}
} catch (error) {
console.error('获取业务类型失败:', error)
Message.error('获取业务类型失败')
}
}
onMounted(() => {
fetchBusinessTypes()
})
const customRequest = (options: any) => {
const { file, onProgress, onSuccess, onError } = options
//
const fileItem = {
file,
status: 'ready',
uid: options.fileItem.uid,
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()
}
}, 300)
}
const handleChange = (fileList) => {
console.log('文件列表变化:', 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
}
const res = await batchAddAttachment(formData.businessType, formDataToSend, params)
if (res) {
Message.success('文件上传成功')
resetForm()
} else {
Message.error('文件上传失败')
}
} catch (error: any) {
console.error('上传失败:', error)
Message.error('上传失败: ' + (error.msg || '未知错误'))
} finally {
submitting.value = false
}
}
const resetForm = () => {
formRef.value.resetFields()
fileList.value = []
formData.files = []
}
</script>
<style scoped lang="scss">
.attachment-upload {
width: 100%;
.upload-trigger {
width: 100%;
height: 100%;
display: flex;
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));
font-size: 12px;
}
}
.form-actions {
margin-top: 24px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="data-storage-container">
<a-card title="数据入库" :bordered="false">
<a-tabs>
<a-tab-pane key="upload" title="附件上传">
<AttachmentUpload />
</a-tab-pane>
<a-tab-pane key="management" title="附件管理">
<AttachmentManagement />
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script setup lang="ts">
import AttachmentUpload from './components/AttachmentUpload.vue'
import AttachmentManagement from './components/AttachmentManagement.vue'
defineOptions({ name: 'DataStorage' })
</script>
<style scoped lang="scss">
.data-storage-container {
padding: 20px;
}
</style>

View File

@ -0,0 +1,354 @@
<template>
<div class="auto-recognition-panel">
<div class="panel-header">
<h3>自动识别设置</h3>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
</a-button>
</div>
<div class="panel-content">
<div class="panel-section">
<div class="section-header">
<h4>识别算法</h4>
<!-- <a-link @click="goToModelManagement">管理模型</a-link> -->
</div>
<a-select v-model:model-value="selectedAlgorithm" placeholder="请选择识别算法" loading-tip="加载中...">
<template v-if="loadingModels">
<a-option value="" disabled>加载中...</a-option>
</template>
<template v-else>
<a-option v-for="model in modelList" :key="model.modelId" :value="model.modelId">
{{ model.modelName }}
</a-option>
<a-empty v-if="modelList.length === 0" description="暂无可用模型" />
</template>
</a-select>
</div>
<div class="panel-section">
<div class="section-header">
<h4>置信度</h4>
</div>
<a-slider
v-model:model-value="confidence"
:min="0"
:max="100"
:step="10"
show-tooltip
:format-tooltip="(value) => `${value}%`"
/>
</div>
<div class="panel-section">
<div class="section-header">
<h4>缺陷类型</h4>
</div>
<div class="defect-types">
<div v-if="loadingDefectTypes" class="loading-defect-types">
<a-spin />
<span>加载缺陷类型...</span>
</div>
<a-checkbox-group v-else v-model:model-value="selectedDefectTypes">
<div
v-for="defectType in defectTypes"
:key="defectType.value"
class="defect-item"
>
<a-checkbox :value="defectType.value">
<span class="defect-label">{{ defectType.label }}</span>
</a-checkbox>
</div>
</a-checkbox-group>
</div>
</div>
<div class="panel-actions">
<a-button type="primary" @click="handleStartRecognition" :loading="isRecognizing" block>
开始识别
</a-button>
<a-button @click="handleResetSettings" block>
重置设置
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconClose } from '@arco-design/web-vue/es/icon'
import { listDefectType } from '@/apis/common/common'
import { getModelConfigList } from '@/apis/model-config'
import type { DefectTypeResp, DefectTypeOption } from '@/apis/common/type'
import type { ModelConfigResponse } from '@/apis/model-config/type'
import { useRouter } from 'vue-router'
const props = defineProps<{
currentImage?: {
url: string
name: string
partName?: string
componentType?: string
}
}>()
const emit = defineEmits<{
close: []
startRecognition: [settings: {
algorithm: string
confidence: number
defectTypes: string[]
}]
}>()
//
const selectedAlgorithm = ref('')
const confidence = ref(100)
const selectedDefectTypes = ref<string[]>([])
const isRecognizing = ref(false)
//
const modelList = ref<ModelConfigResponse[]>([])
const loadingModels = ref(false)
//
const defectTypes = ref<DefectTypeOption[]>([])
const loadingDefectTypes = ref(false)
//
const loadModelList = async () => {
try {
loadingModels.value = true
const response = await getModelConfigList()
if (response && response.rows) {
//
if (Array.isArray(response.rows)) {
modelList.value = response.rows;
} else {
//
const responseData = response.rows;
modelList.value = [];
// API
if (responseData && Array.isArray(responseData)) {
modelList.value = responseData.map((item: any) => ({
modelId: item.modelId || '',
modelName: item.modelName || item.modelId || '',
attachId: item.attachId || '',
confThreshold: item.confThreshold || 0.5,
nmsThreshold: item.nmsThreshold || 0.5,
modelPath: item.modelPath || ''
}));
}
}
//
if (modelList.value.length > 0) {
selectedAlgorithm.value = modelList.value[0].modelId
}
}
} catch (error) {
console.error('获取模型列表失败:', error)
Message.error('获取模型列表失败')
modelList.value = []
} finally {
loadingModels.value = false
}
}
//
const loadDefectTypes = async () => {
try {
loadingDefectTypes.value = true
const response = await listDefectType()
// - API
const defectTypeOptions: DefectTypeOption[] = []
//
response.data.forEach((item: DefectTypeResp) => {
//
Object.entries(item).forEach(([code, name]) => {
defectTypeOptions.push({
value: code,
label: name,
code: code
})
})
})
defectTypes.value = defectTypeOptions
//
if (defectTypes.value.length > 0) {
selectedDefectTypes.value = defectTypes.value.slice(0, 3).map(item => item.value)
}
} catch (error) {
console.error('获取缺陷类型失败:', error)
Message.error('获取缺陷类型失败')
defectTypes.value = []
selectedDefectTypes.value = []
} finally {
loadingDefectTypes.value = false
}
}
//
const handleStartRecognition = async () => {
if (!props.currentImage) {
Message.error('请先选择图像')
return
}
if (!selectedAlgorithm.value) {
Message.error('请选择识别算法')
return
}
if (selectedDefectTypes.value.length === 0) {
Message.error('请选择至少一种缺陷类型')
return
}
const settings = {
algorithm: selectedAlgorithm.value,
confidence: confidence.value,
defectTypes: selectedDefectTypes.value
}
try {
isRecognizing.value = true
Message.success('识别设置已保存,开始识别...')
emit('startRecognition', settings)
} catch (error) {
console.error('识别失败:', error)
Message.error('识别失败,请重试')
} finally {
isRecognizing.value = false
}
}
//
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) : []
}
//
const setRecognizing = (status: boolean) => {
isRecognizing.value = status
}
//
onMounted(() => {
loadModelList()
loadDefectTypes()
})
//
const router = useRouter()
//
const goToModelManagement = () => {
router.push('/construction-operation-platform/implementation-workflow/data-processing/model-config')
}
defineExpose({
setRecognizing
})
</script>
<style scoped lang="scss">
.auto-recognition-panel {
height: 100%;
display: flex;
flex-direction: column;
background: white;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
}
.panel-content {
// flex: 1;
display: flex;
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;
align-items: center;
margin-bottom: 12px;
h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
}
.arco-select {
width: 100%;
}
.defect-types {
.loading-defect-types {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
color: #666;
}
.arco-checkbox-group {
display: flex;
flex-direction: column;
gap: 6px;
.defect-item {
padding: 4px 0;
.arco-checkbox {
font-size: 13px;
}
.defect-label {
margin-right: 8px;
}
}
}
}
}
.panel-actions {
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: auto;
}
}
}
</style>

View File

@ -0,0 +1,445 @@
<template>
<div class="defect-details-form">
<div class="form-header">
<h3>缺陷详情</h3>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
</a-button>
</div>
<div class="form-body">
<a-form :model="form" layout="vertical">
<a-form-item label="缺陷名称" field="defectName">
<a-input v-model="form.defectName" placeholder="请输入缺陷名称" />
</a-form-item>
<!-- 缺陷类型 -->
<a-form-item
field="defectType"
label="缺陷类型"
:rules="[{ required: true, message: '请选择缺陷类型' }]"
>
<a-select
v-model="form.defectType"
placeholder="请选择缺陷类型"
:loading="loadingDefectTypes"
>
<a-option v-for="type in defectTypes" :key="type.code" :value="type.code">{{ type.label }}</a-option>
</a-select>
</a-form-item>
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!-- 缺陷等级 -->
<a-form-item
field="defectLevel"
label="缺陷等级"
:rules="[{ required: true, message: '请选择缺陷等级' }]"
>
<a-select
v-model="form.defectLevel"
placeholder="请选择缺陷等级"
:loading="loadingDefectLevels"
@change="handleDefectLevelChange"
>
<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"
placeholder="请输入缺陷描述"
:rows="3"
/>
</a-form-item>
<a-form-item label="维修建议" field="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-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } 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 { listDefectType } from '@/apis/common/common'
import type { DefectTypeResp, DefectTypeOption } from '@/apis/common/type'
interface DefectFormData {
defectName: string
defectType: string
defectTypeLabel: string
defectLevel: string
defectLevelLabel: string
defectPosition: string
description: string
repairIdea: string
}
const props = defineProps<{
annotation: ExtendedAnnotation | null
}>()
const emit = defineEmits<{
close: []
submit: [formData: DefectFormData, annotation: ExtendedAnnotation]
}>()
//
const submitting = ref(false)
//
const defectTypes = ref<DefectTypeOption[]>([])
const loadingDefectTypes = ref(false)
//
const defectLevels = ref<DefectTypeOption[]>([])
const loadingDefectLevels = ref(false)
//
const form = reactive<DefectFormData>({
defectName: '',
defectType: '',
defectTypeLabel: '',
defectLevel: '',
defectLevelLabel: '',
defectPosition: '',
description: '',
repairIdea: '建议进行进一步检查'
})
//
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) => {
//
const entries = Object.entries(item)
if (entries.length > 0) {
const [code, name] = entries[0]
defectTypeOptions.push({
value: code,
label: name as string,
code: code
})
}
})
}
defectTypes.value = defectTypeOptions
console.log('缺陷类型选项:', defectTypeOptions)
//
if (defectTypeOptions.length > 0 && !form.defectType) {
form.defectType = defectTypeOptions[0].code
form.defectTypeLabel = defectTypeOptions[0].label
}
} catch (error) {
console.error('获取缺陷类型失败:', error)
Message.error('获取缺陷类型失败')
defectTypes.value = []
} finally {
loadingDefectTypes.value = false
}
}
//
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 => {
defectLevelOptions.push({
code: item.code,
label: item.name, // 使namelabel
value: item.value,
name: item.name,
sort: item.sort
})
})
defectLevels.value = defectLevelOptions
} else if (Array.isArray(response.data)) {
//
response.data.forEach((item: any) => {
const entries = Object.entries(item)
if (entries.length > 0) {
const [code, name] = entries[0]
defectLevelOptions.push({
code: code,
label: name as string,
value: code,
name: name as string
})
}
})
defectLevels.value = defectLevelOptions
} else {
console.error('无法解析缺陷等级数据格式:', response)
//
defectLevels.value = [
{ code: 'low', label: '轻微', value: 'low', name: '轻微' },
{ code: 'medium', label: '中等', value: 'medium', 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('中'))
)
form.defectLevel = mediumLevel?.code || defectLevels.value[0].code
form.defectLevelLabel = mediumLevel?.name || mediumLevel?.label || defectLevels.value[0].label
}
} catch (error) {
console.error('加载缺陷等级失败:', error)
//
defectLevels.value = [
{ code: 'low', label: '轻微', value: 'low', name: '轻微' },
{ code: 'medium', label: '中等', value: 'medium', name: '中等' },
{ code: 'high', label: '严重', value: 'high', name: '严重' }
]
} finally {
loadingDefectLevels.value = false
}
}
//
watch(() => props.annotation, (newAnnotation) => {
if (newAnnotation) {
//
const isVirtualAnnotation = newAnnotation.id.startsWith('virtual-')
//
const isMultiAnnotation = newAnnotation.metadata?.isMultiAnnotation
if (isVirtualAnnotation) {
//
form.description = '手动添加的缺陷,请填写详细描述'
} else if (isMultiAnnotation) {
//
const annotationCount = newAnnotation.metadata?.allAnnotations?.length || 0
form.description = `包含${annotationCount}个标注区域的缺陷,请填写详细描述`
} else {
// Canvas
const rect = newAnnotation.points
if (rect && rect.length >= 2) {
const width = Math.abs(rect[1].x - rect[0].x)
const height = Math.abs(rect[1].y - rect[0].y)
form.description = `标注区域大小: ${Math.round(width)}x${Math.round(height)} 像素`
}
}
//
if (newAnnotation.label) {
form.defectName = newAnnotation.label
}
}
}, { immediate: true })
const handleSubmit = async () => {
if (!props.annotation) {
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
})
} else {
Message.loading({
content: '正在保存缺陷信息...',
duration: 0
})
}
//
emit('submit', form, props.annotation)
} catch (error) {
console.error('提交缺陷失败:', error)
Message.error('提交失败,请重试')
} finally {
submitting.value = false
}
}
//
const handleDefectTypeChange = (value: string) => {
const selectedType = defectTypes.value.find(type => type.code === value)
if (selectedType) {
form.defectTypeLabel = selectedType.name
}
}
//
const handleDefectLevelChange = (value: string) => {
const selectedLevel = defectLevels.value.find(level => level.code === value)
if (selectedLevel) {
form.defectLevelLabel = selectedLevel.name
}
}
//
onMounted(() => {
//
if (props.annotation) {
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()
})
</script>
<style scoped lang="scss">
.defect-details-form {
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
}
.form-body {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.form-footer {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
display: flex;
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);
}
}
}
}
:deep(.arco-form-item) {
margin-bottom: 16px;
}
:deep(.arco-form-item-label) {
font-weight: 500;
color: #374151;
}
</style>

View File

@ -0,0 +1,709 @@
<template>
<div class="defect-details-panel">
<!-- 头部 -->
<div class="panel-header">
<h3>缺陷详情</h3>
</div>
<!-- 缺陷详情表单 -->
<div v-if="selectedDefect" class="defect-detail-form">
<div class="form-container">
<!-- 编号和缺陷名称 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">编号</label>
<a-input :model-value="defectForm.defectCode" readonly />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">缺陷名称</label>
<a-input v-model="defectForm.defectName" />
</div>
</div>
<!-- 部位 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">部位</label>
<a-input v-model="defectForm.defectPosition" />
</div>
</div>
<!-- 缺陷类型 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">缺陷类型</label>
<a-select v-model="defectForm.defectType">
<a-option value="crack">裂纹</a-option>
<a-option value="erosion">截蚀</a-option>
<a-option value="wear">磨损</a-option>
<a-option value="corrosion">腐蚀</a-option>
<a-option value="deformation">变形</a-option>
<a-option value="damage">损伤</a-option>
</a-select>
</div>
</div>
<!-- 严重程度 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">严重程度</label>
<a-select v-model="defectForm.defectLevel">
<a-option value="low">轻微</a-option>
<a-option value="medium">中等</a-option>
<a-option value="high">严重</a-option>
<a-option value="critical">危急</a-option>
</a-select>
</div>
</div>
<!-- 维修状态 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">维修状态</label>
<a-select v-model="defectForm.repairStatus">
<a-option value="pending">未处理</a-option>
<a-option value="processing">处理中</a-option>
<a-option value="completed">已完成</a-option>
</a-select>
</div>
</div>
<!-- 轴向尺寸和弦向尺寸 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">轴向尺寸</label>
<div class="size-input-group">
<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"
size="small"
style="width: 70px"
/>
<a-button
size="small"
@click="defectForm.axial = (defectForm.axial || 0) + 1"
>
+
</a-button>
<span class="unit">(mm)</span>
</div>
</div>
<div class="form-group">
<label class="form-label">弦向尺寸</label>
<div class="size-input-group">
<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"
size="small"
style="width: 70px"
/>
<a-button
size="small"
@click="defectForm.chordwise = (defectForm.chordwise || 0) + 1"
>
+
</a-button>
<span class="unit">(mm)</span>
</div>
</div>
</div>
<!-- 面积 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">面积(mm²)</label>
<div class="size-input-group">
<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"
size="small"
style="width: 70px"
/>
<a-button
size="small"
@click="defectForm.area = (defectForm.area || 0) + 1"
>
+
</a-button>
</div>
</div>
</div>
<!-- 描述 -->
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">描述</label>
<a-textarea
v-model="defectForm.description"
:rows="3"
placeholder="叶片前缘纵向裂纹长度约15mm宽度约2mm"
/>
<div class="action-button-container">
<a-button
size="small"
@click="handleSelectFromStandardDescription"
class="standard-library-btn"
>
从标准描述库选择
</a-button>
</div>
</div>
</div>
<!-- 维修建议 -->
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">维修建议</label>
<a-textarea
v-model="defectForm.repairIdea"
:rows="3"
placeholder="建议进行表面修复处理,防止裂纹扩散"
/>
<div class="action-button-container">
<a-button
size="small"
@click="handleSelectFromStandardInfo"
class="standard-library-btn"
>
从标准信息库选择
</a-button>
</div>
</div>
</div>
<!-- 其他信息 (用于测试滚动) -->
<div class="form-row">
<div class="form-group">
<label class="form-label">检测方法</label>
<a-select v-model="defectForm.detectionMethod" placeholder="选择检测方法">
<a-option value="visual">目视检测</a-option>
<a-option value="ultrasonic">超声波检测</a-option>
<a-option value="magnetic">磁粉检测</a-option>
<a-option value="penetrant">渗透检测</a-option>
</a-select>
</div>
<div class="form-group">
<label class="form-label">检测日期</label>
<a-date-picker v-model="defectForm.inspectionDate" style="width: 100%" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">检测人员</label>
<a-input v-model="defectForm.inspector" placeholder="请输入检测人员姓名" />
</div>
<div class="form-group">
<label class="form-label">复检状态</label>
<a-select v-model="defectForm.recheckStatus" placeholder="选择复检状态">
<a-option value="not_required">无需复检</a-option>
<a-option value="pending">待复检</a-option>
<a-option value="passed">复检通过</a-option>
<a-option value="failed">复检未通过</a-option>
</a-select>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">技术备注</label>
<a-textarea
v-model="defectForm.technicalNotes"
:rows="4"
placeholder="记录技术细节、处理方案、注意事项等"
/>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label class="form-label">修复记录</label>
<a-textarea
v-model="defectForm.repairRecord"
:rows="3"
placeholder="记录修复过程、使用材料、处理结果等"
/>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<a-button type="primary" @click="handleSave">保存</a-button>
<a-button status="danger" @click="handleDelete">删除</a-button>
</div>
</div>
<!-- 无缺陷选中状态 -->
<div v-else class="no-defect-selected">
<icon-file class="empty-icon" />
<p>请从左侧选择缺陷进行编辑</p>
</div>
<!-- 标准描述库选择模态框 -->
<a-modal
v-model:visible="standardDescriptionModalVisible"
title="选择标准描述"
width="600px"
@ok="handleConfirmStandardDescription"
@cancel="handleCancelStandardDescription"
>
<div class="standard-library-content">
<a-list :data="standardDescriptions" :bordered="false">
<template #item="{ item }">
<a-list-item
:class="{ 'selected': selectedStandardDescription === item }"
@click="selectedStandardDescription = item"
class="clickable-item"
>
<div class="description-item">
<div class="description-title">{{ item.title }}</div>
<div class="description-content">{{ item.content }}</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</a-modal>
<!-- 标准信息库选择模态框 -->
<a-modal
v-model:visible="standardInfoModalVisible"
title="选择标准维修建议"
width="600px"
@ok="handleConfirmStandardInfo"
@cancel="handleCancelStandardInfo"
>
<div class="standard-library-content">
<a-list :data="standardRepairIdeas" :bordered="false">
<template #item="{ item }">
<a-list-item
:class="{ 'selected': selectedStandardInfo === item }"
@click="selectedStandardInfo = item"
class="clickable-item"
>
<div class="description-item">
<div class="description-title">{{ item.title }}</div>
<div class="description-content">{{ item.content }}</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, watch, reactive } 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'
const props = defineProps<{
selectedDefect: DefectDetectionResult | null
}>()
const emit = defineEmits<{
'edit-defect': [defect: DefectDetectionResult]
'delete-defect': [defectId: string]
}>()
//
const defectForm = reactive({
defectCode: '',
defectName: '',
defectType: '',
defectPosition: '',
defectLevel: '',
repairStatus: '',
axial: 0,
chordwise: 0,
area: 0,
description: '',
repairIdea: '',
detectionMethod: '',
inspectionDate: '',
inspector: '',
recheckStatus: '',
technicalNotes: '',
repairRecord: ''
})
//
const standardDescriptionModalVisible = ref(false)
const selectedStandardDescription = ref<any>(null)
const standardDescriptions = ref([
{
title: '前缘裂纹模板',
content: '叶片前缘纵向裂纹长度约15mm宽度约2mm'
},
{
title: '表面磨损模板',
content: '叶片表面出现明显磨损痕迹,影响空气动力学性能'
},
{
title: '截蚀损伤模板',
content: '叶片前缘截蚀损伤表面粗糙度增加深度约1-3mm'
},
{
title: '腐蚀斑点模板',
content: '叶片表面出现腐蚀斑点直径约5-10mm深度轻微'
}
])
//
const standardInfoModalVisible = ref(false)
const selectedStandardInfo = ref<any>(null)
const standardRepairIdeas = ref([
{
title: '裂纹修复建议',
content: '建议进行表面修复处理,防止裂纹扩散'
},
{
title: '磨损处理建议',
content: '定期监测磨损程度,必要时进行表面打磨和重新涂层'
},
{
title: '截蚀修复建议',
content: '建议进行前缘修复,使用专用胶泥填补并重新整形'
},
{
title: '腐蚀处理建议',
content: '清理腐蚀区域,涂抹防腐涂层,定期检查'
},
{
title: '严重损伤建议',
content: '建议立即停机检修,更换受损部件,避免安全隐患'
}
])
//
watch(() => props.selectedDefect, (newDefect) => {
if (newDefect) {
Object.assign(defectForm, {
defectCode: newDefect.defectCode || '',
defectName: newDefect.defectName || '',
defectType: newDefect.defectType || '',
defectPosition: newDefect.defectPosition || '',
defectLevel: newDefect.defectLevel || '',
repairStatus: newDefect.repairStatus || '',
axial: newDefect.axial || 0,
chordwise: newDefect.chordwise || 0,
area: calculateArea(newDefect.axial || 0, newDefect.chordwise || 0),
description: newDefect.description || '',
repairIdea: newDefect.repairIdea || ''
})
}
}, { immediate: true })
//
const calculateArea = (axial: number, chordwise: number) => {
return Math.round(axial * chordwise)
}
//
watch([() => defectForm.axial, () => defectForm.chordwise], ([newAxial, newChordwise]) => {
defectForm.area = calculateArea(newAxial || 0, newChordwise || 0)
})
//
const handleSave = () => {
if (!props.selectedDefect?.defectId) {
Message.warning('请先选择要编辑的缺陷')
return
}
const updatedDefect = {
...props.selectedDefect,
...defectForm
}
emit('edit-defect', updatedDefect)
Message.success('缺陷信息已保存')
}
//
const handleDelete = () => {
if (!props.selectedDefect?.defectId) {
Message.warning('请先选择要删除的缺陷')
return
}
Modal.confirm({
title: '确认删除',
content: '确定要删除此缺陷记录吗?删除后无法恢复。',
onOk: () => {
emit('delete-defect', props.selectedDefect!.defectId)
Message.success('缺陷已删除')
}
})
}
//
const handleCancel = () => {
if (props.selectedDefect) {
//
Object.assign(defectForm, {
defectCode: props.selectedDefect.defectCode || '',
defectName: props.selectedDefect.defectName || '',
defectType: props.selectedDefect.defectType || '',
defectPosition: props.selectedDefect.defectPosition || '',
defectLevel: props.selectedDefect.defectLevel || '',
repairStatus: props.selectedDefect.repairStatus || '',
axial: props.selectedDefect.axial || 0,
chordwise: props.selectedDefect.chordwise || 0,
area: calculateArea(props.selectedDefect.axial || 0, props.selectedDefect.chordwise || 0),
description: props.selectedDefect.description || '',
repairIdea: props.selectedDefect.repairIdea || ''
})
}
}
//
const handleSelectFromStandardDescription = () => {
standardDescriptionModalVisible.value = true
selectedStandardDescription.value = null
}
const handleConfirmStandardDescription = () => {
if (selectedStandardDescription.value) {
defectForm.description = selectedStandardDescription.value.content
Message.success('已选择标准描述')
}
standardDescriptionModalVisible.value = false
}
const handleCancelStandardDescription = () => {
standardDescriptionModalVisible.value = false
selectedStandardDescription.value = null
}
//
const handleSelectFromStandardInfo = () => {
standardInfoModalVisible.value = true
selectedStandardInfo.value = null
}
const handleConfirmStandardInfo = () => {
if (selectedStandardInfo.value) {
defectForm.repairIdea = selectedStandardInfo.value.content
Message.success('已选择标准维修建议')
}
standardInfoModalVisible.value = false
}
const handleCancelStandardInfo = () => {
standardInfoModalVisible.value = false
selectedStandardInfo.value = null
}
</script>
<style scoped lang="scss">
.defect-details-panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0; /* 确保flex子项可以收缩 */
background: #f8f9fa;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
background: white;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
}
.defect-detail-form {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.form-container {
flex: 1;
padding: 20px;
overflow-y: auto;
background: white;
}
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-group {
flex: 1;
&.full-width {
flex: none;
width: 100%;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
}
.size-input-group {
display: flex;
align-items: center;
gap: 4px;
.unit {
font-size: 12px;
color: #6b7280;
margin-left: 4px;
}
}
.action-button-container {
margin-top: 8px;
display: flex;
justify-content: flex-end;
.standard-library-btn {
font-size: 12px;
height: 28px;
border: 1px solid #d1d5db;
background: #f9fafb;
color: #6b7280;
&:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
}
}
.form-actions {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
background: white;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.no-defect-selected {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #6b7280;
background: white;
margin: 20px;
border-radius: 8px;
border: 2px dashed #d1d5db;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #9ca3af;
}
p {
margin: 0;
font-size: 14px;
}
}
.standard-library-content {
max-height: 400px;
overflow-y: auto;
.clickable-item {
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f9fafb;
}
&.selected {
background-color: #eff6ff;
border-left: 3px solid #3b82f6;
}
}
.description-item {
width: 100%;
.description-title {
font-weight: 500;
color: #1f2937;
margin-bottom: 4px;
}
.description-content {
font-size: 14px;
color: #6b7280;
line-height: 1.4;
}
}
}
// Arco Design
:deep(.arco-input-number) {
.arco-input-inner {
text-align: center;
}
}
:deep(.arco-select) {
width: 100%;
}
:deep(.arco-input),
:deep(.arco-textarea) {
width: 100%;
}
:deep(.arco-list-item) {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
</style>

View File

@ -0,0 +1,331 @@
<template>
<div class="defect-list-panel">
<div class="panel-header">
<h2>缺陷管理</h2>
<div class="header-actions">
<a-button
type="primary"
size="small"
@click="handleAddDefect"
:disabled="!canAddDefect"
>
<template #icon><icon-plus /></template>
新增缺陷
</a-button>
<a-button type="text" @click="$emit('close')">
<template #icon><icon-close /></template>
</a-button>
</div>
</div>
<div class="panel-content">
<!-- 项目树形结构 -->
<div class="tree-section">
<h3>项目管理</h3>
<div class="project-tree">
<a-tree
:data="enhancedTreeData"
:selected-keys="selectedKeys"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
:load-more="handleLoadMore"
:show-line="true"
@select="handleNodeSelect"
>
<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 />
</span>
<span class="node-title">{{ node.name }}</span>
<span v-if="node.imageCount" class="node-count">({{ node.imageCount }})</span>
<span v-if="node.defectCount" class="defect-count">[{{ node.defectCount }}]</span>
</div>
</template>
</a-tree>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { IconClose, IconBug, IconFolder, IconPlus, IconSettings, IconTool, IconApps } from '@arco-design/web-vue/es/icon'
import type { PropType } from 'vue'
import { ref, computed, watch } from 'vue'
//
export interface TreeNode {
id: string
name: string
type: string
imageCount?: number
defectCount?: number
defectLevel?: string
defectType?: string
detectionDate?: string
defectData?: DefectInfo
children?: TreeNode[]
}
//
export interface DefectInfo {
id: string
defectName?: string
defectLevel?: string
defectType?: string
defectPosition?: string
detectionDate?: string
description?: string
repairStatus?: string
labelInfo?: string
turbineId?: string
[key: string]: any
}
//
const props = defineProps({
//
defectList: {
type: Array as PropType<DefectInfo[]>,
default: () => []
},
//
selectedDefect: {
type: Object as PropType<DefectInfo | null>,
default: null
},
//
treeData: {
type: Array as PropType<TreeNode[]>,
default: () => []
},
//
selectedKeys: {
type: Array as PropType<string[]>,
default: () => []
},
//
loading: {
type: Boolean,
default: false
}
})
//
const emit = defineEmits([
'defect-select',
'node-select',
'load-more',
'add-defect',
'turbine-select',
'close'
])
//
const enhancedTreeData = computed(() => {
if (!props.treeData || !Array.isArray(props.treeData)) {
return []
}
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
]
} else if (node.children) {
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
)
//
return turbineDefects.map(defect => ({
id: defect.id,
name: defect.defectName || '未命名缺陷',
type: 'defect',
defectLevel: defect.defectLevel,
defectType: defect.defectType,
detectionDate: defect.detectionDate,
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)
if (defect) {
emit('defect-select', defect)
}
}
emit('node-select', selectedKeys, e)
}
//
const handleLoadMore = (node: any) => {
emit('load-more', node)
}
//
const handleAddDefect = () => {
if (canAddDefect.value) {
emit('add-defect')
}
}
//
const canAddDefect = computed(() => {
//
return props.selectedKeys && props.selectedKeys.length > 0
})
</script>
<style scoped lang="scss">
.defect-list-panel {
display: flex;
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;
border-radius: 8px;
margin: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
overflow: hidden;
display: flex;
flex-direction: column;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1d2129;
padding: 12px 16px;
border-bottom: 1px solid #f2f3f5;
background: white;
}
.project-tree {
flex: 1;
padding: 16px;
overflow-y: auto;
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;
.tree-node {
display: flex;
align-items: center;
gap: 6px;
.node-icon {
color: #6b7280;
}
.node-title {
flex: 1;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-count {
font-size: 12px;
color: #6b7280;
flex-shrink: 0;
}
.defect-count {
font-size: 12px;
color: #ff4d4f;
flex-shrink: 0;
font-weight: 600;
}
}
}
}
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="header-toolbar">
<div class="toolbar-buttons">
<a-button size="large" @click="handleAutoAnnotate" :disabled="!currentImageId">
<template #icon><icon-robot /></template>
自动标注
</a-button>
<a-button size="large" @click="handleManualAnnotate" :disabled="!currentImageId">
<template #icon><icon-edit /></template>
手动标注
</a-button>
<a-button size="large" @click="handleGenerateReport">
<template #icon><icon-file-image /></template>
生成检测报告
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
import {
IconPlayArrow,
IconRobot,
IconEdit,
IconFileImage
} from '@arco-design/web-vue/es/icon'
defineProps<{
currentImageId: string
}>()
const emit = defineEmits<{
start: []
autoAnnotate: []
manualAnnotate: []
generateReport: []
}>()
const handleStart = () => {
emit('start')
}
const handleAutoAnnotate = () => {
emit('autoAnnotate')
}
const handleManualAnnotate = () => {
emit('manualAnnotate')
}
const handleGenerateReport = () => {
emit('generateReport')
}
</script>
<style scoped lang="scss">
.header-toolbar {
background: #1e3a8a;
padding: 12px 20px;
border-bottom: 1px solid #1e40af;
.toolbar-buttons {
display: flex;
gap: 12px;
align-items: center;
.arco-btn {
font-weight: 500;
border-radius: 6px;
&.arco-btn-primary {
background: #3b82f6;
border-color: #3b82f6;
&:hover {
background: #2563eb;
border-color: #2563eb;
}
}
&:not(.arco-btn-primary) {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: white;
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
}
}
}
}
</style>

View File

@ -0,0 +1,359 @@
<template>
<div class="right-preview">
<div class="image-preview-area">
<!-- 缩放控制工具栏 -->
<div v-if="selectedImage" class="zoom-controls">
<a-space>
<a-button size="small" @click="zoomOut" :disabled="scale <= MIN_SCALE">
<template #icon>
<icon-minus />
</template>
</a-button>
<span class="zoom-text">{{ Math.round(scale * 100) }}%</span>
<a-button size="small" @click="zoomIn" :disabled="scale >= MAX_SCALE">
<template #icon>
<icon-plus />
</template>
</a-button>
<a-button size="small" @click="resetZoom">
<template #icon>
<icon-refresh />
</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"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
>
<div
class="image-container"
:style="{
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
transition: isDragging ? 'none' : 'transform 0.2s ease-out'
}"
>
<img
:src="getImageUrl(selectedImage.imagePath)"
:alt="selectedImage.imageName"
draggable="false"
@load="onImageLoad"
/>
</div>
</div>
<div v-else class="empty-preview">
<icon-image class="empty-icon" />
<p>请从下方缩略图中选择图像</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { IconImage, IconMinus, IconPlus, IconRefresh } from '@arco-design/web-vue/es/icon'
const props = defineProps<{
selectedImage: any | null
}>()
//
const scale = ref(1)
const translateX = ref(0)
const translateY = ref(0)
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const initialTranslateX = ref(0)
const initialTranslateY = ref(0)
//
const imageWidth = ref(0)
const imageHeight = ref(0)
const containerWidth = ref(0)
const containerHeight = ref(0)
//
const MIN_SCALE = 0.1
const MAX_SCALE = 5
const SCALE_STEP = 0.1
//
watch(() => props.selectedImage, () => {
resetZoom()
})
//
const resetZoom = () => {
scale.value = 1
translateX.value = 0
translateY.value = 0
}
//
const zoomIn = () => {
const newScale = Math.min(MAX_SCALE, scale.value + SCALE_STEP)
scale.value = newScale
}
//
const zoomOut = () => {
const newScale = Math.max(MIN_SCALE, scale.value - SCALE_STEP)
scale.value = newScale
// 1
if (newScale <= 1) {
translateX.value = 0
translateY.value = 0
}
}
//
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) {
containerWidth.value = container.clientWidth
containerHeight.value = container.clientHeight
}
})
}
//
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
}
}
//
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))
}
//
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
translateY.value = 0
} else {
//
applyBoundaryLimits()
}
}
}
//
const handleMouseDown = (event: MouseEvent) => {
event.preventDefault()
isDragging.value = true
dragStartX.value = event.clientX
dragStartY.value = event.clientY
initialTranslateX.value = translateX.value
initialTranslateY.value = translateY.value
}
//
const handleMouseMove = (event: MouseEvent) => {
if (isDragging.value) {
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()
}
}
//
const handleMouseUp = (event: MouseEvent) => {
event.preventDefault()
isDragging.value = false
}
//
const handleMouseLeave = () => {
isDragging.value = false
}
// URL
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
}
</script>
<style scoped lang="scss">
.right-preview {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
}
.zoom-controls {
padding: 8px 16px;
background: white;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
.zoom-text {
min-width: 50px;
text-align: center;
font-size: 12px;
color: #666;
}
}
.image-preview-area {
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
.preview-header {
padding: 16px;
background: white;
border-bottom: 1px solid #e5e7eb;
text-align: center;
flex-shrink: 0;
h3 {
margin: 0 0 8px 0;
font-size: 18px;
color: #1f2937;
}
p {
margin: 0;
color: #6b7280;
font-size: 14px;
}
}
.preview-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
overflow: hidden;
width: 100%;
height: 100%;
min-height: 0;
position: relative;
.image-viewer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: grab;
user-select: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&.dragging {
cursor: grabbing;
}
.image-container {
display: flex;
align-items: center;
justify-content: center;
transform-origin: center;
width: 100%;
height: 100%;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
pointer-events: none;
}
}
}
.empty-preview {
text-align: center;
color: #6b7280;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="left-panel">
<div class="panel-header">
<h3>项目管理</h3>
</div>
<div class="project-tree">
<a-tree
:data="projectTreeData"
:selected-keys="selectedKeys"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
:load-more="handleLoadMore"
:show-line="true"
@select="handleNodeSelect"
>
<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 />
</span>
<span class="node-title">{{ node.name }}</span>
<span v-if="node.imageCount" class="node-count">({{ node.imageCount }})</span>
</div>
</template>
</a-tree>
</div>
</div>
</template>
<script setup lang="ts">
import {
IconFolder,
IconSettings,
IconTool,
IconApps
} from '@arco-design/web-vue/es/icon'
import type { ProjectTreeNode } from '@/apis/industrial-image'
defineProps<{
projectTreeData: ProjectTreeNode[]
selectedKeys: string[]
}>()
const emit = defineEmits<{
nodeSelect: [keys: string[]]
loadMore: [node: ProjectTreeNode]
}>()
const handleNodeSelect = (keys: string[]) => {
emit('nodeSelect', keys)
}
const handleLoadMore = (node: ProjectTreeNode) => {
emit('loadMore', node)
}
</script>
<style scoped lang="scss">
.left-panel {
width: 280px;
background: #f8f9fa;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
height: 100%; /* 填满父容器 */
overflow: hidden;
.panel-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
background: white;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
}
.project-tree {
flex: 1;
padding: 16px;
overflow-y: auto;
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;
.tree-node {
display: flex;
align-items: center;
gap: 6px;
.node-icon {
color: #6b7280;
}
.node-title {
flex: 1;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-count {
font-size: 12px;
color: #6b7280;
flex-shrink: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,451 @@
<template>
<div class="recognition-results">
<div class="results-header">
<h3>识别结果</h3>
<div class="header-actions">
<a-button type="text" @click="handleSaveResults">
<template #icon><icon-save /></template>
保存结果
</a-button>
<a-button type="text" @click="handleExportResults">
<template #icon><icon-export /></template>
导出
</a-button>
</div>
</div>
<div class="results-content">
<!-- 识别统计 -->
<div class="statistics-section">
<div class="stat-item">
<span class="stat-label">裂纹</span>
<span class="stat-value">{{ statistics.crack }}</span>
<span class="stat-confidence">置信度: {{ statistics.crackConfidence }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">腐蚀</span>
<span class="stat-value">{{ statistics.corrosion }}</span>
<span class="stat-confidence">置信度: {{ statistics.corrosionConfidence }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">磨损</span>
<span class="stat-value">{{ statistics.wear }}</span>
<span class="stat-confidence">置信度: {{ statistics.wearConfidence }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">变形</span>
<span class="stat-value">{{ statistics.deformation }}</span>
<span class="stat-confidence">置信度: {{ statistics.deformationConfidence }}%</span>
</div>
</div>
<!-- 详细结果列表 -->
<div class="results-list">
<h4>详细结果</h4>
<div class="result-items">
<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>
</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>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
IconSave,
IconExport
} from '@arco-design/web-vue/es/icon'
import { Message } from '@arco-design/web-vue'
import type { DefectDetectionResult } from '@/apis/industrial-image/defect'
const props = defineProps<{
results: DefectDetectionResult[]
isProcessing: boolean
}>()
const emit = defineEmits<{
resultSelect: [index: number]
saveResults: [results: DefectDetectionResult[]]
exportResults: [results: DefectDetectionResult[]]
}>()
const selectedResultIndex = ref(-1)
//
const statistics = computed(() => {
const stats = {
crack: 0,
crackConfidence: 0,
corrosion: 0,
corrosionConfidence: 0,
wear: 0,
wearConfidence: 0,
deformation: 0,
deformationConfidence: 0
}
if (props.results.length === 0) return stats
//
const typeMapping: Record<string, string> = {
bmlw: 'crack', // ->
hfms: 'wear', // ->
mpps: 'deformation', // ->
bcps: 'deformation', // ->
jbps: 'deformation', // ->
ljss: 'corrosion', // ->
bcpf: 'deformation', // ->
fbps: 'deformation', // ->
taps: 'corrosion', // ->
}
//
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'] || []
stats.crack = crackResults.length
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'] || []
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'] || []
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'] || []
stats.deformation = deformationResults.length
stats.deformationConfidence = deformationResults.length > 0
? Math.round(deformationResults.reduce((sum, r) => sum + ((r.markInfo?.confidence || 0) * 100), 0) / deformationResults.length)
: 0
return stats
})
//
const getDefectTypeName = (type: string): string => {
const typeMap: Record<string, string> = {
crack: '裂纹',
corrosion: '腐蚀',
wear: '磨损',
deformation: '变形',
scratch: '划痕',
hole: '孔洞',
dirt: '污垢',
//
bmlw: '表面裂纹',
hfms: '合缝磨损',
mpps: '蒙皮破损',
bcps: '布层破损',
jbps: '局部破损',
ljss: '累计损伤',
bcpf: '布层剥落',
fbps: '腹部破损',
taps: '涂层破损',
//
}
return typeMap[type] || type
}
//
const getDefectTypeClass = (type: string): string => {
const classMap: Record<string, string> = {
crack: 'defect-crack',
corrosion: 'defect-corrosion',
wear: 'defect-wear',
deformation: 'defect-deformation',
scratch: 'defect-scratch',
hole: 'defect-hole',
dirt: 'defect-dirt',
//
bmlw: 'defect-crack', // ->
hfms: 'defect-wear', // ->
mpps: 'defect-deformation', // ->
bcps: 'defect-deformation', // ->
jbps: 'defect-deformation', // ->
ljss: 'defect-corrosion', // ->
bcpf: 'defect-deformation', // ->
fbps: 'defect-deformation', // ->
taps: 'defect-corrosion', // ->
}
return classMap[type] || 'defect-default'
}
//
const getRecommendation = (type: string, confidence: number): string => {
if (confidence < 60) return '建议人工复核'
const recommendations: Record<string, string> = {
crack: '立即维修,防止扩散',
corrosion: '清洁并涂保护层',
wear: '定期监测,必要时更换',
deformation: '检查结构完整性',
scratch: '轻微处理即可',
hole: '立即修补',
dirt: '清洁处理'
}
return recommendations[type] || '建议进一步检查'
}
//
const formatPosition = (bbox: number[] | undefined): string => {
if (!bbox || bbox.length < 2) return '未知位置'
return `(${bbox[0]?.toFixed(0)}, ${bbox[1]?.toFixed(0)})`
}
//
const formatSize = (bbox: number[] | undefined): string => {
if (!bbox || bbox.length < 4) return '未知尺寸'
return `${bbox[2]?.toFixed(0)} × ${bbox[3]?.toFixed(0)}`
}
//
const handleResultSelect = (index: number) => {
selectedResultIndex.value = index
emit('resultSelect', index)
}
//
const handleSaveResults = () => {
emit('saveResults', props.results)
Message.success('结果已保存')
}
//
const handleExportResults = () => {
emit('exportResults', props.results)
Message.success('结果已导出')
}
</script>
<style scoped lang="scss">
.recognition-results {
background: white;
height: 100%;
display: flex;
flex-direction: column;
.results-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.header-actions {
display: flex;
gap: 8px;
}
}
.results-content {
flex: 1;
padding: 20px;
overflow-y: auto;
.statistics-section {
margin-bottom: 24px;
.stat-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 8px;
.stat-label {
font-weight: 500;
color: #374151;
min-width: 60px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #1f2937;
min-width: 30px;
}
.stat-confidence {
font-size: 12px;
color: #6b7280;
margin-left: auto;
}
}
}
.results-list {
h4 {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.result-items {
.result-item {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
&.active {
border-color: #3b82f6;
background: #f0f9ff;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
}
.result-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.result-type {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.defect-crack {
background: #fee2e2;
color: #dc2626;
}
&.defect-corrosion {
background: #fef3c7;
color: #d97706;
}
&.defect-wear {
background: #e0e7ff;
color: #5b21b6;
}
&.defect-deformation {
background: #f3e8ff;
color: #7c3aed;
}
&.defect-scratch {
background: #ecfdf5;
color: #059669;
}
&.defect-hole {
background: #fef2f2;
color: #dc2626;
}
&.defect-dirt {
background: #f3f4f6;
color: #374151;
}
}
.result-confidence {
font-weight: 600;
color: #1f2937;
margin-left: auto;
}
}
.result-position,
.result-size {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.result-recommendation {
font-size: 12px;
color: #059669;
font-weight: 500;
margin-top: 8px;
}
}
}
}
}
//
.results-content::-webkit-scrollbar {
width: 6px;
}
.results-content::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.results-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
&:hover {
background: #94a3b8;
}
}
}
</style>

View File

@ -0,0 +1,870 @@
<template>
<div class="industrial-image-container">
<!-- 头部按钮栏 -->
<HeaderToolbar
:current-image-id="currentImageId"
@start="handleStart"
@auto-annotate="handleAutoAnnotate"
@manual-annotate="handleManualAnnotate"
@generate-report="handleGenerateReport"
/>
<!-- 主体内容区域 -->
<div class="main-content">
<!-- 上部区域 -->
<div class="top-section" :class="{
'auto-recognition-layout': isAutoRecognitionMode,
'manual-annotation-layout': isManualAnnotationMode,
'top-section-expanded': isImageListCollapsed || isAutoRecognitionMode || isManualAnnotationMode
}">
<!-- 自动识别模式三列布局 -->
<template v-if="isAutoRecognitionMode">
<!-- 左侧自动识别设置面板 -->
<div class="auto-left-panel">
<AutoRecognitionSettings
:current-image="selectedImage"
@close="handleCloseAutoRecognition"
@start-recognition="handleStartRecognition"
/>
</div>
<!-- 中间图像显示区域 -->
<div class="auto-center-panel">
<ImagePreview
:selected-image="selectedImage"
/>
</div>
<!-- 右侧识别结果区域 -->
<div class="auto-right-panel">
<RecognitionResults
:results="recognitionResults"
:is-processing="isRecognizing"
@result-select="handleRecognitionResultSelect"
@save-results="handleSaveRecognitionResults"
@export-results="handleExportRecognitionResults"
/>
</div>
</template>
<!-- 手动标注模式三列布局 -->
<template v-else-if="isManualAnnotationMode">
<!-- 左侧缺陷列表面板 -->
<div class="manual-left-panel">
<DefectListPanel
:defect-list="defectList"
:selected-defect="selectedDefect"
:tree-data="projectTreeData"
:selected-keys="selectedKeys"
@defect-select="handleDefectSelect"
@node-select="handleNodeSelect"
@load-more="handleLoadMore"
@turbine-select="handleTurbineSelect"
@add-defect="handleAddDefectFromPanel"
@close="handleCloseManualAnnotation"
/>
</div>
<!-- 中间图像标注区域 -->
<div class="manual-center-panel">
<ImageCanvas
:selected-image="selectedImage"
:annotations="selectedDefectAnnotations"
@annotation-finish="handleAnnotationFinish"
@annotation-update="handleAnnotationUpdate"
@annotation-delete="handleAnnotationDelete"
/>
</div>
<!-- 右侧缺陷详情编辑区域 -->
<div class="manual-right-panel">
<DefectDetailsForm
v-if="currentAnnotation"
:annotation="currentAnnotation"
@close="handleCloseDefectForm"
@submit="handleDefectFormSubmit"
/>
<div v-else class="no-annotation-prompt">
<icon-bug class="prompt-icon" />
<p>请在图像上绘制矩形标注缺陷</p>
</div>
</div>
</template>
<!-- 普通模式双列布局 -->
<template v-else>
<!-- 左侧面板 -->
<div class="left-panel">
<!-- 项目管理 -->
<ProjectTree
v-if="!isManualAnnotationMode"
:project-tree-data="projectTreeData"
:selected-keys="selectedKeys"
@node-select="handleNodeSelect"
@load-more="handleLoadMore"
/>
<!-- 手动标注 -->
<ManualAnnotation
v-if="isManualAnnotationMode"
:tree-data="projectTreeData"
:selected-keys="selectedKeys"
:defect-list="defectList"
:selected-defect="selectedDefect"
@close="handleCloseManualAnnotation"
@defect-select="handleDefectSelect"
@add-defect="handleAddDefect"
@edit-defect="handleEditDefect"
@delete-defect="handleDeleteDefectById"
/>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 图像预览区域 -->
<ImagePreview
:selected-image="selectedImage"
/>
</div>
</template>
</div>
<!-- 下部工业图像列表区域 - 仅在非自动识别和非手动标注模式下显示 -->
<div v-if="!isAutoRecognitionMode && !isManualAnnotationMode" class="bottom-section" :class="{ 'collapsed': isImageListCollapsed }">
<IndustrialImageList
:image-list="imageList"
:selected-image-id="selectedImageId"
@import-images="handleImportImages"
@image-select="handleImageSelect"
@image-preview="handleImagePreview"
@image-process="handleImageProcess"
@image-delete="handleImageDelete"
@search="handleSearch"
@collapsed-change="handleImageListCollapse"
/>
</div>
</div>
<!-- 模态框 -->
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } 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 RecognitionResults from './components/RecognitionResults.vue'
import DefectListPanel from './components/DefectListPanel.vue'
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 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'
defineOptions({ name: 'IndustrialImageProcessing' })
// /
const isImageListCollapsed = ref(false)
const handleImageListCollapse = (collapsed: boolean) => {
isImageListCollapsed.value = collapsed
}
//
const currentAnnotation = ref<Annotation | null>(null)
//
const defectTypes = ref<DefectType[]>([])
//
const defectLevels = ref<DefectLevelType[]>([])
//
const loadDefectTypes = async () => {
try {
const response = await getDefectTypes()
if (response.data && response.data.code === 0) {
defectTypes.value = response.data.data || []
}
} catch (error) {
console.error('加载缺陷类型失败:', error)
defectTypes.value = []
}
}
//
const loadDefectLevels = async () => {
try {
const response = await getDefectLevels()
if (response.data && response.data.code === 0) {
defectLevels.value = response.data.data || []
}
} catch (error) {
console.error('加载缺陷等级失败:', error)
defectLevels.value = []
}
}
// 使
const {
//
projectTreeData,
selectedKeys,
currentProjectId,
imageList,
selectedImage,
selectedImageId,
searchKeyword,
previewModalVisible,
previewImage,
processModalVisible,
processImage,
importModalVisible,
annotationModalVisible,
loading,
currentImageId,
//
importWizardVisible,
selectedTurbineId,
currentTurbineParts,
//
isAutoRecognitionMode,
recognitionResults,
isRecognizing,
//
isManualAnnotationMode,
defectList,
selectedDefect,
selectedDefectAnnotations,
//
reportGenerationVisible,
reportUnitData,
//
loadProjectTree,
handleLoadMore,
handleNodeSelect,
loadImageList,
handleImageSelect,
handleImagePreview,
handleImageProcess,
handleImageDelete,
handleSearch,
handleStart,
handleImportImages,
handleAutoAnnotate,
handleManualAnnotate,
handleGenerateReport,
handleImportSuccess,
handleAnnotationSaved,
handleProcessSuccess,
handleWizardImportSuccess,
//
handleCloseAutoRecognition,
handleStartRecognition,
handleRecognitionResultSelect,
handleSaveRecognitionResults,
handleExportRecognitionResults,
//
handleCloseManualAnnotation,
handleDefectSelect,
handleAddDefect,
handleEditDefect,
handleDeleteDefectById,
// Canvas
handleAnnotationAdd,
handleAnnotationUpdate,
handleAnnotationDelete,
//
handleReportGenerated,
init
} = useIndustrialImage()
//
const treeData = computed(() => {
console.log('计算treeData, projectTreeData:', projectTreeData.value)
if (!projectTreeData.value || !Array.isArray(projectTreeData.value)) {
console.log('projectTreeData为空或不是数组返回测试数据')
//
return [
{
key: 'test-project-1',
title: '测试项目1',
type: 'project',
defectCount: 0,
children: [
{
key: 'test-turbine-1',
title: '测试机组1',
type: 'turbine',
defectCount: 0,
children: [
{
key: 'test-part-1',
title: '测试部件1',
type: 'part',
defectCount: 0
}
]
}
]
}
] as TreeNode[]
}
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 => {
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 => {
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
}
}).filter(Boolean) || []
}
return turbineNode
}).filter(Boolean) || []
}
console.log('创建的project节点:', projectNode)
return projectNode
}).filter(item => item !== null) as TreeNode[]
console.log('最终的treeData:', result)
return result
})
// Canvas
const handleCanvasAnnotationAdd = (annotation: Annotation) => {
currentAnnotation.value = annotation
}
//
const handleAnnotationFinish = async (annotations: Annotation[], imageBlob: Blob) => {
try {
//
if (!selectedImageId.value && !selectedImage.value?.imageId) {
Message.error('请先选择一张图像')
return
}
//
const fileName = `annotated_${selectedImage.value?.imageName || 'image'}_${Date.now()}.png`
const uploadResponse = await uploadAnnotatedImage(imageBlob, fileName)
if ( !uploadResponse.data) {
Message.error('上传标注图片失败')
return
}
const attachInfo = uploadResponse.data
console.log('上传成功,附件信息:', attachInfo)
//
const combinedAnnotation: Annotation = {
id: `multi-annotation-${Date.now()}`,
type: 'rectangle',
points: annotations[0]?.points || [{ x: 0, y: 0 }, { x: 100, y: 100 }],
color: '#ff4d4f',
label: `${annotations.length}个标注区域`,
metadata: {
isMultiAnnotation: true,
attachId: attachInfo,
// attachPath: attachInfo.attachPath,
// attachInfo: attachInfo,
allAnnotations: annotations
}
}
//
currentAnnotation.value = combinedAnnotation
Message.success(`成功绘制${annotations.length}个区域,请填写缺陷详情`)
} catch (error) {
console.error('处理标注完成失败:', error)
Message.error('处理标注失败')
}
}
//
const handleCloseDefectForm = () => {
currentAnnotation.value = null
}
//
const handleAddDefectFromPanel = () => {
//
if (!selectedKeys.value || selectedKeys.value.length === 0) {
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 }
],
color: '#ff4d4f',
label: '手动添加的缺陷'
}
//
currentAnnotation.value = virtualAnnotation
Message.info('请在右侧表单中填写缺陷详情')
}
//
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
}))
} 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
}))
}
//
defectList.value = defects
console.log('查询到缺陷列表:', defects.length, '条记录')
} else {
console.error('加载机组缺陷列表失败:', response)
defectList.value = []
}
} catch (error) {
console.error('加载机组缺陷列表出错:', error)
Message.error('加载缺陷列表失败')
defectList.value = []
}
}
//
const loadDefectList = async () => {
try {
//
if (!selectedImageId.value && !selectedImage.value?.imageId) {
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
}))
} 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
}))
}
//
defectList.value = defects
} else {
console.error('加载缺陷列表失败:', response)
defectList.value = []
}
} catch (error) {
console.error('加载缺陷列表出错:', error)
Message.error('加载缺陷列表失败')
defectList.value = []
}
}
//
const handleDefectFormSubmit = async (formData: any, annotation: Annotation) => {
try {
//
if (!selectedImageId.value && !selectedImage.value?.imageId) {
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,
attachPath: '',
// axial: 0,
// chordwise: 0,
// defectCode: `MANUAL_${Date.now()}`,
defectId: '',
defectLevel: formData.defectLevel,
defectLevelLabel: formData.defectLevelLabel || getDefectLevelLabel(formData.defectLevel),
defectName: formData.defectName,
defectPosition: formData.defectPosition,
defectType: formData.defectType,
defectTypeLabel: formData.defectTypeLabel || getDefectTypeLabel(formData.defectType),
description: formData.description,
detectionDate: new Date().toISOString().split('T')[0],
imageId: imageId,
labelInfo: '',
// labelInfo: isMultiAnnotation ?
// JSON.stringify(annotation.metadata?.allAnnotations || []) :
// JSON.stringify(annotation),
markInfo: {
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.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
},
repairIdea: formData.repairIdea,
repairStatus: 'PENDING',
repairStatusLabel: '待处理',
source: 'MANUAL',
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
})
} catch (error) {
console.error('添加缺陷失败:', error)
Message.clear()
Message.error('保存缺陷失败,请重试')
}
}
//
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 => {
if (annotation.points && annotation.points.length >= 2) {
const [p1, p2] = annotation.points
minX = Math.min(minX, p1.x, p2.x)
minY = Math.min(minY, p1.y, p2.y)
maxX = Math.max(maxX, p1.x, p2.x)
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)
return level ? level.name : levelCode
}
//
const getDefectTypeLabel = (typeCode: string): string => {
const type = defectTypes.value.find(t => t.code === typeCode)
return type ? type.name : typeCode
}
//
onMounted(() => {
init()
loadDefectTypes()
loadDefectLevels()
})
</script>
<style scoped lang="scss">
.industrial-image-container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
position: relative;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.top-section {
display: flex;
flex: 1; /* 改为flex布局自适应高度 */
border-bottom: 1px solid #e5e7eb;
min-height: 400px; /* 最小高度,确保有足够空间 */
max-height: calc(100vh - 100px - 300px); /* 最大高度,减去头部高度和底部默认高度 */
transition: max-height 0.3s ease, flex 0.3s ease;
&.top-section-expanded {
max-height: 100%; /* 当底部收起时,高度更大 */
}
.left-panel {
width: 300px;
border-right: 1px solid #e5e7eb;
overflow: visible; /* 允许滚动条显示 */
display: flex;
flex-direction: column;
}
.right-panel {
flex: 1;
overflow: hidden;
}
}
/* 自动识别模式三列布局 */
.top-section.auto-recognition-layout {
display: flex;
height: 100%; /* 自动识别模式占据全部高度 */
.auto-left-panel {
width: 280px;
border-right: 1px solid #e5e7eb;
overflow: visible;
display: flex;
flex-direction: column;
background: #f8f9fa;
flex-shrink: 0;
}
.auto-center-panel {
flex: 1;
border-right: 1px solid #e5e7eb;
overflow: hidden;
display: flex;
flex-direction: column;
background: #ffffff;
position: relative;
min-width: 0; /* 确保flex项目可以收缩 */
/* 让图像区域居中显示 */
align-items: center;
justify-content: center;
}
.auto-right-panel {
width: 320px;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f8f9fa;
flex-shrink: 0;
}
}
/* 手动标注模式三列布局 */
.top-section.manual-annotation-layout {
display: flex;
height: 100%; /* 手动标注模式占据全部高度 */
.manual-left-panel {
width: 280px;
border-right: 1px solid #e5e7eb;
overflow: visible;
display: flex;
flex-direction: column;
background: #f8f9fa;
flex-shrink: 0;
}
.manual-center-panel {
flex: 1;
border-right: 1px solid #e5e7eb;
overflow: hidden;
display: flex;
flex-direction: column;
background: #ffffff;
position: relative;
min-width: 0; /* 确保flex项目可以收缩 */
/* 让图像区域居中显示 */
align-items: center;
justify-content: center;
}
.manual-right-panel {
width: 320px;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f8f9fa;
flex-shrink: 0;
}
}
.bottom-section {
flex: 0 0 300px; /* 固定高度 */
display: flex;
flex-direction: column;
overflow: hidden;
transition: flex 0.3s ease, height 0.3s ease;
&.collapsed {
flex: 0 0 0;
min-height: 0;
max-height: 0;
}
}
.no-annotation-prompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #6b7280;
background: white;
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>

View File

@ -0,0 +1,498 @@
<template>
<div class="model-config-container">
<a-card title="模型配置" :bordered="false">
<a-space direction="vertical" fill :size="16">
<!-- 操作栏 -->
<div class="operation-bar">
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
新建配置
</a-button>
<a-button @click="refreshList">
<template #icon><icon-refresh /></template>
刷新
</a-button>
</a-space>
<a-space>
<!-- 搜索栏 -->
<a-input-search
v-model="searchKeyword"
placeholder="请输入模型名称或ID搜索"
search-button
@search="handleSearch"
/>
</a-space>
</div>
<!-- 列表 -->
<a-table
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
row-key="modelId"
>
<template #columns>
<!-- <a-table-column title="模型ID" data-index="modelId" /> -->
<a-table-column title="模型名称" data-index="modelName" />
<a-table-column title="模型路径" data-index="modelPath" />
<a-table-column title="置信度阈值" data-index="confThreshold">
<template #cell="{ record }">
{{ record.confThreshold ? record.confThreshold.toFixed(2) : '-' }}
</template>
</a-table-column>
<a-table-column title="NMS阈值" data-index="nmsThreshold">
<template #cell="{ record }">
{{ record.nmsThreshold ? record.nmsThreshold.toFixed(2) : '-' }}
</template>
</a-table-column>
<a-table-column title="操作" fixed="right" :width="180">
<template #cell="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleEdit(record)">
<template #icon><icon-edit /></template>
编辑
</a-button>
<a-button type="text" size="small" @click="handleView(record)">
<template #icon><icon-eye /></template>
查看
</a-button>
<a-popconfirm
title="确定要删除此配置吗?"
@ok="handleDelete(record)"
>
<a-button type="text" status="danger" size="small">
<template #icon><icon-delete /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table-column>
</template>
</a-table>
</a-space>
</a-card>
<!-- 新增/编辑表单 -->
<a-modal
v-model:visible="formVisible"
:title="isEdit ? '编辑模型配置' : '新增模型配置'"
@cancel="closeForm"
@before-ok="handleSubmit"
unmount-on-close
>
<a-form
ref="formRef"
:model="formData"
label-align="right"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
auto-label-width
>
<!-- <a-form-item
field="modelId"
label="模型ID"
:validate-trigger="['change', 'blur']"
:rules="[{ required: true, message: '请输入模型ID' }]"
>
<a-input
v-model="formData.modelId"
placeholder="请输入模型ID"
:disabled="isEdit"
/>
</a-form-item> -->
<a-form-item
field="modelName"
label="模型名称"
:validate-trigger="['change', 'blur']"
:rules="[{ required: true, message: '请输入模型名称' }]"
>
<a-input
v-model="formData.modelName"
placeholder="请输入模型名称"
/>
</a-form-item>
<a-form-item
field="attachId"
label="模型附件"
>
<a-space>
<a-upload
:custom-request="uploadModelFile"
>
<a-button type="primary">
<template #icon><icon-upload /></template>
上传模型文件
</a-button>
</a-upload>
<a-button
v-if="formData.attachId"
status="danger"
@click="formData.attachId = ''"
>
<template #icon><icon-delete /></template>
清除
</a-button>
</a-space>
<div class="upload-tip" v-if="uploadingFile">
<a-spin /> 上传中...{{ uploadProgress }}%
</div>
<div class="upload-tip success" v-if="uploadedFileName && !uploadingFile">
<icon-check-circle /> 已上传: {{ uploadedFileName }}
</div>
</a-form-item>
<a-form-item
field="confThreshold"
label="置信度阈值"
:validate-trigger="['change', 'blur']"
:rules="[
{ required: true, message: '请输入置信度阈值' },
{
validator: (value, cb) => {
if (value < 0 || value > 1) {
cb('置信度阈值必须在0-1之间');
}
}
}
]"
>
<a-input-number
v-model="formData.confThreshold"
placeholder="请输入置信度阈值"
:min="0"
:max="1"
:precision="2"
:step="0.01"
style="width: 100%;"
/>
</a-form-item>
<a-form-item
field="nmsThreshold"
label="NMS阈值"
:validate-trigger="['change', 'blur']"
:rules="[
{ required: true, message: '请输入NMS阈值' },
{
validator: (value, cb) => {
if (value < 0 || value > 1) {
cb('NMS阈值必须在0-1之间');
}
}
}
]"
>
<a-input-number
v-model="formData.nmsThreshold"
placeholder="请输入NMS阈值"
:min="0"
:max="1"
:precision="2"
:step="0.01"
style="width: 100%;"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 查看详情 -->
<a-modal
v-model:visible="detailVisible"
title="模型配置详情"
@cancel="closeDetail"
:footer="false"
unmount-on-close
>
<a-descriptions
:data="detailData"
:column="1"
title="基本信息"
:bordered="true"
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { Message } from '@arco-design/web-vue';
import {
IconPlus,
IconRefresh,
IconEdit,
IconEye,
IconDelete,
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';
defineOptions({ name: 'ModelConfig' });
//
const tableData = ref<any[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
});
//
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 uploadingFile = ref(false);
const uploadProgress = ref(0);
const uploadedFileName = ref('');
//
const fetchModelConfigList = async () => {
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;
} else {
tableData.value = [];
pagination.total = 0;
}
} catch (error) {
console.error('获取模型配置列表失败:', error);
Message.error('获取模型配置列表失败');
} finally {
loading.value = false;
}
};
//
onMounted(() => {
fetchModelConfigList();
});
//
const refreshList = () => {
fetchModelConfigList();
};
//
const handleSearch = () => {
pagination.current = 1;
fetchModelConfigList();
};
//
const onPageChange = (page: number) => {
pagination.current = page;
fetchModelConfigList();
};
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize;
fetchModelConfigList();
};
//
const handleAdd = () => {
isEdit.value = false;
resetForm();
formVisible.value = true;
};
//
const handleEdit = async (record: ModelConfigResponse) => {
isEdit.value = true;
resetForm();
try {
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;
}
} catch (error) {
console.error('获取详情失败:', error);
Message.error('获取详情失败');
}
};
//
const handleView = async (record: ModelConfigResponse) => {
try {
const res = await getModelConfigDetail(record.modelId);
if (res.data) {
const modelData = res.data.data;
detailData.value = [
// { label: 'ID', value: modelData.modelId },
{ label: '模型名称', value: modelData.modelName },
{ label: '模型路径', value: modelData.modelPath || '-' },
{ 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;
}
} catch (error) {
console.error('获取详情失败:', error);
Message.error('获取详情失败');
}
};
//
const handleDelete = async (record: ModelConfigResponse) => {
try {
await deleteModelConfig(record.modelId);
Message.success('删除成功');
fetchModelConfigList();
} catch (error) {
console.error('删除失败:', error);
Message.error('删除失败');
}
};
//
const handleSubmit = async () => {
if (!formRef.value) return false;
try {
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;
} catch (error) {
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;
};
//
const closeForm = () => {
formVisible.value = false;
resetForm();
};
//
const closeDetail = () => {
detailVisible.value = false;
detailData.value = [];
};
//
const uploadModelFile = async (options: any) => {
const {onProgress, onError, onSuccess, fileItem, name} = options;
try {
// FormData
const uploadFormData = new FormData();
uploadFormData.append(name || 'file', fileItem.file);
// API /attach-info/{businessType}
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);
} else {
Message.error('模型文件上传失败');
onError(new Error('上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
Message.error('上传失败: ' + (error as any)?.msg || '未知错误');
onError(error);
} finally {
uploadingFile.value = false;
}
};
</script>
<style scoped lang="scss">
.model-config-container {
.operation-bar {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.upload-tip {
margin-top: 8px;
color: #86909c;
font-size: 14px;
&.success {
color: #00b42a;
}
}
}
</style>

View File

@ -38,7 +38,7 @@ const IconMap: Record<number, Component> = {
const router = useRouter()
//
const back = () => {
router.replace({ path: '/' })
router.replace({ path: '/project-management/bidding/tender-documents' })
}
</script>

View File

@ -0,0 +1,484 @@
<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" />
</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>
</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="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>
</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>
</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(() => {
// 访
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]
},
{
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]
},
{
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()
}
})
})
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
.stat-card {
margin-bottom: 20px;
text-align: center;
}
</style>

View File

@ -0,0 +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'
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]
},
{
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 (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: {
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: '系统资源管理' }
]
}
]
})
}
//
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()
}
})
})
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +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>
</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>
</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(() => {
//
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]
},
{
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 (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]
},
{
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()
}
})
})
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,267 @@
<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(() => {
//
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
},
emphasis: {
focus: 'series'
},
data: [65, 72, 58, 80, 75]
},
{
name: '剩余',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
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]
},
{
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()
}
})
})
})
</script>
<style scoped>
.general-card {
margin-bottom: 20px;
}
.stat-card {
margin-bottom: 20px;
text-align: center;
}
</style>

View File

@ -101,7 +101,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
username: '',
adminType: '',
status: '',

View File

@ -54,7 +54,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
userName: '',
deptName: '',
status: '',

View File

@ -104,7 +104,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
userName: '',
deptName: '',
pointLevel: '',

View File

@ -61,7 +61,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
userName: '',
deptName: '',
performanceLevel: '',

View File

@ -0,0 +1,601 @@
<template>
<GiPageLayout>
<div class="certification-manage-container">
<div class="page-header">
<h2 class="page-title">人员资质管理</h2>
<a-button type="primary" @click="showAddModal">
<template #icon>
<icon-plus />
</template>
新增资质
</a-button>
</div>
<!-- 搜索表单 -->
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item label="证书名称" field="certificationName">
<a-input
v-model="searchForm.certificationName"
placeholder="请输入证书名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="证书类型" field="certificationType">
<a-input
v-model="searchForm.certificationType"
placeholder="请输入证书类型"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="用户姓名" field="userName">
<a-input
v-model="searchForm.userName"
placeholder="请输入用户姓名"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<icon-search />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<!-- 人员资质表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="certificationList"
:pagination="paginationConfig"
:loading="loading"
row-key="certificationId"
@page-change="handlePageChange"
>
<template #userName="{ record }">
<span>{{ getUserName(record.userId) }}</span>
</template>
<template #certificationImage="{ record }">
<a-image
v-if="record.certificationImage"
:src="record.certificationImage"
width="60"
height="40"
fit="cover"
show-loader
preview
/>
<span v-else>-</span>
</template>
<template #validityPeriod="{ record }">
<span>{{ record.validityDateBegin }} {{ record.validityDateEnd }}</span>
</template>
<template #actions="{ record }">
<a-space>
<a-button type="primary" size="small" @click="editRecord(record)">
编辑
</a-button>
<a-button type="primary" status="danger" size="small" @click="deleteRecord(record)">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 新增/编辑资质信息模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑资质信息' : '新增资质信息'"
width="700px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书编号" field="certificationCode">
<a-input v-model="formData.certificationCode" placeholder="请输入证书编号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证书名称" field="certificationName">
<a-input v-model="formData.certificationName" placeholder="请输入证书名称" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书类型" field="certificationType">
<a-input v-model="formData.certificationType" placeholder="请输入证书类型" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="持证人" field="userId">
<a-select
v-model="formData.userId"
placeholder="请选择持证人"
:loading="loadingUsers"
allow-search
:filter-option="filterUserOption"
>
<a-option
v-for="user in userList"
:key="user.userId"
:value="user.userId"
:label="user.name"
>
{{ user.name }}({{ user.account }})
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="有效期开始" field="validityDateBegin">
<a-date-picker
v-model="formData.validityDateBegin"
placeholder="请选择有效期开始日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="有效期结束" field="validityDateEnd">
<a-date-picker
v-model="formData.validityDateEnd"
placeholder="请选择有效期结束日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="证书图片" field="certificationImage">
<a-upload
:custom-request="handleUpload"
:show-file-list="false"
accept="image/*"
:before-upload="beforeUpload"
>
<template #upload-button>
<div class="upload-wrapper">
<a-image
v-if="formData.certificationImage"
:src="formData.certificationImage"
width="200"
height="120"
fit="cover"
show-loader
preview
/>
<div v-else class="upload-placeholder">
<icon-plus />
<div>点击上传证书图片</div>
</div>
</div>
</template>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus, IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import * as CertificationAPI from '@/apis/employee'
import type { CertificationInfo, CertificationListParams, SimpleUserInfo } from '@/apis/employee'
import { uploadFile } from '@/apis/common/common'
defineOptions({ name: 'HRCertification' })
//
const loading = ref(false)
const loadingUsers = ref(false)
const modalVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
//
const certificationList = ref<CertificationInfo[]>([])
const userList = ref<SimpleUserInfo[]>([])
//
const searchForm = reactive<CertificationListParams>({
certificationName: '',
certificationType: '',
userName: ''
})
//
const paginationConfig = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true
})
//
const formData = reactive<CertificationInfo>({
certificationId: '',
certificationCode: '',
certificationImage: '',
certificationName: '',
certificationType: '',
userId: '',
validityDateBegin: '',
validityDateEnd: ''
})
//
const formRules = {
certificationCode: [
{ required: true, message: '请输入证书编号' }
],
certificationName: [
{ required: true, message: '请输入证书名称' }
],
certificationType: [
{ required: true, message: '请输入证书类型' }
],
userId: [
{ required: true, message: '请选择持证人' }
],
validityDateBegin: [
{ required: true, message: '请选择有效期开始日期' }
],
validityDateEnd: [
{ required: true, message: '请选择有效期结束日期' }
]
}
//
const columns = [
{
title: '证书编号',
dataIndex: 'certificationCode',
width: 150
},
{
title: '证书名称',
dataIndex: 'certificationName',
width: 200
},
{
title: '证书类型',
dataIndex: 'certificationType',
width: 150
},
{
title: '持证人',
dataIndex: 'userId',
slotName: 'userName',
width: 120
},
{
title: '证书图片',
dataIndex: 'certificationImage',
slotName: 'certificationImage',
width: 100
},
{
title: '有效期',
dataIndex: 'validityPeriod',
slotName: 'validityPeriod',
width: 200
},
{
title: '操作',
slotName: 'actions',
width: 150,
fixed: 'right'
}
]
//
const getUserName = (userId: string) => {
const user = userList.value.find(u => u.userId === userId)
return user ? user.name : '-'
}
//
const filterUserOption = (inputValue: string, option: any) => {
const user = userList.value.find(u => u.userId === option.value)
if (!user) return false
const searchText = inputValue.toLowerCase()
return user.name.toLowerCase().includes(searchText) ||
user.account.toLowerCase().includes(searchText)
}
//
const getUserList = async () => {
try {
loadingUsers.value = true
const response = await CertificationAPI.getUserList()
console.log('用户列表响应:', response)
if (response.data) {
userList.value = response.data || []
}
} catch (error) {
console.error('获取用户列表失败:', error)
Message.error('获取用户列表失败')
} finally {
loadingUsers.value = false
}
}
//
const getCertificationList = async () => {
try {
loading.value = true
const params: CertificationListParams = {
...searchForm,
current: paginationConfig.current,
size: paginationConfig.pageSize
}
const response = await CertificationAPI.getCertificationList(params)
console.log('资质信息列表响应:', response)
if (response.data) {
certificationList.value = response.data.records || []
paginationConfig.total = response.data.total || 0
}
} catch (error) {
console.error('获取资质信息列表失败:', error)
Message.error('获取资质信息列表失败')
} finally {
loading.value = false
}
}
//
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
Message.error('只能上传图片文件')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
Message.error('图片大小不能超过 2MB')
return false
}
return true
}
//
const handleUpload = async (option: any) => {
try {
const uploadFormData = new FormData()
uploadFormData.append('file', option.fileItem.file)
const response = await uploadFile(uploadFormData)
if (response.data && response.data.url) {
formData.certificationImage = response.data.url
Message.success('图片上传成功')
option.onSuccess()
} else {
Message.error('图片上传失败')
option.onError()
}
} catch (error) {
console.error('图片上传失败:', error)
Message.error('图片上传失败')
option.onError()
}
}
//
const handleSearch = () => {
paginationConfig.current = 1
getCertificationList()
}
//
const handleReset = () => {
Object.assign(searchForm, {
certificationName: '',
certificationType: '',
userName: ''
})
paginationConfig.current = 1
getCertificationList()
}
//
const handlePageChange = (page: number) => {
paginationConfig.current = page
getCertificationList()
}
//
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
}
//
const editRecord = (record: CertificationInfo) => {
isEdit.value = true
Object.assign(formData, record)
modalVisible.value = true
}
//
const deleteRecord = (record: CertificationInfo) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这条资质信息吗?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await CertificationAPI.deleteCertification(record.certificationId!)
Message.success('删除成功')
await getCertificationList()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
})
}
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (isEdit.value) {
await CertificationAPI.updateCertification(formData.certificationId!, formData)
Message.success('更新成功')
} else {
await CertificationAPI.createCertification(formData)
Message.success('创建成功')
}
modalVisible.value = false
resetForm()
await getCertificationList()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败,请重试')
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(formData, {
certificationId: '',
certificationCode: '',
certificationImage: '',
certificationName: '',
certificationType: '',
userId: '',
validityDateBegin: '',
validityDateEnd: ''
})
formRef.value?.resetFields()
}
//
const init = async () => {
await Promise.all([
getUserList(),
getCertificationList()
])
}
//
onMounted(() => {
init()
})
</script>
<style scoped>
.certification-manage-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.search-card {
margin-bottom: 20px;
}
.table-card {
margin-bottom: 20px;
}
.upload-wrapper {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
}
.upload-wrapper:hover {
border-color: #40a9ff;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 200px;
height: 120px;
background-color: #fafafa;
color: #999;
}
.upload-placeholder .arco-icon {
font-size: 24px;
margin-bottom: 8px;
}
</style>

View File

@ -71,7 +71,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
userName: '',
deptName: '',
salaryMonth: '',

View File

@ -0,0 +1,309 @@
<template>
<GiPageLayout>
<div class="health-records-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">健康档案</h2>
</div>
<!-- 2023年度体检报告 -->
<a-card class="health-report-card" :bordered="false">
<template #title>
<div class="card-header">
<span class="card-title">2023年度体检报告</span>
<a-tag color="green">正常</a-tag>
</div>
</template>
<div class="report-meta">
<span class="meta-item">体检日期2023-10-15</span>
<span class="meta-item">有效期至2024-10-15</span>
</div>
<div class="report-content">
<!-- 基本信息 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<div class="info-grid">
<div class="info-item">
<span class="info-label">体检医院</span>
<span class="info-value">北京协和医院</span>
</div>
<div class="info-item">
<span class="info-label">体检套餐</span>
<span class="info-value">高级管理人员套餐</span>
</div>
<div class="info-item">
<span class="info-label">总评</span>
<span class="info-value">健康状况良好无明显异常</span>
</div>
<div class="info-item">
<span class="info-label">建议</span>
<span class="info-value">保持规律作息适量运动控制饮食</span>
</div>
</div>
</div>
<!-- 检验结果和检查结果 -->
<div class="results-section">
<div class="results-grid">
<!-- 检验结果 -->
<div class="result-column">
<h4 class="section-title">检验结果</h4>
<div class="result-item">
<span class="result-label">血常规</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">尿常规</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">肝功能</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">肾功能</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">血脂</span>
<span class="result-value">总胆固醇 5.2 mmol/L</span>
<a-tag color="orange" size="small">偏高</a-tag>
</div>
<div class="result-item">
<span class="result-label">血糖</span>
<span class="result-value">5.1 mmol/L (正常)</span>
</div>
</div>
<!-- 检查结果 -->
<div class="result-column">
<h4 class="section-title">检查结果</h4>
<div class="result-item">
<span class="result-label">心电图</span>
<span class="result-value">窦性心律正常心电图</span>
</div>
<div class="result-item">
<span class="result-label">胸部X光</span>
<span class="result-value">心肺膈未见明显异常</span>
</div>
<div class="result-item">
<span class="result-label">腹部B超</span>
<span class="result-value">肝胆胰脾未见明显异常</span>
</div>
<div class="result-item">
<span class="result-label">眼科检查</span>
<span class="result-value">视力: 左眼1.0右眼1.2</span>
<a-tag color="orange" size="small">需要保护</a-tag>
</div>
</div>
</div>
</div>
</div>
</a-card>
<!-- 2022年度体检报告 -->
<a-card class="health-report-card" :bordered="false">
<template #title>
<div class="card-header">
<span class="card-title">2022年度体检报告</span>
<a-tag color="green">正常</a-tag>
</div>
</template>
<div class="report-meta">
<span class="meta-item">体检日期2022-10-20</span>
<span class="meta-item">有效期至2023-10-20</span>
</div>
<div class="report-content">
<!-- 基本信息 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<div class="info-grid">
<div class="info-item">
<span class="info-label">体检医院</span>
<span class="info-value">北京协和医院</span>
</div>
<div class="info-item">
<span class="info-label">体检套餐</span>
<span class="info-value">高级管理人员套餐</span>
</div>
<div class="info-item">
<span class="info-label">总评</span>
<span class="info-value">健康状况良好</span>
</div>
<div class="info-item">
<span class="info-label">建议</span>
<span class="info-value">注意控制体重减少高脂饮食</span>
</div>
</div>
</div>
<!-- 检验结果 -->
<div class="results-section">
<div class="results-grid">
<div class="result-column">
<h4 class="section-title">检验结果</h4>
<div class="result-item">
<span class="result-label">血常规</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">尿常规</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">肝功能</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">肾功能</span>
<span class="result-value">正常</span>
</div>
<div class="result-item">
<span class="result-label">血脂</span>
<span class="result-value">总胆固醇 5.0 mmol/L</span>
<a-tag color="orange" size="small">轻度</a-tag>
</div>
<div class="result-item">
<span class="result-label">血糖</span>
<span class="result-value">5.0 mmol/L (正常)</span>
</div>
</div>
</div>
</div>
</div>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.health-records-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.health-report-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
.report-meta {
display: flex;
gap: 20px;
margin-bottom: 20px;
color: #4e5969;
font-size: 14px;
}
.meta-item {
font-weight: 500;
}
.report-content {
padding: 20px 0;
}
.info-section {
margin-bottom: 30px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 15px;
border-bottom: 2px solid #e5e6eb;
padding-bottom: 8px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: flex-start;
}
.info-label {
font-weight: 600;
width: 100px;
flex-shrink: 0;
color: #1d2129;
}
.info-value {
color: #4e5969;
word-break: break-word;
}
.results-section {
margin-bottom: 20px;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.result-column {
background: #f7f8fa;
padding: 20px;
border-radius: 8px;
}
.result-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.result-label {
font-weight: 600;
width: 80px;
flex-shrink: 0;
color: #1d2129;
}
.result-value {
color: #4e5969;
flex: 1;
}
</style>

View File

@ -0,0 +1,209 @@
<template>
<GiPageLayout>
<div class="my-insurance-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">我的保险信息</h2>
</div>
<!-- 保险信息表格 -->
<a-card class="insurance-table-card" :bordered="false">
<a-table
:columns="insuranceColumns"
:data="insuranceData"
:pagination="false"
row-key="id"
>
<template #type="{ record }">
<a-tag
:color="record.type === '医疗保险' ? 'blue' : record.type === '意外险' ? 'green' : 'orange'"
>
{{ record.type }}
</a-tag>
</template>
<template #status="{ record }">
<a-tag
:color="record.status === '有效' ? 'green' : 'red'"
>
{{ record.status }}
</a-tag>
</template>
<template #action="{ record }">
<a-button
type="primary"
size="small"
@click="handleViewDetails(record)"
>
查看详情
</a-button>
</template>
</a-table>
</a-card>
<!-- 查看详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="保险详情"
width="800px"
>
<div class="detail-content" v-if="selectedRecord">
<div class="detail-item">
<span class="detail-label">保险公司</span>
<span class="detail-value">{{ selectedRecord.company }}</span>
</div>
<div class="detail-item">
<span class="detail-label">保险类型</span>
<span class="detail-value">{{ selectedRecord.type }}</span>
</div>
<div class="detail-item">
<span class="detail-label">保单号</span>
<span class="detail-value">{{ selectedRecord.policyNo }}</span>
</div>
<div class="detail-item">
<span class="detail-label">生效日期</span>
<span class="detail-value">{{ selectedRecord.startDate }}</span>
</div>
<div class="detail-item">
<span class="detail-label">到期日期</span>
<span class="detail-value">{{ selectedRecord.endDate }}</span>
</div>
<div class="detail-item">
<span class="detail-label">状态</span>
<span class="detail-value">{{ selectedRecord.status }}</span>
</div>
<div class="detail-item">
<span class="detail-label">保险金额</span>
<span class="detail-value">{{ selectedRecord.amount }}</span>
</div>
<div class="detail-item">
<span class="detail-label">保险范围</span>
<span class="detail-value">{{ selectedRecord.coverage }}</span>
</div>
<div class="detail-item">
<span class="detail-label">受益人</span>
<span class="detail-value">{{ selectedRecord.beneficiary }}</span>
</div>
<div class="detail-item">
<span class="detail-label">备注</span>
<span class="detail-value">{{ selectedRecord.remark }}</span>
</div>
</div>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
//
const insuranceColumns = [
{ title: '保险公司', dataIndex: 'company', width: 150 },
{ title: '保险类型', dataIndex: 'type', slotName: 'type', width: 120 },
{ title: '保单号', dataIndex: 'policyNo', width: 150 },
{ title: '生效日期', dataIndex: 'startDate', width: 120 },
{ title: '到期日期', dataIndex: 'endDate', width: 120 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '操作', slotName: 'action', width: 120 }
]
//
const insuranceData = ref([
{
id: 1,
company: '中国人寿',
type: '医疗保险',
policyNo: 'CL12345678',
startDate: '2023-01-01',
endDate: '2023-12-31',
status: '有效',
amount: '50万元',
coverage: '住院医疗、门诊医疗、重大疾病',
beneficiary: '法定继承人',
remark: '企业统一购买,覆盖基本医疗保险'
},
{
id: 2,
company: '平安保险',
type: '意外险',
policyNo: 'PA87654321',
startDate: '2023-01-01',
endDate: '2023-12-31',
status: '有效',
amount: '100万元',
coverage: '意外伤害、意外医疗、交通意外',
beneficiary: '法定继承人',
remark: '个人购买,工作期间意外保障'
},
{
id: 3,
company: '泰康保险',
type: '养老保险',
policyNo: 'TK33445566',
startDate: '2022-01-01',
endDate: '2042-12-31',
status: '有效',
amount: '200万元',
coverage: '养老金、年金给付',
beneficiary: '法定继承人',
remark: '企业年金计划,长期养老保障'
}
])
//
const detailModalVisible = ref(false)
const selectedRecord = ref<any>(null)
//
const handleViewDetails = (record: any) => {
selectedRecord.value = record
detailModalVisible.value = true
}
</script>
<style scoped>
.my-insurance-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.insurance-table-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.detail-content {
padding: 20px;
}
.detail-item {
display: flex;
margin-bottom: 16px;
align-items: flex-start;
}
.detail-label {
font-weight: 600;
width: 120px;
flex-shrink: 0;
color: #1d2129;
}
.detail-value {
color: #4e5969;
word-break: break-word;
}
</style>

View File

@ -0,0 +1,243 @@
<template>
<GiPageLayout>
<div class="insurance-overview-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">工作台概览</h2>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ insuranceStats.total }}</div>
<div class="stat-label">我的保险</div>
<div class="stat-status success">全部有效</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ insuranceStats.lastCheckup }}</div>
<div class="stat-label">最近体检</div>
<div class="stat-status normal">正常</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ insuranceStats.expiringCount }}</div>
<div class="stat-label">即将到期</div>
<div class="stat-status warning">医疗保险</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ insuranceStats.healthScore }}</div>
<div class="stat-label">健康评分</div>
<div class="stat-status success"> {{ insuranceStats.healthTrend }} 较上月</div>
</div>
</a-card>
</div>
<!-- 提醒信息 -->
<div class="reminder-section">
<a-alert
type="warning"
:message="medicalInsuranceReminder"
:closable="false"
show-icon
/>
<a-alert
type="info"
:message="checkupReminder"
:closable="false"
show-icon
/>
</div>
<!-- 我的提醒表格 -->
<a-card class="reminder-table-card" :bordered="false">
<template #title>
<div class="card-title">我的提醒</div>
</template>
<a-table
:columns="reminderColumns"
:data="reminderData"
:pagination="false"
row-key="id"
>
<template #status="{ record }">
<a-tag
:color="record.status === 'pending' ? 'orange' : record.status === 'completed' ? 'green' : 'gray'"
>
{{ record.status === 'pending' ? '即将到期' : record.status === 'completed' ? '待安排' : '已完成' }}
</a-tag>
</template>
<template #action="{ record }">
<a-button
type="primary"
size="small"
@click="handleAction(record)"
>
{{ record.actionText }}
</a-button>
</template>
</a-table>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
//
const insuranceStats = reactive({
total: 3,
lastCheckup: '2023-10-15',
expiringCount: 1,
healthScore: 86,
healthTrend: '2%'
})
//
const medicalInsuranceReminder = '您的医疗保险将于 15天 后到期请及时联系HR处理续保事宜。'
const checkupReminder = '您的下一次体检安排在 2024-04-15请提前做好准备。'
//
const reminderColumns = [
{ title: '事项', dataIndex: 'item', width: 200 },
{ title: '日期', dataIndex: 'date', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 120 },
{ title: '操作', slotName: 'action', width: 150 }
]
//
const reminderData = ref([
{
id: 1,
item: '医疗保险续保',
date: '2023-12-15',
status: 'pending',
actionText: '详情'
},
{
id: 2,
item: '年度体检',
date: '2024-04-15',
status: 'completed',
actionText: '详情'
},
{
id: 3,
item: '健康问卷',
date: '2023-12-01',
status: 'completed',
actionText: '填写'
}
])
//
const handleAction = (record: any) => {
Message.success(`处理 ${record.item} 操作`)
}
</script>
<style scoped>
.insurance-overview-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-content {
text-align: center;
padding: 20px;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: #1d2129;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #4e5969;
margin-bottom: 12px;
}
.stat-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.stat-status.success {
background: #f0f9ff;
color: #1890ff;
}
.stat-status.normal {
background: #f6ffed;
color: #52c41a;
}
.stat-status.warning {
background: #fff7e6;
color: #fa8c16;
}
.reminder-section {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.reminder-table-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
}
</style>

View File

@ -0,0 +1,351 @@
<template>
<GiPageLayout>
<div class="personal-info-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">个人信息</h2>
<a-button type="primary" @click="handleEdit">
<template #icon><icon-edit /></template>
编辑信息
</a-button>
</div>
<!-- 个人信息展示 -->
<a-card class="info-card" :bordered="false">
<div class="info-sections">
<!-- 基本信息 -->
<div class="info-section">
<h3 class="section-title">基本信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">姓名</span>
<span class="info-value">{{ personalInfo.name }}</span>
</div>
<div class="info-item">
<span class="info-label">性别</span>
<span class="info-value">{{ personalInfo.gender }}</span>
</div>
<div class="info-item">
<span class="info-label">出生日期</span>
<span class="info-value">{{ personalInfo.birthDate }}</span>
</div>
<div class="info-item">
<span class="info-label">身份证号</span>
<span class="info-value">{{ personalInfo.idCard }}</span>
</div>
<div class="info-item">
<span class="info-label">婚姻状况</span>
<span class="info-value">{{ personalInfo.maritalStatus }}</span>
</div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ personalInfo.nationality }}</span>
</div>
</div>
</div>
<!-- 联系信息 -->
<div class="info-section">
<h3 class="section-title">联系信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">手机号码</span>
<span class="info-value">{{ personalInfo.phone }}</span>
</div>
<div class="info-item">
<span class="info-label">邮箱地址</span>
<span class="info-value">{{ personalInfo.email }}</span>
</div>
<div class="info-item">
<span class="info-label">现住址</span>
<span class="info-value">{{ personalInfo.address }}</span>
</div>
<div class="info-item">
<span class="info-label">紧急联系人</span>
<span class="info-value">{{ personalInfo.emergencyContact }}</span>
</div>
<div class="info-item">
<span class="info-label">紧急联系电话</span>
<span class="info-value">{{ personalInfo.emergencyPhone }}</span>
</div>
</div>
</div>
<!-- 工作信息 -->
<div class="info-section">
<h3 class="section-title">工作信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">员工编号</span>
<span class="info-value">{{ personalInfo.employeeId }}</span>
</div>
<div class="info-item">
<span class="info-label">部门</span>
<span class="info-value">{{ personalInfo.department }}</span>
</div>
<div class="info-item">
<span class="info-label">职位</span>
<span class="info-value">{{ personalInfo.position }}</span>
</div>
<div class="info-item">
<span class="info-label">入职日期</span>
<span class="info-value">{{ personalInfo.joinDate }}</span>
</div>
<div class="info-item">
<span class="info-label">工作年限</span>
<span class="info-value">{{ personalInfo.workYears }}</span>
</div>
<div class="info-item">
<span class="info-label">直属上级</span>
<span class="info-value">{{ personalInfo.supervisor }}</span>
</div>
</div>
</div>
</div>
</a-card>
<!-- 编辑信息弹窗 -->
<a-modal
v-model:visible="editModalVisible"
title="编辑个人信息"
width="800px"
@ok="handleSave"
@cancel="handleCancel"
>
<a-form :model="editForm" layout="vertical">
<div class="form-sections">
<!-- 基本信息 -->
<div class="form-section">
<h4 class="form-section-title">基本信息</h4>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item label="姓名">
<a-input v-model="editForm.name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="性别">
<a-select v-model="editForm.gender">
<a-option value="男"></a-option>
<a-option value="女"></a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item label="出生日期">
<a-date-picker v-model="editForm.birthDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="婚姻状况">
<a-select v-model="editForm.maritalStatus">
<a-option value="未婚">未婚</a-option>
<a-option value="已婚">已婚</a-option>
<a-option value="离异">离异</a-option>
<a-option value="丧偶">丧偶</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 联系信息 -->
<div class="form-section">
<h4 class="form-section-title">联系信息</h4>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item label="手机号码">
<a-input v-model="editForm.phone" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="邮箱地址">
<a-input v-model="editForm.email" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="现住址">
<a-input v-model="editForm.address" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item label="紧急联系人">
<a-input v-model="editForm.emergencyContact" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="紧急联系电话">
<a-input v-model="editForm.emergencyPhone" />
</a-form-item>
</a-col>
</a-row>
</div>
</div>
</a-form>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconEdit } from '@arco-design/web-vue/es/icon'
//
const personalInfo = reactive({
name: '张三',
gender: '男',
birthDate: '1990-05-15',
idCard: '11010219900515****',
maritalStatus: '已婚',
nationality: '汉族',
phone: '138****8888',
email: 'zhangsan@example.com',
address: '北京市朝阳区XX街道XX号',
emergencyContact: '李四',
emergencyPhone: '139****9999',
employeeId: 'EMP001',
department: '技术部',
position: '高级工程师',
joinDate: '2020-03-01',
workYears: '3年8个月',
supervisor: '王五'
})
//
const editForm = reactive({
name: '',
gender: '',
birthDate: '',
maritalStatus: '',
phone: '',
email: '',
address: '',
emergencyContact: '',
emergencyPhone: ''
})
//
const editModalVisible = ref(false)
//
const handleEdit = () => {
//
Object.assign(editForm, {
name: personalInfo.name,
gender: personalInfo.gender,
birthDate: personalInfo.birthDate,
maritalStatus: personalInfo.maritalStatus,
phone: personalInfo.phone,
email: personalInfo.email,
address: personalInfo.address,
emergencyContact: personalInfo.emergencyContact,
emergencyPhone: personalInfo.emergencyPhone
})
editModalVisible.value = true
}
//
const handleSave = () => {
//
Object.assign(personalInfo, editForm)
editModalVisible.value = false
Message.success('个人信息更新成功')
}
//
const handleCancel = () => {
editModalVisible.value = false
}
</script>
<style scoped>
.personal-info-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.info-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.info-sections {
padding: 20px;
}
.info-section {
margin-bottom: 30px;
}
.info-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 2px solid #e5e6eb;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
align-items: center;
}
.info-label {
font-weight: 600;
width: 120px;
flex-shrink: 0;
color: #1d2129;
}
.info-value {
color: #4e5969;
word-break: break-word;
}
.form-sections {
max-height: 60vh;
overflow-y: auto;
}
.form-section {
margin-bottom: 24px;
}
.form-section-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e6eb;
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<GiPageLayout>
<div class="policy-files-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">保单文件</h2>
</div>
<!-- 文件列表 -->
<div class="file-list">
<div
v-for="file in fileList"
:key="file.id"
class="file-item"
>
<div class="file-info">
<div class="file-icon">
<icon-file-pdf v-if="file.type === 'pdf'" />
<icon-file v-else />
</div>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">上传于 {{ file.uploadDate }}</div>
</div>
</div>
<div class="file-actions">
<a-button
type="primary"
size="small"
@click="handleView(file)"
>
查看
</a-button>
<a-button
type="outline"
size="small"
status="success"
@click="handleDownload(file)"
>
下载
</a-button>
</div>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconFilePdf, IconFile } from '@arco-design/web-vue/es/icon'
//
const fileList = ref([
{
id: 1,
name: '中国人寿-医疗保险-保单.pdf',
type: 'pdf',
uploadDate: '2023-01-05',
size: '2.5MB',
url: '/files/insurance/medical-insurance-policy.pdf'
},
{
id: 2,
name: '平安保险-意外险-保单.pdf',
type: 'pdf',
uploadDate: '2023-01-05',
size: '1.8MB',
url: '/files/insurance/accident-insurance-policy.pdf'
},
{
id: 3,
name: '泰康保险-养老保险-保单.pdf',
type: 'pdf',
uploadDate: '2022-01-05',
size: '3.2MB',
url: '/files/insurance/pension-insurance-policy.pdf'
},
{
id: 4,
name: '保险条款说明.docx',
type: 'docx',
uploadDate: '2022-01-10',
size: '1.5MB',
url: '/files/insurance/insurance-terms.docx'
}
])
//
const handleView = (file: any) => {
Message.info(`查看文件:${file.name}`)
//
}
//
const handleDownload = (file: any) => {
Message.success(`下载文件:${file.name}`)
//
//
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.click()
}
</script>
<style scoped>
.policy-files-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.file-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.file-item:hover {
transform: translateY(-2px);
}
.file-info {
display: flex;
align-items: center;
gap: 16px;
}
.file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f9ff;
border-radius: 8px;
color: #1890ff;
font-size: 20px;
}
.file-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.file-name {
font-size: 16px;
font-weight: 500;
color: #1d2129;
}
.file-meta {
font-size: 12px;
color: #4e5969;
}
.file-actions {
display: flex;
gap: 8px;
}
</style>

View File

@ -0,0 +1,507 @@
<template>
<div class="container">
<div class="header">
<h2>保险公司管理</h2>
<a-button type="primary" @click="showAddCompanyModal">
<template #icon>
<icon-plus />
</template>
添加保险公司
</a-button>
</div>
<div class="search-form">
<a-form :model="searchForm" layout="inline">
<a-form-item label="保险公司名称" field="insuranceCompanyName">
<a-input
v-model="searchForm.insuranceCompanyName"
placeholder="请输入保险公司名称"
style="width: 200px"
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="联系人" field="contact">
<a-input
v-model="searchForm.contact"
placeholder="请输入联系人"
style="width: 200px"
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="合作状态" field="status">
<a-select
v-model="searchForm.status"
placeholder="请选择合作状态"
style="width: 150px"
allow-clear
>
<a-option value="0">合作中</a-option>
<a-option value="1">已终止</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<icon-search />
</template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<div class="table-container">
<a-table
:columns="columns"
:data="companyList"
:pagination="paginationConfig"
:loading="loading"
row-key="id"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '合作中' : '已终止' }}
</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-button
type="primary"
size="small"
@click="editCompany(record)"
>
编辑
</a-button>
<a-button
type="primary"
status="warning"
size="small"
@click="terminateCooperation(record)"
v-if="record.status === '0'"
>
终止合作
</a-button>
<a-button
type="primary"
status="success"
size="small"
@click="resumeCooperation(record)"
v-if="record.status === '1'"
>
恢复合作
</a-button>
<a-button
type="primary"
status="danger"
size="small"
@click="deleteCompany(record)"
>
删除
</a-button>
</a-space>
</template>
</a-table>
</div>
<!-- 添加/编辑保险公司模态框 -->
<a-modal
v-model:visible="companyModalVisible"
:title="isEdit ? '编辑保险公司' : '添加保险公司'"
width="500px"
@ok="handleCompanySubmit"
@cancel="handleCompanyCancel"
:confirm-loading="submitLoading"
>
<a-form
ref="companyFormRef"
:model="companyForm"
:rules="companyRules"
layout="vertical"
>
<a-form-item label="保险公司名称" field="insuranceCompanyName">
<a-input v-model="companyForm.insuranceCompanyName" placeholder="请输入保险公司名称" />
</a-form-item>
<a-form-item label="联系人" field="contact">
<a-input v-model="companyForm.contact" placeholder="请输入联系人" />
</a-form-item>
<a-form-item label="联系电话" field="contactPhone">
<a-input v-model="companyForm.contactPhone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="电子邮箱" field="email">
<a-input v-model="companyForm.email" placeholder="请输入电子邮箱" />
</a-form-item>
<a-form-item label="公司地址" field="address">
<a-input v-model="companyForm.address" placeholder="请输入公司地址" />
</a-form-item>
<a-form-item label="合作开始日期" field="startDate">
<a-date-picker v-model="companyForm.startDate" style="width: 100%" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus, IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import * as InsuranceCompanyAPI from '@/apis/insurance-company'
//
interface Company {
id?: string
insuranceCompanyName: string
contact: string
contactPhone: string
status: string
email?: string
address?: string
startDate?: string
}
//
const columns = [
{
title: '保险公司名称',
dataIndex: 'insuranceCompanyName',
width: 200,
},
{
title: '联系人',
dataIndex: 'contact',
width: 120,
},
{
title: '联系电话',
dataIndex: 'contactPhone',
width: 150,
},
{
title: '电子邮箱',
dataIndex: 'email',
width: 180,
},
{
title: '合作状态',
dataIndex: 'status',
slotName: 'status',
width: 100,
},
{
title: '操作',
slotName: 'actions',
width: 200,
},
]
//
const loading = ref(false)
const submitLoading = ref(false)
const companyList = ref<Company[]>([])
const companyModalVisible = ref(false)
const isEdit = ref(false)
const companyFormRef = ref()
//
const searchForm = reactive({
insuranceCompanyName: '',
contact: '',
status: '',
})
//
const paginationConfig = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showPageSize: true,
pageSizeOptions: ['10', '20', '50', '100']
})
//
const companyForm = reactive({
id: '',
insuranceCompanyName: '',
contact: '',
contactPhone: '',
status: '0',
email: '',
address: '',
startDate: ''
})
//
const companyRules = {
insuranceCompanyName: [
{ required: true, message: '请输入保险公司名称' },
],
contact: [
{ required: true, message: '请输入联系人' },
],
contactPhone: [
{ required: true, message: '请输入联系电话' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码'
},
],
email: [
{
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '请输入正确的邮箱地址'
},
],
}
//
const getCompanyList = async () => {
try {
loading.value = true
const params = {
...searchForm,
current: paginationConfig.current,
size: paginationConfig.pageSize,
}
const response = await InsuranceCompanyAPI.getInsuranceCompanyList(params)
console.log('保险公司列表响应:', response)
if (response.data) {
companyList.value = response.data || []
paginationConfig.total = response.data.total || 0
} else {
Message.error('获取保险公司列表失败')
}
} catch (error) {
console.error('获取保险公司列表失败:', error)
Message.error('获取保险公司列表失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
paginationConfig.current = 1
getCompanyList()
}
//
const handleReset = () => {
Object.assign(searchForm, {
insuranceCompanyName: '',
contact: '',
status: '',
})
paginationConfig.current = 1
getCompanyList()
}
//
const handlePageChange = (page: number) => {
paginationConfig.current = page
getCompanyList()
}
//
const handlePageSizeChange = (pageSize: number) => {
paginationConfig.pageSize = pageSize
paginationConfig.current = 1
getCompanyList()
}
//
const showAddCompanyModal = () => {
isEdit.value = false
resetForm()
companyModalVisible.value = true
}
//
const editCompany = (record: Company) => {
isEdit.value = true
Object.assign(companyForm, record)
companyModalVisible.value = true
}
//
const terminateCooperation = (record: Company) => {
Modal.confirm({
title: '确认终止合作',
content: `确定要终止与 ${record.insuranceCompanyName} 的合作吗?`,
onOk: async () => {
try {
const response = await InsuranceCompanyAPI.terminateCooperation(record.id!)
if (response.code === 200) {
Message.success('合作已终止')
getCompanyList()
} else {
Message.error(response.msg || '终止合作失败')
}
} catch (error) {
console.error('终止合作失败:', error)
Message.error('终止合作失败')
}
}
})
}
//
const resumeCooperation = (record: Company) => {
Modal.confirm({
title: '确认恢复合作',
content: `确定要恢复与 ${record.insuranceCompanyName} 的合作吗?`,
onOk: async () => {
try {
const response = await InsuranceCompanyAPI.resumeCooperation(record.id!)
if (response.code === 200) {
Message.success('合作已恢复')
getCompanyList()
} else {
Message.error(response.msg || '恢复合作失败')
}
} catch (error) {
console.error('恢复合作失败:', error)
Message.error('恢复合作失败')
}
}
})
}
//
const deleteCompany = (record: Company) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除 ${record.insuranceCompanyName} 吗?此操作不可恢复。`,
onOk: async () => {
try {
const response = await InsuranceCompanyAPI.deleteInsuranceCompany(record.id!)
if (response.code === 200) {
Message.success('删除成功')
getCompanyList()
} else {
Message.error(response.msg || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
})
}
//
const handleCompanySubmit = async () => {
try {
await companyFormRef.value?.validate()
submitLoading.value = true
const formData = { ...companyForm }
if (isEdit.value) {
//
const response = await InsuranceCompanyAPI.updateInsuranceCompany(formData.id!, formData)
if (response.code === 200) {
Message.success('保险公司信息更新成功')
companyModalVisible.value = false
resetForm()
getCompanyList()
} else {
Message.error(response.msg || '更新失败')
}
} else {
//
const response = await InsuranceCompanyAPI.createInsuranceCompany(formData)
if (response.code === 200) {
Message.success('保险公司添加成功')
companyModalVisible.value = false
resetForm()
getCompanyList()
} else {
Message.error(response.msg || '添加失败')
}
}
} catch (error) {
console.error('表单验证失败:', error)
} finally {
submitLoading.value = false
}
}
//
const handleCompanyCancel = () => {
companyModalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(companyForm, {
id: '',
insuranceCompanyName: '',
contact: '',
contactPhone: '',
status: '0',
email: '',
address: '',
startDate: ''
})
companyFormRef.value?.resetFields()
}
//
onMounted(() => {
getCompanyList()
})
</script>
<style scoped>
.container {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.search-form {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.table-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -0,0 +1,409 @@
<template>
<GiPageLayout>
<div class="file-management-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">保单文件管理</h2>
<a-button type="primary" @click="handleUploadFile">
<template #icon><icon-upload /></template>
上传文件
</a-button>
</div>
<!-- 搜索表单 -->
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item>
<a-input
v-model="searchForm.keyword"
placeholder="搜索员工姓名或文件名..."
style="width: 300px"
allow-clear
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
搜索
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 文件信息表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="tableData"
:pagination="pagination"
:loading="loading"
row-key="id"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #fileType="{ record }">
<a-tag color="blue">
{{ record.fileType }}
</a-tag>
</template>
<template #action="{ record }">
<a-button
type="text"
size="small"
@click="handleView(record)"
>
查看
</a-button>
<a-button
type="text"
size="small"
status="success"
@click="handleDownload(record)"
>
下载
</a-button>
<a-button
type="text"
size="small"
status="danger"
@click="handleDelete(record)"
>
删除
</a-button>
</template>
</a-table>
</a-card>
<!-- 上传文件弹窗 -->
<a-modal
v-model:visible="uploadModalVisible"
title="上传保单文件"
width="600px"
@ok="handleUploadOk"
@cancel="handleUploadCancel"
>
<a-form :model="uploadForm" layout="vertical">
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="员工" required>
<a-select
v-model="uploadForm.employeeId"
placeholder="请选择员工"
:loading="loadingEmployees"
allow-search
:filter-option="true"
>
<a-option
v-for="employee in employeeOptions"
:key="employee.id"
:value="employee.id"
>
{{ employee.nickname || employee.username }} ({{ employee.username }})
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="文件类型" required>
<a-select v-model="uploadForm.fileType" placeholder="请选择文件类型">
<a-option value="保险合同">保险合同</a-option>
<a-option value="保单">保单</a-option>
<a-option value="理赔申请">理赔申请</a-option>
<a-option value="体检报告">体检报告</a-option>
<a-option value="其他文件">其他文件</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="选择文件" required>
<a-upload
:auto-upload="false"
:show-file-list="true"
:limit="1"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
>
<template #upload-button>
<a-button>
<template #icon><icon-upload /></template>
选择文件
</a-button>
</template>
</a-upload>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="文件描述">
<a-textarea v-model="uploadForm.description" placeholder="请输入文件描述" :auto-size="{ minRows: 3, maxRows: 5 }" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
<!-- 查看文件详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="文件详情"
width="600px"
>
<div class="detail-content" v-if="selectedRecord">
<div class="detail-item">
<span class="detail-label">员工姓名</span>
<span class="detail-value">{{ selectedRecord.employeeName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">工号</span>
<span class="detail-value">{{ selectedRecord.employeeId }}</span>
</div>
<div class="detail-item">
<span class="detail-label">文件名</span>
<span class="detail-value">{{ selectedRecord.fileName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">文件类型</span>
<a-tag color="blue">{{ selectedRecord.fileType }}</a-tag>
</div>
<div class="detail-item">
<span class="detail-label">上传日期</span>
<span class="detail-value">{{ selectedRecord.uploadDate }}</span>
</div>
<div class="detail-item" v-if="selectedRecord.remarks">
<span class="detail-label">备注</span>
<span class="detail-value">{{ selectedRecord.remarks }}</span>
</div>
</div>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconUpload,
IconSearch
} from '@arco-design/web-vue/es/icon'
import { listAllUser } from '@/apis/system/user'
import type { UserResp } from '@/apis/system/type'
//
const searchForm = reactive({
keyword: ''
})
//
const employeeOptions = ref<UserResp[]>([])
const loadingEmployees = ref(false)
//
const columns = [
{ title: '员工姓名', dataIndex: 'employeeName', width: 120 },
{ title: '工号', dataIndex: 'employeeId', width: 120 },
{ title: '文件名', dataIndex: 'fileName', width: 200 },
{ title: '文件类型', dataIndex: 'fileType', slotName: 'fileType', width: 120 },
{ title: '上传日期', dataIndex: 'uploadDate', width: 140 },
{ title: '操作', slotName: 'action', width: 180 }
]
//
const tableData = ref([
{
id: 1,
employeeName: '张三',
employeeId: 'EMP001',
fileName: '医疗保险合同.pdf',
fileType: 'PDF文档',
uploadDate: '2023-01-05',
remarks: '医疗保险相关合同文件'
},
{
id: 2,
employeeName: '李四',
employeeId: 'EMP002',
fileName: '意外险合同.pdf',
fileType: 'PDF文档',
uploadDate: '2023-01-05',
remarks: '意外险保单合同'
},
{
id: 3,
employeeName: '王五',
employeeId: 'EMP003',
fileName: '养老保险合同.pdf',
fileType: 'PDF文档',
uploadDate: '2022-01-05',
remarks: '养老保险合同文档'
}
])
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const loading = ref(false)
//
const uploadModalVisible = ref(false)
const detailModalVisible = ref(false)
//
const uploadForm = reactive({
employeeName: '',
employeeId: '',
fileName: '',
fileType: '',
remarks: '',
description: ''
})
//
const selectedRecord = ref<any>(null)
//
const handleSearch = () => {
loading.value = true
setTimeout(() => {
loading.value = false
Message.success('搜索完成')
}, 1000)
}
//
const handlePageChange = (current: number) => {
pagination.current = current
handleSearch()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
handleSearch()
}
//
const handleUploadFile = () => {
uploadModalVisible.value = true
Object.assign(uploadForm, {
employeeName: '',
employeeId: '',
fileName: '',
fileType: '',
remarks: ''
})
}
//
const handleView = (record: any) => {
selectedRecord.value = record
detailModalVisible.value = true
}
//
const handleDownload = (record: any) => {
Message.success(`下载文件 ${record.fileName} 成功`)
//
}
//
const handleDelete = (record: any) => {
Message.success(`删除文件 ${record.fileName} 成功`)
}
//
const handleUploadOk = () => {
Message.success('文件上传成功')
uploadModalVisible.value = false
}
//
const handleUploadCancel = () => {
uploadModalVisible.value = false
}
//
const fetchEmployeeOptions = async () => {
try {
loadingEmployees.value = true
const response = await listAllUser({})
employeeOptions.value = response.data || []
} catch (error) {
console.error('获取员工列表失败:', error)
Message.error('获取员工列表失败')
} finally {
loadingEmployees.value = false
}
}
//
onMounted(() => {
handleSearch()
fetchEmployeeOptions()
})
</script>
<style scoped>
.file-management-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.search-card {
margin-bottom: 20px;
}
.table-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.detail-content {
padding: 20px;
}
.detail-item {
display: flex;
margin-bottom: 16px;
align-items: flex-start;
}
.detail-label {
font-weight: 600;
width: 100px;
flex-shrink: 0;
color: #1d2129;
}
.detail-value {
color: #4e5969;
word-break: break-word;
}
</style>

View File

@ -0,0 +1,516 @@
<template>
<GiPageLayout>
<div class="health-management-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">健康档案管理</h2>
<a-button type="primary" @click="handleAddRecord">
<template #icon><icon-plus /></template>
添加记录
</a-button>
</div>
<!-- 搜索表单 -->
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item>
<a-input
v-model="searchForm.keyword"
placeholder="搜索员工姓名或工号..."
style="width: 300px"
allow-clear
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
搜索
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 健康档案表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="tableData"
:pagination="pagination"
:loading="loading"
row-key="id"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #result="{ record }">
<a-tag
:color="record.result === '正常' ? 'green' : record.result === '轻微异常' ? 'orange' : 'red'"
>
{{ record.result }}
</a-tag>
</template>
<template #action="{ record }">
<a-button
type="text"
size="small"
@click="handleView(record)"
>
查看
</a-button>
<a-button
type="text"
size="small"
@click="handleEdit(record)"
>
编辑
</a-button>
</template>
</a-table>
</a-card>
<!-- 添加/编辑记录弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑健康记录' : '添加健康记录'"
width="800px"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form :model="formData" layout="vertical">
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="员工" required>
<a-select
v-model="formData.employeeId"
placeholder="请选择员工"
:loading="loadingEmployees"
allow-search
:filter-option="true"
>
<a-option
v-for="employee in employeeOptions"
:key="employee.id"
:value="employee.id"
>
{{ employee.nickname || employee.username }} ({{ employee.username }})
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检日期" required>
<a-date-picker v-model="formData.checkupDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检医院" required>
<a-input v-model="formData.hospital" placeholder="请输入体检医院" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检类型" required>
<a-select v-model="formData.checkType" placeholder="请选择体检类型">
<a-option value="常规体检">常规体检</a-option>
<a-option value="入职体检">入职体检</a-option>
<a-option value="年度体检">年度体检</a-option>
<a-option value="特殊体检">特殊体检</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检结果" required>
<a-select v-model="formData.result" placeholder="请选择体检结果">
<a-option value="正常">正常</a-option>
<a-option value="轻微异常">轻微异常</a-option>
<a-option value="异常">异常</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="下次体检日期" required>
<a-date-picker v-model="formData.nextCheckupDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检总结">
<a-textarea v-model="formData.summary" placeholder="请输入体检总结" :auto-size="{ minRows: 3, maxRows: 5 }" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="医生建议">
<a-textarea v-model="formData.suggestions" placeholder="请输入医生建议" :auto-size="{ minRows: 3, maxRows: 5 }" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="体检报告">
<a-upload
:file-list="formData.reportFiles"
@change="handleFileChange"
action="#"
:before-upload="beforeUpload"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
>
<template #upload-button>
<a-button type="outline">选择文件</a-button>
</template>
</a-upload>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
<!-- 查看详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="健康档案详情"
width="800px"
>
<div class="detail-content" v-if="selectedRecord">
<div class="detail-section">
<h4 class="section-title">基本信息</h4>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">员工姓名</span>
<span class="detail-value">{{ selectedRecord.employeeName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">工号</span>
<span class="detail-value">{{ selectedRecord.employeeId }}</span>
</div>
<div class="detail-item">
<span class="detail-label">体检日期</span>
<span class="detail-value">{{ selectedRecord.checkupDate }}</span>
</div>
<div class="detail-item">
<span class="detail-label">体检医院</span>
<span class="detail-value">{{ selectedRecord.hospital }}</span>
</div>
<div class="detail-item">
<span class="detail-label">体检结果</span>
<a-tag
:color="selectedRecord.result === '正常' ? 'green' : selectedRecord.result === '轻微异常' ? 'orange' : 'red'"
>
{{ selectedRecord.result }}
</a-tag>
</div>
<div class="detail-item">
<span class="detail-label">下次体检</span>
<span class="detail-value">{{ selectedRecord.nextCheckupDate }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="selectedRecord.remarks">
<h4 class="section-title">体检备注</h4>
<p class="remarks-text">{{ selectedRecord.remarks }}</p>
</div>
</div>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconPlus,
IconSearch
} from '@arco-design/web-vue/es/icon'
import { listAllUser } from '@/apis/system/user'
import type { UserResp } from '@/apis/system/type'
//
const searchForm = reactive({
keyword: ''
})
//
const employeeOptions = ref<UserResp[]>([])
const loadingEmployees = ref(false)
//
const columns = [
{ title: '员工姓名', dataIndex: 'employeeName', width: 120 },
{ title: '工号', dataIndex: 'employeeId', width: 120 },
{ title: '最近体检日期', dataIndex: 'checkupDate', width: 140 },
{ title: '体检医院', dataIndex: 'hospital', width: 160 },
{ title: '体检结果', dataIndex: 'result', slotName: 'result', width: 120 },
{ title: '下次体检日期', dataIndex: 'nextCheckupDate', width: 140 },
{ title: '操作', slotName: 'action', width: 120 }
]
//
const tableData = ref([
{
id: 1,
employeeName: '张三',
employeeId: 'EMP001',
checkupDate: '2023-10-15',
hospital: '北京协和医院',
result: '正常',
nextCheckupDate: '2024-10-15',
remarks: '身体状况良好,各项指标正常'
},
{
id: 2,
employeeName: '李四',
employeeId: 'EMP002',
checkupDate: '2023-09-20',
hospital: '上海瑞金医院',
result: '轻微异常',
nextCheckupDate: '2024-03-20',
remarks: '血压稍高,建议控制饮食,加强运动'
},
{
id: 3,
employeeName: '王五',
employeeId: 'EMP003',
checkupDate: '2023-08-10',
hospital: '广州中山医院',
result: '异常',
nextCheckupDate: '2023-11-10',
remarks: '肝功能异常,需要进一步检查和治疗'
}
])
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const loading = ref(false)
//
const modalVisible = ref(false)
const detailModalVisible = ref(false)
const isEdit = ref(false)
//
const formData = reactive({
id: null as number | null,
employeeName: '',
employeeId: '',
checkupDate: '',
hospital: '',
result: '',
nextCheckupDate: '',
remarks: '',
checkType: '',
summary: '',
suggestions: '',
reportFiles: [] as any[]
})
//
const selectedRecord = ref<any>(null)
//
const handleSearch = () => {
loading.value = true
setTimeout(() => {
loading.value = false
Message.success('搜索完成')
}, 1000)
}
//
const handlePageChange = (current: number) => {
pagination.current = current
handleSearch()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
handleSearch()
}
//
const handleAddRecord = () => {
isEdit.value = false
modalVisible.value = true
Object.assign(formData, {
id: null,
employeeName: '',
employeeId: '',
checkupDate: '',
hospital: '',
result: '',
nextCheckupDate: '',
remarks: '',
checkType: '',
summary: '',
suggestions: '',
reportFiles: []
})
}
//
const handleEdit = (record: any) => {
isEdit.value = true
modalVisible.value = true
Object.assign(formData, record)
}
//
const handleView = (record: any) => {
selectedRecord.value = record
detailModalVisible.value = true
}
//
const handleModalOk = () => {
if (isEdit.value) {
Message.success('编辑健康记录成功')
} else {
Message.success('添加健康记录成功')
}
modalVisible.value = false
}
//
const handleModalCancel = () => {
modalVisible.value = false
}
//
const handleFileChange = (fileList: any[]) => {
formData.reportFiles = fileList
}
//
const beforeUpload = (file: any) => {
const isValidType = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/jpg', 'image/png'].includes(file.type)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isValidType) {
Message.error('文件类型错误,仅支持 PDF、Word、图片格式')
return false
}
if (!isLt10M) {
Message.error('文件大小不能超过10MB')
return false
}
return true
}
//
const fetchEmployeeOptions = async () => {
try {
loadingEmployees.value = true
const response = await listAllUser({})
employeeOptions.value = response.data || []
} catch (error) {
console.error('获取员工列表失败:', error)
Message.error('获取员工列表失败')
} finally {
loadingEmployees.value = false
}
}
//
onMounted(() => {
handleSearch()
fetchEmployeeOptions()
})
</script>
<style scoped>
.health-management-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.search-card {
margin-bottom: 20px;
}
.table-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.detail-content {
padding: 20px;
}
.detail-section {
margin-bottom: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e6eb;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
}
.detail-label {
font-weight: 600;
width: 120px;
flex-shrink: 0;
color: #1d2129;
}
.detail-value {
color: #4e5969;
word-break: break-word;
}
.remarks-text {
color: #4e5969;
line-height: 1.6;
margin: 0;
}
</style>

View File

@ -0,0 +1,659 @@
<template>
<GiPageLayout>
<div class="insurance-manage-container">
<div class="page-header">
<h2 class="page-title">保险信息管理</h2>
<a-button type="primary" @click="showAddModal">
<template #icon>
<icon-plus />
</template>
新增保险
</a-button>
</div>
<!-- 搜索表单 -->
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item label="保险公司" field="insuranceCompanyId">
<a-select
v-model="searchForm.insuranceCompanyId"
placeholder="请选择保险公司"
allow-clear
:loading="loadingCompanies"
style="width: 200px"
>
<a-option
v-for="company in companyList"
:key="company.id"
:value="company.id"
>
{{ company.insuranceCompanyName }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="保险类型" field="insuranceTypeId">
<a-select
v-model="searchForm.insuranceTypeId"
placeholder="请选择保险类型"
allow-clear
:loading="loadingTypes"
style="width: 200px"
>
<a-option
v-for="type in typeList"
:key="type.id"
:value="type.id"
>
{{ type.insuranceTypeName }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="用户" field="userId">
<a-input
v-model="searchForm.userId"
placeholder="请输入用户ID"
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<icon-search />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<!-- 保险信息表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="insuranceList"
:pagination="paginationConfig"
:loading="loading"
row-key="id"
@page-change="handlePageChange"
>
<template #insuranceCompany="{ record }">
<span>{{ getCompanyName(record.insuranceCompanyId) }}</span>
</template>
<template #insuranceType="{ record }">
<span>{{ getTypeName(record.insuranceTypeId) }}</span>
</template>
<template #status="{ record }">
<a-tag
:color="record.status === 'active' ? 'green' : 'red'"
>
{{ record.status === 'active' ? '有效' : '失效' }}
</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-button type="primary" size="small" @click="editRecord(record)">
编辑
</a-button>
<a-button type="primary" status="danger" size="small" @click="deleteRecord(record)">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 新增/编辑保险信息模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑保险信息' : '新增保险信息'"
width="600px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保险公司" field="insuranceCompanyId">
<a-select
v-model="formData.insuranceCompanyId"
placeholder="请选择保险公司"
:loading="loadingCompanies"
>
<a-option
v-for="company in companyList"
:key="company.insuranceCompanyId"
:value="company.insuranceCompanyId"
>
{{ company.insuranceCompanyName }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保险类型" field="insuranceTypeId">
<a-select
v-model="formData.insuranceTypeId"
placeholder="请选择保险类型"
:loading="loadingTypes"
>
<a-option
v-for="type in typeList"
:key="type.insuranceTypeId"
:value="type.insuranceTypeId"
>
{{ type.insuranceTypeName }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="选择人员" field="userId">
<a-select
v-model="formData.userId"
placeholder="请选择员工"
allow-search
:filter-option="false"
@search="searchEmployees"
>
<a-option
v-for="employee in employeeOptions"
:key="employee.userId"
:value="employee.userId"
>
{{ employee.nickname || employee.name }} ({{ employee.deptName }})
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保险单号" field="insuranceBillCode">
<a-input v-model="formData.insuranceBillCode" placeholder="请输入保险单号" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="生效日期" field="effectiveDate">
<a-date-picker
v-model="formData.effectiveDate"
placeholder="请选择生效日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="到期日期" field="expireDate">
<a-date-picker
v-model="formData.expireDate"
placeholder="请选择到期日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保险金额" field="insuranceAmount">
<a-input-number
v-model="formData.insuranceAmount"
placeholder="请输入保险金额"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保险费" field="insurancePremium">
<a-input-number
v-model="formData.insurancePremium"
placeholder="请输入保险费"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="受益人" field="beneficiary">
<a-input v-model="formData.beneficiary" placeholder="请输入受益人" />
</a-form-item>
<a-col :span="12">
<a-form-item label="上传保单" field="attachInfoId">
<a-upload
:custom-request="uploadInsuranceFile"
>
<a-button type="primary">
<template #icon><icon-upload /></template>
上传保单
</a-button>
</a-upload>
</a-form-item>
</a-col>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus, IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import * as InsuranceAPI from '@/apis/insurance'
import { listAllUser } from '@/apis/system/user'
import type { InsuranceInfo, InsuranceListParams } from '@/apis/insurance'
import * as InsuranceCompanyAPI from '@/apis/insurance-company'
import * as InsuranceTypeAPI from '@/apis/insurance-type'
import { addAttachInsurance } from '@/apis/attach-info'
//
const loading = ref(false)
const loadingCompanies = ref(false)
const loadingTypes = ref(false)
const modalVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
//
const insuranceList = ref<InsuranceInfo[]>([])
const companyList = ref<any[]>([])
const typeList = ref<any[]>([])
//
const searchForm = reactive<InsuranceListParams>({
insuranceCompanyId: '',
insuranceTypeId: '',
userId: ''
})
//
const paginationConfig = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true
})
//
const formData = reactive<InsuranceInfo>({
id: '',
attachInfoId: '',
insuranceCompanyId: '',
insuranceTypeId: '',
userId: '',
insuranceBillCode: '',
effectiveDate: '',
expireDate: '',
insuranceAmount: 0,
insurancePremium: 0,
beneficiary: '',
remark: ''
})
//
const formRules = {
insuranceCompanyId: [
{ required: true, message: '请选择保险公司' }
],
insuranceTypeId: [
{ required: true, message: '请选择保险类型' }
],
userId: [
{ required: true, message: '请输入用户ID' }
],
insuranceBillCode: [
{ required: true, message: '请输入保险单号' }
],
effectiveDate: [
{ required: true, message: '请选择生效日期' }
],
expireDate: [
{ required: true, message: '请选择到期日期' }
],
insuranceAmount: [
{ required: true, message: '请输入保险金额' }
],
insurancePremium: [
{ required: true, message: '请输入保险费' }
]
}
//
const columns = [
{
title: '用户名称',
dataIndex: 'name',
width: 100
},
{
title: '保险公司',
dataIndex: 'insuranceCompanyName',
slotName: 'insuranceCompanyName',
width: 150
},
{
title: '保险类型',
dataIndex: 'insuranceTypeName',
slotName: 'insuranceTypeName',
width: 120
},
{
title: '保险单号',
dataIndex: 'insuranceBillCode',
width: 150
},
{
title: '生效日期',
dataIndex: 'effectiveDate',
width: 120
},
{
title: '到期日期',
dataIndex: 'expireDate',
width: 120
},
{
title: '保险金额',
dataIndex: 'insuranceAmount',
width: 120
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 80
},
{
title: '操作',
slotName: 'actions',
width: 150,
fixed: 'right'
}
]
//
const getCompanyName = (id: string) => {
const company = companyList.value.find(c => c.id === id)
return company ? company.insuranceCompanyName : '-'
}
//
const getTypeName = (id: string) => {
const type = typeList.value.find(t => t.id === id)
return type ? type.insuranceTypeName : '-'
}
//
const getCompanyList = async () => {
try {
loadingCompanies.value = true
const response = await InsuranceCompanyAPI.getInsuranceCompanyList({})
console.log('保险公司列表响应:', response)
if (response.data) {
companyList.value = response.data || []
}
} catch (error) {
console.error('获取保险公司列表失败:', error)
Message.error('获取保险公司列表失败')
} finally {
loadingCompanies.value = false
}
}
//
const getTypeList = async () => {
try {
loadingTypes.value = true
const response = await InsuranceTypeAPI.getInsuranceTypeList()
console.log('保险类型列表响应:', response)
if (response.data) {
typeList.value = response.data || []
}
} catch (error) {
console.error('获取保险类型列表失败:', error)
Message.error('获取保险类型列表失败')
} finally {
loadingTypes.value = false
}
}
//
const getInsuranceList = async () => {
try {
loading.value = true
const params: InsuranceListParams = {
...searchForm,
current: paginationConfig.current,
size: paginationConfig.pageSize
}
const response = await InsuranceAPI.getInsuranceList(params)
console.log('保险信息列表响应:', response)
if (response.data) {
insuranceList.value = response.data || []
paginationConfig.total = response.data.total || 0
}
} catch (error) {
console.error('获取保险信息列表失败:', error)
Message.error('获取保险信息列表失败')
} finally {
loading.value = false
}
}
//
const searchEmployees = (keyword: string) => {
fetchEmployeeOptions(keyword)
}
const employeeOptions = ref<any[]>([])
//
const fetchEmployeeOptions = async (keyword?: string) => {
try {
const response = await listAllUser({ description: keyword })
employeeOptions.value = response.data || []
} catch (error) {
console.error('获取员工列表失败:', error)
Message.error('获取员工列表失败')
} finally {
}
}
const uploadInsuranceFile = async (options: any) => {
if(!formData.userId || !formData.insuranceBillCode){
Message.error('请先选择员工和输入保险单号')
return
}
const uploadForm = new FormData()
uploadForm.append('file', options.fileItem.file)
uploadForm.append('userDefinedPath', `${formData.userId}/${formData.insuranceBillCode}`)
const response = await addAttachInsurance(uploadForm)
console.log('上传保险文件响应:', response)
formData.attachInfoId = response.data
}
//
const handleSearch = () => {
paginationConfig.current = 1
getInsuranceList()
}
//
const handleReset = () => {
Object.assign(searchForm, {
insuranceCompanyId: '',
insuranceTypeId: '',
userId: ''
})
paginationConfig.current = 1
getInsuranceList()
}
//
const handlePageChange = (page: number) => {
paginationConfig.current = page
getInsuranceList()
}
//
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
}
//
const editRecord = (record: InsuranceInfo) => {
isEdit.value = true
Object.assign(formData, record)
modalVisible.value = true
}
//
const deleteRecord = (record: InsuranceInfo) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这条保险信息吗?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await InsuranceAPI.deleteInsurance(record.id!)
Message.success('删除成功')
await getInsuranceList()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
})
}
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (isEdit.value) {
await InsuranceAPI.updateInsurance(formData.id!, formData)
Message.success('更新成功')
} else {
await InsuranceAPI.createInsurance(formData)
Message.success('创建成功')
}
modalVisible.value = false
resetForm()
await getInsuranceList()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败,请重试')
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(formData, {
id: '',
attachInfoId: '',
insuranceCompanyId: '',
insuranceTypeId: '',
userId: '',
insuranceBillCode: '',
effectiveDate: '',
expireDate: '',
insuranceAmount: 0,
insurancePremium: 0,
beneficiary: '',
remark: ''
})
formRef.value?.resetFields()
}
//
const init = async () => {
await Promise.all([
getCompanyList(),
getTypeList(),
fetchEmployeeOptions(),
getInsuranceList()
])
}
//
onMounted(() => {
init()
})
</script>
<style scoped>
.insurance-manage-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.search-card {
margin-bottom: 20px;
}
.table-card {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,310 @@
<template>
<GiPageLayout>
<div class="system-insurance-overview-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">工作台概览</h2>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ stats.totalEmployees }}</div>
<div class="stat-label">员工总数</div>
<div class="stat-trend"> 5% 较上月</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ stats.validPolicies }}</div>
<div class="stat-label">有效保单</div>
<div class="stat-trend"> 3% 较上月</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ stats.expiringPolicies }}</div>
<div class="stat-label">即将到期</div>
<div class="stat-trend"> 5% 较上月</div>
</div>
</a-card>
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-value">{{ stats.annualPremium }}</div>
<div class="stat-label">年度总保费</div>
<div class="stat-trend"> 8% 较上年</div>
</div>
</a-card>
</div>
<!-- 筛选和操作区域 -->
<div class="filter-section">
<div class="filter-left">
<a-select v-model="selectedDepartment" placeholder="全部部门" style="width: 150px">
<a-option value="">全部部门</a-option>
<a-option value="tech">技术部</a-option>
<a-option value="sales">销售部</a-option>
<a-option value="hr">人事部</a-option>
<a-option value="finance">财务部</a-option>
</a-select>
<a-button type="primary" @click="handleFilter">
筛选
</a-button>
</div>
<div class="filter-right">
<a-button type="outline" @click="handleExportExcel">
<template #icon><icon-download /></template>
导出Excel
</a-button>
</div>
</div>
<!-- 即将到期提醒 -->
<a-alert
type="warning"
:message="expiringAlert"
:closable="false"
show-icon
class="expiring-alert"
/>
<!-- 保险类型分布图 -->
<a-card class="chart-card" :bordered="false">
<template #title>
<div class="chart-title">
<icon-bar-chart />
保险类型分布图表
</div>
</template>
<div class="chart-container">
<div class="chart-placeholder">
<div class="chart-legend">
<div class="legend-item">
<div class="legend-color medical"></div>
<span class="legend-text">医疗保险 (45%)</span>
</div>
<div class="legend-item">
<div class="legend-color accident"></div>
<span class="legend-text">意外险 (25%)</span>
</div>
<div class="legend-item">
<div class="legend-color pension"></div>
<span class="legend-text">养老保险 (20%)</span>
</div>
<div class="legend-item">
<div class="legend-color life"></div>
<span class="legend-text">人寿保险 (10%)</span>
</div>
</div>
<div class="chart-visual">
<!-- 简单的饼图可视化 -->
<svg width="300" height="300" viewBox="0 0 300 300">
<circle cx="150" cy="150" r="80" fill="#1890ff" stroke="#fff" stroke-width="2" />
<circle cx="150" cy="150" r="80" fill="#52c41a" stroke="#fff" stroke-width="2"
stroke-dasharray="125.6 502.4" stroke-dashoffset="0"
transform="rotate(90 150 150)" />
<circle cx="150" cy="150" r="80" fill="#faad14" stroke="#fff" stroke-width="2"
stroke-dasharray="100.5 527.5" stroke-dashoffset="-125.6"
transform="rotate(90 150 150)" />
<circle cx="150" cy="150" r="80" fill="#f5222d" stroke="#fff" stroke-width="2"
stroke-dasharray="62.8 565.2" stroke-dashoffset="-226.1"
transform="rotate(90 150 150)" />
</svg>
</div>
</div>
</div>
</a-card>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconDownload, IconBarChart } from '@arco-design/web-vue/es/icon'
//
const stats = reactive({
totalEmployees: 156,
validPolicies: 142,
expiringPolicies: 23,
annualPremium: '¥ 1,256,800'
})
//
const selectedDepartment = ref('')
//
const expiringAlert = '您有 3份 保险即将在30天内到期请及时处理。'
//
const handleFilter = () => {
Message.info('筛选功能已执行')
}
// Excel
const handleExportExcel = () => {
Message.success('Excel文件导出成功')
}
</script>
<style scoped>
.system-insurance-overview-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #1d2129;
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-content {
text-align: center;
padding: 20px;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: #1d2129;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #4e5969;
margin-bottom: 8px;
}
.stat-trend {
font-size: 12px;
color: #52c41a;
font-weight: 500;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.filter-left {
display: flex;
align-items: center;
gap: 12px;
}
.filter-right {
display: flex;
gap: 12px;
}
.expiring-alert {
margin-bottom: 20px;
}
.chart-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #1d2129;
}
.chart-container {
padding: 20px;
}
.chart-placeholder {
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
}
.chart-legend {
display: flex;
flex-direction: column;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 12px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
}
.legend-color.medical {
background: #1890ff;
}
.legend-color.accident {
background: #52c41a;
}
.legend-color.pension {
background: #faad14;
}
.legend-color.life {
background: #f5222d;
}
.legend-text {
font-size: 14px;
color: #4e5969;
}
.chart-visual {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div class="container">
<div class="header">
<h2>保险类型管理</h2>
<a-button type="primary" @click="showAddTypeModal">
<template #icon>
<icon-plus />
</template>
添加保险类型
</a-button>
</div>
<div class="table-container">
<a-table
:columns="columns"
:data="typeList"
:pagination="false"
:loading="loading"
row-key="id"
>
<template #actions="{ record }">
<a-space>
<a-button
type="primary"
size="small"
@click="editType(record)"
>
编辑
</a-button>
<a-button
type="primary"
status="danger"
size="small"
@click="deleteType(record)"
>
删除
</a-button>
</a-space>
</template>
</a-table>
</div>
<!-- 添加/编辑保险类型模态框 -->
<a-modal
v-model:visible="typeModalVisible"
:title="isEdit ? '编辑保险类型' : '添加保险类型'"
width="500px"
@ok="handleTypeSubmit"
@cancel="handleTypeCancel"
>
<a-form
ref="typeFormRef"
:model="typeForm"
:rules="typeRules"
layout="vertical"
>
<a-form-item label="保险类型名称" field="insuranceTypeName">
<a-input v-model="typeForm.insuranceTypeName" placeholder="请输入保险类型名称" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea
v-model="typeForm.description"
placeholder="请输入保险类型描述"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
<a-form-item label="保障范围" field="coverage">
<a-textarea
v-model="typeForm.coverage"
placeholder="请输入保障范围"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus } from '@arco-design/web-vue/es/icon'
import * as InsuranceTypeAPI from '@/apis/insurance-type'
//
interface InsuranceType {
id?: string
insuranceTypeName: string
description: string
coverage?: string
}
//
const columns = [
{
title: '保险类型名称',
dataIndex: 'insuranceTypeName',
width: 150,
},
{
title: '描述',
dataIndex: 'description',
width: 200,
},
{
title: '保障范围',
dataIndex: 'coverage',
width: 300,
},
{
title: '操作',
slotName: 'actions',
width: 150,
},
]
//
const loading = ref(false)
const typeList = ref<InsuranceType[]>([])
const typeModalVisible = ref(false)
const isEdit = ref(false)
const typeFormRef = ref()
//
const typeForm = reactive({
id: '',
insuranceTypeName: '',
description: '',
coverage: ''
})
//
const typeRules = {
insuranceTypeName: [
{ required: true, message: '请输入保险类型名称' },
],
description: [
{ required: true, message: '请输入保险类型描述' },
],
}
//
const getTypeList = async () => {
try {
loading.value = true
const response = await InsuranceTypeAPI.getInsuranceTypeList()
console.log('保险类型列表响应:', response)
if (response.data) {
typeList.value = response.data || []
} else {
Message.error('获取保险类型列表失败')
}
} catch (error) {
console.error('获取保险类型列表失败:', error)
Message.error('获取保险类型列表失败')
} finally {
loading.value = false
}
}
//
const showAddTypeModal = () => {
isEdit.value = false
resetForm()
typeModalVisible.value = true
}
//
const editType = (record: InsuranceType) => {
isEdit.value = true
Object.assign(typeForm, record)
typeModalVisible.value = true
}
//
const deleteType = (record: InsuranceType) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除保险类型"${record.insuranceTypeName}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
if (record.id) {
await InsuranceTypeAPI.deleteInsuranceType(record.id)
Message.success('保险类型删除成功')
await getTypeList()
}
} catch (error) {
console.error('删除保险类型失败:', error)
Message.error('删除保险类型失败')
}
},
})
}
//
const handleTypeSubmit = async () => {
try {
await typeFormRef.value?.validate()
if (isEdit.value) {
//
if (typeForm.id) {
await InsuranceTypeAPI.updateInsuranceType(typeForm.id, typeForm)
Message.success('保险类型信息更新成功')
}
} else {
//
await InsuranceTypeAPI.createInsuranceType(typeForm)
Message.success('保险类型添加成功')
}
typeModalVisible.value = false
resetForm()
await getTypeList()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败,请重试')
}
}
//
const handleTypeCancel = () => {
typeModalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(typeForm, {
id: '',
insuranceTypeName: '',
description: '',
coverage: ''
})
typeFormRef.value?.resetFields()
}
//
const initData = () => {
getTypeList()
}
//
onMounted(() => {
initData()
})
</script>
<style scoped>
.container {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.table-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -52,7 +52,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
userName: '',
projectName: '',
startDate: '',

View File

@ -108,7 +108,7 @@ const handleLogin = async () => {
const { rememberMe } = loginConfig.value
loginConfig.value.account = rememberMe ? form.account : ''
await router.push({
path: (redirect as string) || '/',
path: (redirect as string) || '/project-management/projects/initiation',
query: {
...othersQuery,
},

View File

@ -28,7 +28,7 @@ const handleSocialLogin = () => {
.then(() => {
tabsStore.reset()
router.push({
path: (redirect as string) || '/',
path: (redirect as string) || '/project-management/bidding/tender-documents',
query: {
...othersQuery,
},

View File

@ -0,0 +1,502 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">数据入库 - 1号机组叶片A型检查</h2>
</div>
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="图片" title="图片">
<div class="tab-content">
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
:custom-request="customUpload"
:show-file-list="false"
multiple
:accept="getAcceptType()"
@change="handleFileChange"
drag
class="upload-dragger"
>
<div class="upload-content">
<div class="upload-icon">
<icon-upload :size="48" />
</div>
<div class="upload-text">
<p class="primary-text">将文件拖拽到此处<span class="link-text">点击上传</span></p>
<p class="secondary-text">支持上传任意不超过100MB格式文件</p>
</div>
</div>
</a-upload>
</div>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<a-button
type="primary"
@click="startUpload"
:loading="uploading"
:disabled="uploadQueue.length === 0"
>
开始上传
</a-button>
<a-button @click="clearFiles">清空文件</a-button>
<a-button @click="batchImport">批量导入...</a-button>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<h3 class="section-title">已上传数据</h3>
<a-table
:columns="fileColumns"
:data="uploadedFiles"
:pagination="false"
:scroll="{ x: '100%' }"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)" size="small">
{{ record.status }}
</a-tag>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">预览</a-button>
<a-button size="small" status="danger" @click="deleteFile(record)">删除</a-button>
</a-space>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="video" tab="视频" title="视频">
<div class="tab-content">
<a-empty description="视频上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="audio" tab="语音" title="语音">
<div class="tab-content">
<a-empty description="语音上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="document" tab="文档" title="文档">
<div class="tab-content">
<a-empty description="文档上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="other" tab="其他" title="其他">
<div class="tab-content">
<a-empty description="其他文件上传功能开发中..." />
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 文件预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
title="文件预览"
:width="800"
:footer="false"
>
<div class="preview-container">
<div v-if="previewFileData && previewFileData.type === 'image'" class="image-preview">
<img :src="previewFileData.url" :alt="previewFileData.name" style="max-width: 100%; height: auto;" />
</div>
<div v-else-if="previewFileData && previewFileData.type === 'video'" class="video-preview">
<video :src="previewFileData.url" controls style="max-width: 100%; height: auto;" />
</div>
<div v-else class="file-info">
<p>文件名{{ previewFileData?.name }}</p>
<p>文件类型{{ previewFileData?.type }}</p>
<p>文件大小{{ formatFileSize(previewFileData?.size) }}</p>
</div>
</div>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
//
const activeTab = ref('image')
//
const uploading = ref(false)
const uploadQueue = ref<any[]>([])
//
const previewModalVisible = ref(false)
const previewFileData = ref<any>(null)
//
const uploadedFiles = ref([
{
id: 1,
name: 'IMG_20231105_1430.jpg',
type: 'image',
size: 3355443, // 3.2MB
uploadTime: '2023-11-05 14:32',
status: '成功',
url: '/api/files/IMG_20231105_1430.jpg'
},
{
id: 2,
name: 'VID_20231106_0915.mp4',
type: 'video',
size: 47185920, // 45.6MB
uploadTime: '2023-11-06 09:18',
status: '成功',
url: '/api/files/VID_20231106_0915.mp4'
},
{
id: 3,
name: 'IMG_20231107_1645.jpg',
type: 'image',
size: 2936013, // 2.8MB
uploadTime: '2023-11-07 16:48',
status: '成功',
url: '/api/files/IMG_20231107_1645.jpg'
}
])
//
const fileColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'name', width: 250 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 100 },
{ title: '大小', dataIndex: 'size', slotName: 'size', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const getAcceptType = () => {
const typeMap: Record<string, string> = {
'image': 'image/*',
'video': 'video/*',
'audio': 'audio/*',
'document': '.pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx',
'other': '*'
}
return typeMap[activeTab.value] || '*'
}
//
const getFileTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'image': 'blue',
'video': 'green',
'audio': 'orange',
'document': 'purple',
'other': 'gray'
}
return colorMap[type] || 'gray'
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'成功': 'green',
'失败': 'red',
'上传中': 'blue',
'等待': 'orange'
}
return colorMap[status] || 'gray'
}
//
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
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]
}
//
const customUpload = (option: any) => {
const { file } = option
//
if (file.size > 100 * 1024 * 1024) {
Message.error('文件大小不能超过100MB')
return
}
//
uploadQueue.value.push({
file,
name: file.name,
size: file.size,
type: getFileType(file.name),
status: '等待'
})
Message.success(`文件 ${file.name} 已添加到上传队列`)
}
//
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension || '')) {
return 'image'
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(extension || '')) {
return 'video'
} else if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension || '')) {
return 'audio'
} else if (['pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension || '')) {
return 'document'
} else {
return 'other'
}
}
//
const handleFileChange = (fileList: any[]) => {
//
}
//
const startUpload = async () => {
if (uploadQueue.value.length === 0) {
Message.warning('请先选择要上传的文件')
return
}
uploading.value = true
try {
//
for (let i = 0; i < uploadQueue.value.length; i++) {
const queueItem = uploadQueue.value[i]
queueItem.status = '上传中'
//
await new Promise(resolve => setTimeout(resolve, 1000))
//
uploadedFiles.value.push({
id: Date.now() + i,
name: queueItem.name,
type: queueItem.type,
size: queueItem.size,
uploadTime: new Date().toLocaleString(),
status: '成功',
url: `/api/files/${queueItem.name}`
})
}
uploadQueue.value = []
Message.success('所有文件上传完成')
} catch (error) {
Message.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
//
const clearFiles = () => {
uploadQueue.value = []
Message.success('已清空文件队列')
}
//
const batchImport = () => {
Message.info('批量导入功能开发中...')
}
//
const previewFile = (file: any) => {
previewFileData.value = file
previewModalVisible.value = true
}
//
const deleteFile = (file: any) => {
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
Message.success('文件已删除')
}
}
</script>
<style scoped lang="less">
.data-storage-container {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.page-header {
margin-bottom: 16px;
.page-title {
font-size: 20px;
font-weight: 500;
color: #262626;
margin: 0;
}
}
.tabs-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tab-content {
margin-top: 16px;
}
.upload-section {
margin-bottom: 16px;
.upload-dragger {
:deep(.arco-upload-drag) {
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
padding: 40px;
text-align: center;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
background: #f0f7ff;
}
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.upload-icon {
color: #bfbfbf;
font-size: 48px;
}
.upload-text {
.primary-text {
font-size: 16px;
color: #595959;
margin: 0 0 8px 0;
.link-text {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.secondary-text {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.uploaded-files-section {
.section-title {
font-size: 16px;
font-weight: 500;
color: #262626;
margin: 0 0 16px 0;
}
}
.preview-container {
text-align: center;
.image-preview,
.video-preview {
max-height: 500px;
overflow: hidden;
}
.file-info {
text-align: left;
p {
margin: 8px 0;
font-size: 14px;
color: #595959;
}
}
}
:deep(.arco-tabs-nav) {
margin-bottom: 0;
}
:deep(.arco-tabs-tab) {
font-size: 14px;
padding: 8px 16px;
}
:deep(.arco-table-th) {
background-color: #fafafa;
color: #8c8c8c;
font-weight: 500;
}
:deep(.arco-table-td) {
padding: 12px 16px;
}
:deep(.arco-tag) {
border-radius: 4px;
font-size: 12px;
}
:deep(.arco-btn-size-small) {
padding: 2px 8px;
font-size: 12px;
}
:deep(.arco-upload-drag:hover) {
border-color: #1890ff;
}
</style>

View File

@ -67,7 +67,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
droneName: '',
model: '',
status: '',

View File

@ -55,7 +55,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
deviceName: '',
location: '',
status: '',

View File

@ -67,7 +67,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
projectName: '',
noticeCode: '',
sendStatus: '',

View File

@ -67,7 +67,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
projectName: '',
bidCode: '',
status: '',

View File

@ -67,7 +67,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
projectName: '',
tenderCode: '',
status: '',

View File

@ -83,7 +83,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
projectName: '',
costType: '',
costCategory: '',

View File

@ -72,7 +72,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
contractName: '',
contractCode: '',
supplier: '',

View File

@ -72,7 +72,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
contractName: '',
contractCode: '',
client: '',

View File

@ -81,7 +81,7 @@ import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
const searchForm = reactive({
let searchForm = reactive({
projectName: '',
projectCode: '',
projectType: '',

View File

@ -0,0 +1,329 @@
<template>
<div class="project-table-container">
<a-table
:data="data"
:columns="columns"
:loading="loading"
:pagination="false"
:scroll="{ x: '100%', y: '100%' }"
row-key="id"
:stripe="false"
size="large"
>
<!-- 项目名称 -->
<template #projectName="{ record }">
<div class="project-name">
<span class="name-text">{{ record.projectName }}</span>
</div>
</template>
<!-- 机组数量 -->
<template #unitCount="{ record }">
<span class="unit-count">{{ record.unitCount }}</span>
</template>
<!-- 开始日期 -->
<template #startDate="{ record }">
<span class="date-text">{{ record.startDate }}</span>
</template>
<!-- 结束日期 -->
<template #endDate="{ record }">
<span class="date-text">{{ record.endDate }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 进度 -->
<template #progress="{ record }">
<div class="progress-container">
<a-progress
:percent="record.progress"
:color="getProgressColor(record.progress)"
size="medium"
:show-text="false"
:stroke-width="6"
/>
<span class="progress-text">{{ record.progress }}%</span>
</div>
</template>
<!-- 操作 -->
<template #action="{ record }">
<div class="action-buttons">
<a-button size="small" @click="handleView(record)">
查看
</a-button>
<a-button
type="primary"
size="small"
@click="handleEnter(record)"
>
进入
</a-button>
</div>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectData } from '../types'
interface Props {
data: ProjectData[]
loading?: boolean
}
interface Emits {
(e: 'view', project: ProjectData): void
(e: 'enter', project: ProjectData): void
}
const props = withDefaults(defineProps<Props>(), {
loading: false
})
const emit = defineEmits<Emits>()
//
const columns: TableColumnData[] = [
{
title: '项目名称',
dataIndex: 'projectName',
slotName: 'projectName',
width: 300,
ellipsis: true,
tooltip: true
},
{
title: '机组数量',
dataIndex: 'unitCount',
slotName: 'unitCount',
width: 120,
align: 'center'
},
{
title: '开始日期',
dataIndex: 'startDate',
slotName: 'startDate',
width: 150,
align: 'center'
},
{
title: '结束日期',
dataIndex: 'endDate',
slotName: 'endDate',
width: 150,
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 120,
align: 'center'
},
{
title: '进度',
dataIndex: 'progress',
slotName: 'progress',
width: 200,
align: 'center'
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 150,
align: 'center',
fixed: 'right'
}
]
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'not_started': 'gray',
'in_progress': 'blue',
'completed': 'green',
'paused': 'orange',
'cancelled': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'not_started': '未开始',
'in_progress': '进行中',
'completed': '已完成',
'paused': '已暂停',
'cancelled': '已取消'
}
return textMap[status] || status
}
//
const getProgressColor = (progress: number) => {
if (progress === 0) return '#d9d9d9'
if (progress === 100) return '#52c41a'
if (progress >= 70) return '#1890ff'
if (progress >= 40) return '#faad14'
return '#ff4d4f'
}
//
const handleView = (record: ProjectData) => {
emit('view', record)
}
const handleEnter = (record: ProjectData) => {
emit('enter', record)
}
</script>
<style scoped lang="less">
.project-table-container {
height: 100%;
background: #fff;
border-radius: 8px;
}
.project-name {
.name-text {
font-weight: 500;
color: #333;
}
}
.unit-count {
font-weight: 600;
color: #1890ff;
}
.date-text {
color: #666;
font-size: 13px;
}
.progress-container {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.progress-text {
font-size: 12px;
color: #666;
font-weight: 500;
min-width: 35px;
text-align: right;
}
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
:deep(.arco-table) {
.arco-table-thead {
.arco-table-th {
background-color: #fafafa;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e8e8e8;
}
}
.arco-table-tbody {
.arco-table-tr {
&:hover {
background-color: #f5f5f5;
}
}
.arco-table-td {
border-bottom: 1px solid #f0f0f0;
padding: 16px 12px;
}
}
}
:deep(.arco-tag) {
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
:deep(.arco-progress-line-outer) {
border-radius: 4px;
}
:deep(.arco-progress-line-inner) {
border-radius: 4px;
transition: all 0.3s ease;
}
//
:deep(.arco-tag-gray) {
background-color: #f5f5f5;
border-color: #d9d9d9;
color: #666;
}
:deep(.arco-tag-blue) {
background-color: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
}
:deep(.arco-tag-green) {
background-color: #f6ffed;
border-color: #b7eb8f;
color: #52c41a;
}
:deep(.arco-tag-orange) {
background-color: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
}
:deep(.arco-tag-red) {
background-color: #fff1f0;
border-color: #ffccc7;
color: #ff4d4f;
}
//
@media (max-width: 768px) {
:deep(.arco-table-td) {
padding: 12px 8px;
font-size: 14px;
}
.action-buttons {
flex-direction: column;
gap: 4px;
}
.progress-container {
gap: 8px;
.progress-text {
font-size: 11px;
}
}
}
</style>

View File

@ -0,0 +1,268 @@
<template>
<GiPageLayout>
<div class="project-list-container">
<!-- 顶部搜索和操作区域 -->
<div class="header-section">
<div class="search-area">
<a-input-search
v-model="searchKeyword"
placeholder="搜索项目名称"
style="width: 300px"
@search="handleSearch"
@clear="handleClear"
allow-clear
>
<template #prefix>
<GiSvgIcon name="search" />
</template>
</a-input-search>
</div>
<div class="action-area">
<a-button @click="refreshData">
<template #icon>
<GiSvgIcon name="refresh" />
</template>
刷新
</a-button>
<a-button type="primary" @click="openAddModal">
<template #icon>
<GiSvgIcon name="plus" />
</template>
新建项目
</a-button>
</div>
</div>
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs
v-model:active-key="activeTab"
type="line"
size="large"
@change="handleTabChange"
>
<a-tab-pane key="all" tab="全部项目" title="全部项目">
<ProjectTable
:data="filteredProjects"
:loading="loading"
@view="handleView"
@enter="handleEnter"
/>
</a-tab-pane>
<a-tab-pane key="my" tab="我的项目" title="我的项目">
<ProjectTable
:data="myProjects"
:loading="loading"
@view="handleView"
@enter="handleEnter"
/>
</a-tab-pane>
<a-tab-pane key="pending" tab="待开工项目" title="待开工项目">
<ProjectTable
:data="pendingProjects"
:loading="loading"
@view="handleView"
@enter="handleEnter"
/>
</a-tab-pane>
</a-tabs>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import ProjectTable from './components/ProjectTable.vue'
import type { ProjectData, TabKey } from './types'
import { ProjectStatus } from './types'
defineOptions({ name: 'ProjectList' })
//
const loading = ref(false)
const activeTab = ref<TabKey>('all')
const searchKeyword = ref('')
//
const projectList = ref<ProjectData[]>([
{
id: 1,
projectName: 'A风场2023年检查',
unitCount: 15,
startDate: '2023-10-01',
endDate: '2023-12-31',
status: ProjectStatus.IN_PROGRESS,
progress: 65,
manager: '张三',
isMyProject: true
},
{
id: 2,
projectName: 'B风场维修项目',
unitCount: 8,
startDate: '2023-09-15',
endDate: '2023-11-30',
status: ProjectStatus.IN_PROGRESS,
progress: 45,
manager: '李四',
isMyProject: false
},
{
id: 3,
projectName: 'C风场年度检查',
unitCount: 20,
startDate: '2023-08-01',
endDate: '2023-10-31',
status: ProjectStatus.COMPLETED,
progress: 100,
manager: '王五',
isMyProject: true
},
{
id: 4,
projectName: 'D风场初步检查',
unitCount: 12,
startDate: '2023-11-10',
endDate: '2023-12-15',
status: ProjectStatus.NOT_STARTED,
progress: 0,
manager: '赵六',
isMyProject: false
}
])
// -
const filteredProjects = computed(() => {
if (!searchKeyword.value) {
return projectList.value
}
return projectList.value.filter(project =>
project.projectName.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// -
const myProjects = computed(() => {
return filteredProjects.value.filter(project => project.isMyProject)
})
// -
const pendingProjects = computed(() => {
return filteredProjects.value.filter(project => project.status === 'not_started')
})
//
const handleSearch = (value: string) => {
searchKeyword.value = value
}
const handleClear = () => {
searchKeyword.value = ''
}
//
const handleTabChange = (key: string) => {
activeTab.value = key as TabKey
}
//
const refreshData = async () => {
loading.value = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 800))
Message.success('数据已刷新')
} catch (error) {
Message.error('刷新失败')
} finally {
loading.value = false
}
}
//
const openAddModal = () => {
Message.info('打开新建项目对话框')
}
//
const handleView = (project: any) => {
Message.info(`查看项目: ${project.projectName}`)
}
//
const handleEnter = (project: any) => {
Message.info(`进入项目: ${project.projectName}`)
}
onMounted(() => {
//
})
</script>
<style scoped lang="less">
.project-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 0 4px;
}
.search-area {
flex: 1;
}
.action-area {
display: flex;
gap: 12px;
align-items: center;
}
.tabs-section {
flex: 1;
min-height: 0;
}
:deep(.arco-tabs-content) {
height: 100%;
}
:deep(.arco-tabs-pane) {
height: 100%;
}
:deep(.arco-tabs-nav-tab) {
font-weight: 500;
font-size: 16px;
}
:deep(.arco-tabs-nav-tab-list) {
padding: 0 4px;
}
//
@media (max-width: 768px) {
.header-section {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.search-area {
flex: none;
}
.action-area {
justify-content: center;
}
}
</style>

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