diff --git a/.env.development b/.env.development index 8b0f18b..b362568 100644 --- a/.env.development +++ b/.env.development @@ -4,8 +4,8 @@ VITE_API_PREFIX = '/dev-api' # 接口地址 # VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/' -# VITE_API_BASE_URL = 'http://localhost:8888/' -VITE_API_BASE_URL = 'http://10.18.34.163:8888/' +VITE_API_BASE_URL = 'http://localhost:8888/' +# VITE_API_BASE_URL = 'http://10.18.34.163:8888/' # VITE_API_BASE_URL = 'http://10.18.34.213:8888/' # 接口地址 (WebSocket) diff --git a/src/apis/equipment/procurement.ts b/src/apis/equipment/procurement.ts index dd8b3f6..d58e7d1 100644 --- a/src/apis/equipment/procurement.ts +++ b/src/apis/equipment/procurement.ts @@ -99,6 +99,37 @@ export const equipmentProcurementApi = { return http.get>(`/equipment/procurement/detail/${equipmentId}`) }, + /** + * 确认收货(扩展版,支持完整设备数据) + */ + receiveGoods: (equipmentId: string, data: ReceiptRequest) => { + console.log('📦 收货API被调用,设备ID:', equipmentId) + console.log('📦 收货数据:', data) + + return http.post>(`/equipment/procurement/receipt/${equipmentId}`, data) + }, + + /** + * 获取收货详情 + */ + getReceiptDetail: (equipmentId: string) => { + return http.get>(`/equipment/procurement/receipt/${equipmentId}`) + }, + + /** + * 执行付款 + */ + makePayment: (equipmentId: string, data: PaymentRequest) => { + return http.post>(`/equipment/procurement/payment/${equipmentId}`, data) + }, + + /** + * 获取支付详情 + */ + getPaymentDetail: (equipmentId: string) => { + return http.get>(`/equipment/procurement/payment/${equipmentId}`) + }, + /** * 获取采购统计信息 */ @@ -117,9 +148,6 @@ export const equipmentProcurementApi = { * 导出设备采购记录 */ export: (params: EquipmentListReq) => { - return http.get('/equipment/procurement/export', { - params, - responseType: 'blob' - }) + return http.get('/equipment/procurement/export', params, { responseType: 'blob' }) } } \ No newline at end of file diff --git a/src/apis/equipment/type.ts b/src/apis/equipment/type.ts index 2bd5a1e..f9bf6d9 100644 --- a/src/apis/equipment/type.ts +++ b/src/apis/equipment/type.ts @@ -101,28 +101,28 @@ export interface EquipmentResp { /** 资产编号 */ assetCode?: string /** 设备名称 */ - equipmentName: string + equipmentName?: string /** 设备类型 */ - equipmentType: string - /** 设备类型描述 */ + equipmentType?: string + /** 设备类型标签 */ equipmentTypeLabel?: string /** 设备型号 */ - equipmentModel: string - /** 设备SN */ - equipmentSn: string + equipmentModel?: string + /** 设备序列号 */ + equipmentSn?: string /** 品牌 */ brand?: string /** 配置规格/参数 */ specification?: string /** 设备状态 */ - equipmentStatus: string - /** 设备状态描述 */ + equipmentStatus?: string + /** 设备状态标签 */ equipmentStatusLabel?: string /** 使用状态 */ - useStatus: string + useStatus?: string /** 位置状态 */ locationStatus?: string - /** 位置状态描述 */ + /** 位置状态标签 */ locationStatusLabel?: string /** 设备当前物理位置 */ physicalLocation?: string @@ -130,7 +130,7 @@ export interface EquipmentResp { responsiblePerson?: string /** 健康状态 */ healthStatus?: string - /** 健康状态描述 */ + /** 健康状态标签 */ healthStatusLabel?: string /** 采购时间 */ purchaseTime?: string @@ -138,13 +138,13 @@ export interface EquipmentResp { inStockTime?: string /** 启用时间 */ activationTime?: string - /** 预计报废时间 */ + /** 预期报废时间 */ expectedScrapTime?: string /** 实际报废时间 */ actualScrapTime?: string /** 状态变更时间 */ statusChangeTime?: string - /** 采购订单 */ + /** 采购订单号 */ purchaseOrder?: string /** 供应商名称 */ supplierName?: string @@ -158,9 +158,9 @@ export interface EquipmentResp { depreciationYears?: number /** 残值 */ salvageValue?: number - /** 保修截止日期 */ + /** 保修到期日期 */ warrantyExpireDate?: string - /** 上次维护日期 */ + /** 最后维护日期 */ lastMaintenanceDate?: string /** 下次维护日期 */ nextMaintenanceDate?: string @@ -176,7 +176,7 @@ export interface EquipmentResp { projectName?: string /** 使用人ID */ userId?: string - /** 使用人 */ + /** 使用人姓名 */ name?: string /** 创建时间 */ createTime?: string @@ -194,12 +194,14 @@ export interface EquipmentResp { inventoryBasis?: string /** 动态记录 */ dynamicRecord?: string - /** 采购状态 */ procurementStatus?: string - /** 审批状态 */ approvalStatus?: string + /** 收货状态 */ + receiptStatus?: string + /** 支付状态 */ + paymentStatus?: string } /** @@ -406,3 +408,221 @@ export interface EquipmentApprovalResp { /** 更新时间 */ updateTime: string } + +/** + * 收货请求参数(扩展版,包含完整设备信息) + */ +export interface ReceiptRequest { + // 收货特有信息 + receiptTime: string + receiptPerson: string + receiptQuantity: number + receiptRemark?: string + appearanceCheck: string + functionTest: string + packageIntegrity: string + accessoryIntegrity: string + checkResult: 'PASS' | 'FAIL' | 'CONDITIONAL' + checkRemark?: string + storageLocation: string + storageManager: string + + // 设备基本信息(从采购数据继承) + equipmentName?: string + equipmentModel?: string + equipmentType?: string + equipmentSn?: string + brand?: string + specification?: string + assetCode?: string + + // 采购信息(从采购数据继承) + purchaseOrder?: string + supplierName?: string + purchasePrice?: number + purchaseTime?: string + quantity?: number + unitPrice?: number + totalPrice?: number + + // 入库信息 + inStockTime?: string + physicalLocation?: string + locationStatus?: string + responsiblePerson?: string + inventoryBarcode?: string + + // 状态信息 + equipmentStatus?: string + useStatus?: string + healthStatus?: string + receiptStatus?: string + + // 其他管理信息 + depreciationMethod?: string + depreciationYears?: number + salvageValue?: number + currentNetValue?: number + + // 系统字段 + createTime?: string + updateTime?: string +} + +/** + * 收货详情响应 + */ +export interface ReceiptDetail { + /** 设备ID */ + equipmentId: string + /** 设备名称 */ + equipmentName: string + /** 设备类型 */ + equipmentType: string + /** 设备型号 */ + equipmentModel: string + /** 品牌 */ + brand: string + /** 供应商 */ + supplierName: string + /** 采购订单 */ + purchaseOrder: string + /** 收货状态 */ + receiptStatus: string + /** 收货时间 */ + receiptTime: string + /** 收货人 */ + receiptPerson: string + /** 收货数量 */ + receiptQuantity: number + /** 收货备注 */ + receiptRemark?: string + /** 外观检查 */ + appearanceCheck: string + /** 功能测试 */ + functionTest: string + /** 包装完整性 */ + packageIntegrity: string + /** 配件完整性 */ + accessoryIntegrity: string + /** 检查结果 */ + checkResult: string + /** 检查备注 */ + checkRemark?: string + /** 入库状态 */ + storageStatus: string + /** 入库时间 */ + storageTime: string + /** 入库位置 */ + storageLocation: string + /** 库管员 */ + storageManager: string +} + +/** + * 支付请求参数 + */ +export interface PaymentRequest { + /** 支付方式 */ + paymentMethod: string + /** 支付金额 */ + paymentAmount: number + /** 支付时间 */ + paymentTime: string + /** 支付人 */ + paymentPerson: string + /** 支付备注 */ + paymentRemark?: string + /** 发票类型 */ + invoiceType: string + /** 发票号码 */ + invoiceNumber: string + /** 开票日期 */ + invoiceDate: string + /** 发票金额 */ + invoiceAmount: number + /** 税率 */ + taxRate: number + /** 税额 */ + taxAmount: number + /** 不含税金额 */ + amountWithoutTax: number + /** 合同编号 */ + contractNumber: string + /** 合同金额 */ + contractAmount: number + /** 签订日期 */ + contractDate: string + /** 付款条件 */ + paymentTerms: string + /** 付款期限 */ + paymentDeadline: string +} + +/** + * 支付详情响应 + */ +export interface PaymentDetail { + /** 设备ID */ + equipmentId: string + /** 设备名称 */ + equipmentName: string + /** 设备类型 */ + equipmentType: string + /** 设备型号 */ + equipmentModel: string + /** 品牌 */ + brand: string + /** 供应商 */ + supplierName: string + /** 采购订单 */ + purchaseOrder: string + /** 采购价格 */ + purchasePrice: number + /** 采购数量 */ + quantity: number + /** 总金额 */ + totalPrice: number + /** 采购日期 */ + purchaseTime: string + /** 支付状态 */ + paymentStatus: string + /** 支付方式 */ + paymentMethod: string + /** 支付金额 */ + paymentAmount: number + /** 支付时间 */ + paymentTime: string + /** 支付人 */ + paymentPerson: string + /** 支付备注 */ + paymentRemark?: string + /** 发票状态 */ + invoiceStatus: string + /** 发票类型 */ + invoiceType: string + /** 发票号码 */ + invoiceNumber: string + /** 开票日期 */ + invoiceDate: string + /** 发票金额 */ + invoiceAmount: number + /** 税率 */ + taxRate: number + /** 税额 */ + taxAmount: number + /** 不含税金额 */ + amountWithoutTax: number + /** 合同编号 */ + contractNumber: string + /** 合同状态 */ + contractStatus: string + /** 合同金额 */ + contractAmount: number + /** 签订日期 */ + contractDate: string + /** 付款条件 */ + paymentTerms: string + /** 付款期限 */ + paymentDeadline: string +} diff --git a/src/apis/project/index.ts b/src/apis/project/index.ts index bde6b8a..3471f0c 100644 --- a/src/apis/project/index.ts +++ b/src/apis/project/index.ts @@ -15,6 +15,12 @@ export function getProject(id: string | number) { return http.get(`${BASE_URL}/${id}`) } +/** @desc 获取项目详情(标准详情接口) */ +export function getProjectDetail(id: string | number) { + return http.get(`${BASE_URL}/detail/${id}`) +} + + /** @desc 新增项目 */ export function addProject(data: any) { return http.post(`${BASE_URL}`, data) @@ -49,4 +55,4 @@ export function importProject(file: File) { /** @desc 导出项目 */ export function exportProject(query: T.ProjectQuery) { return http.download(`${BASE_URL}/export`, query) -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/apis/project/type.ts b/src/apis/project/type.ts index 506cb93..5d5f527 100644 --- a/src/apis/project/type.ts +++ b/src/apis/project/type.ts @@ -18,6 +18,8 @@ export interface ProjectResp { projectCategory?: string // 项目类型/服务 projectManagerId?: string // 项目经理ID projectManagerName?: string // 项目经理姓名 + projectOrigin?: string // 项目来源 + projectStaff?: string[] // 施工人员 startDate?: string // 开始日期 endDate?: string // 结束日期 @@ -28,10 +30,10 @@ export interface ProjectResp { coverUrl?: string // 封面URL createDt?: Date updateDt?: Date - + // 为了保持向后兼容,添加一些别名字段 id?: string // projectId的别名 - fieldName?: string // farmName的别名 + fieldName?: string // farmName的别名 fieldLocation?: string // farmAddress的别名 commissionUnit?: string // client的别名 commissionContact?: string // clientContact的别名 @@ -85,7 +87,7 @@ export interface TaskQuery { status?: string } -export interface TaskPageQuery extends TaskQuery, PageQuery {} +export interface TaskPageQuery extends TaskQuery, PageQuery {} // ==================== 人员调度相关类型 ==================== @@ -339,4 +341,4 @@ export interface PageRes { total: number page: number pageSize: number -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/NotificationCenter/index.vue b/src/components/NotificationCenter/index.vue index 7fb4ccf..7230f61 100644 --- a/src/components/NotificationCenter/index.vue +++ b/src/components/NotificationCenter/index.vue @@ -19,7 +19,7 @@ :mask-closable="true" :closable="true" :destroy-on-close="false" - :z-index="999999" + :z-index="1000" class="notification-modal" > @@ -673,7 +673,7 @@ defineExpose({ diff --git a/src/router/route.ts b/src/router/route.ts index c7279f4..05536b6 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -1199,46 +1199,141 @@ export const systemRoutes: RouteRecordRaw[] = [ ], }, { - path: '/chat-platform', - name: 'ChatPlatform', - component: Layout, - redirect: '/chat-platform/options', - meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 }, + path: '/image-detection', + name: 'ImageDetection', + component: Layout, + redirect: '/Image-detection/tower-monitoring/clearance-monitoring', + meta: { + title: '图像检测', + icon: 'monitor', + hidden: false, + sort: 6.5, + }, + children: [ - // { - // path: '/chat-platform/options', - // name: 'ChatOptions', - // component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换 - // meta: { - // title: '二级选项1', - // icon: 'setting', - // hidden: false - // } - // } - ], + { + path: '/image-detection/image-analysis', + name: 'ImageAnalysis', + component: () => import('@/components/ParentView/index.vue'), + meta: { + title: '检查图像分析', + icon: 'line-chart', + hidden: false, + }, + redirect: '/image-detection/image-analysis/defect-detection', + children: [ + {path: '/image-detection/image-analysis/defect-detection', + name: 'DefectDetection', + component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm/index.vue'), + meta: { + title: '缺陷检测', + icon: 'line-chart', + hidden: false, + } + }, + {path: '/image-detection/image-analysis/defect-edit', + name: 'DefectEdit', + component: () => import('@/components/ParentView/index.vue'), + meta: { + title: '缺陷编辑', + icon: 'line-chart', + hidden: false, + } + }, + {path: '/image-detection/image-analysis/generate-reports', + name: 'GenerateReports', + component: () => import('@/views/project-operation-platform/data-processing/report-generation/index.vue'), + meta: { + title: '生成报告', + icon: 'line-chart', + hidden: false, + } + }, + {path: '/image-detection/image-analysis/defect-base', + name: 'DefectBase', + component: () => import('@/views/project-operation-platform/data-processing/standard-info/index.vue'), + meta: { + title: '缺陷标准数据信息库', + icon: 'line-chart', + hidden: false, + } + }, + ] + }, + { + path: '/tower-monitoring', + name: 'TowerMonitoring', + component: () => import('@/components/ParentView/index.vue'), + redirect: '/tower-monitoring/clearance-monitoring', + meta: { + title: '音视频检测', + icon: 'monitor', + hidden: false, + sort: 6.5, }, + children: [ + { + path: '/tower-monitoring/clearance-monitoring', + name: 'ClearanceMonitoring', + component: () => import('@/views/tower-monitoring/deformation.vue'), + meta: { + title: '净空监测', + icon: 'fullscreen', + hidden: false, + }, + }, + { + path: '/tower-monitoring/deformation-monitoring', + name: 'DeformationMonitoring', + component: () => import('@/views/tower-monitoring/clearance.vue'), + meta: { + title: '形变监测', + icon: 'line-chart', + hidden: false, + }, + }, + { + path: '/tower-monitoring/whistle-monitoring', + name: 'WhistleMonitoring', + component: () => import('@/views/tower-monitoring/whistle.vue'), + meta: { + title: '哨声监测', + icon: 'sound', + hidden: false, + }, + }, + { + path: '/tower-monitoring/vibration-monitoring', + name: 'VibrationMonitoring', + component: () => import('@/views/tower-monitoring/vibration.vue'), + meta: { + title: '振动监测', + icon: 'shake', + hidden: false, + }, + }, + ], + }, + { + path: '/image-detection/reporting-center', + name: 'ReportingCenter', + component: () => import('@/views/tower-monitoring/vibration.vue'), + meta: { + title: '报告中心', + icon: 'shake', + hidden: false, + }, + } + ] +}, + // { - // path: '/user/profile', - // name: 'UserProfile', + // path: '/chat-platform', + // name: 'ChatPlatform', // component: Layout, - // redirect: '/user/profile', - // meta: { - // title: '个人中心', - // icon: 'user', - // hidden: false, - // sort: 100, - // }, + // redirect: '/chat-platform/options', + // meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 }, // children: [ - // { - // path: '/user/profile', - // name: 'UsersProfile', - // component: () => import('@/views/user/profile/index.vue'), - // meta: { - // title: '个人中心', - // icon: 'user', - // hidden: false, - // }, - // }, // ], // }, { diff --git a/src/types/components.d.ts b/src/types/components.d.ts index a427ece..7fa6b1b 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -7,70 +7,7 @@ export {} declare module 'vue' { export interface GlobalComponents { - ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default'] - ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default'] - Avatar: typeof import('./../components/Avatar/index.vue')['default'] - Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] - CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] - Chart: typeof import('./../components/Chart/index.vue')['default'] - CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default'] - ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default'] - CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default'] - CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default'] - DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default'] - DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default'] - FilePreview: typeof import('./../components/FilePreview/index.vue')['default'] - GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default'] - GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default'] - GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default'] - GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default'] - GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default'] - GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default'] - GiDot: typeof import('./../components/GiDot/index.tsx')['default'] - GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default'] - GiFooter: typeof import('./../components/GiFooter/index.vue')['default'] - GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default'] - GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default'] - GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default'] - GiIframe: typeof import('./../components/GiIframe/index.vue')['default'] - GiOption: typeof import('./../components/GiOption/index.vue')['default'] - GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default'] - GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default'] - GiSpace: typeof import('./../components/GiSpace/index.vue')['default'] - GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default'] - GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default'] - GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default'] - GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default'] - GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default'] - GiTag: typeof import('./../components/GiTag/index.tsx')['default'] - GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default'] - HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default'] - Icon403: typeof import('./../components/icons/Icon403.vue')['default'] - Icon404: typeof import('./../components/icons/Icon404.vue')['default'] - Icon500: typeof import('./../components/icons/Icon500.vue')['default'] - IconBorders: typeof import('./../components/icons/IconBorders.vue')['default'] - 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'] - NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default'] - ParentView: typeof import('./../components/ParentView/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default'] - SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default'] - TextCopy: typeof import('./../components/TextCopy/index.vue')['default'] - TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default'] - UserSelect: typeof import('./../components/UserSelect/index.vue')['default'] - Verify: typeof import('./../components/Verify/index.vue')['default'] - VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default'] - VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default'] - WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default'] - YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default'] } } diff --git a/src/views/bussiness-data/bussiness.vue b/src/views/bussiness-data/bussiness.vue index 9cebdba..67adb78 100644 --- a/src/views/bussiness-data/bussiness.vue +++ b/src/views/bussiness-data/bussiness.vue @@ -2,7 +2,7 @@ - + + - - - - - - 上传文件 - - - - 新建文件夹 - - + + - -
-
- 文件列表 ({{ fileList.length }}) -
-
- -
-
- - - - - - - - - - - -
- - - -
- 文件名 -
-
-
-
-
-
- -
- 类型 -
-
-
-
-
-
- -
- 大小 -
-
-
-
-
-
- -
- 修改时间 -
-
-
-
-
-
- 操作 -
- - - - - -
- -
- {{ file.fileName || file.name }} -
{{ file.fileName || file.name }}
-
-
-
- - - -
{{ fileTypeText(getFileExtension(file.fileName || file.name)) }}
-
- - - -
{{ formatFileListSize(file.fileSize || file.size) }}
-
- - - -
{{ formatUploadTime(file.uploadTime || file.uploadTime) }}
-
- - - -
- - - - - - - - - - - - -
-
-
-
+ -
- -
- - - - - - +
@@ -408,128 +192,13 @@ - - + - - - -
- - - - - 点击选择文件 - - - - -
- 支持 {{ allowedFileTypesText }} 等格式,单个文件不超过 {{ maxFileSizeText }} -
-
- - -
-
-
- -
-
{{ file.name }}
-
- {{ formatFileSize(file.size) }} - {{ file.error }} -
-
-
- - -
- -
- - -
- - - - - - -
-
-
-
- - - - - 根目录 - - {{ folder.name }} - - - -
-
+ :folder-list="folderList" + :current-folder-id="currentFolderId" + @upload-success="handleUploadSuccess" + /> // 导入核心依赖 import { ref, reactive, onMounted, computed, watch, nextTick, h } from 'vue'; +// 导入子组件 +import FilePagination from './components/FilePagination.vue'; +import FileHeader from './components/FileHeader.vue'; +import FileList from './components/FileList.vue'; +import FileUpload from './components/FileUpload.vue'; import { IconFolder, IconFile, IconPlus, - IconUpload, + IconMenuFold, IconMenuUnfold, IconEye, @@ -589,10 +263,10 @@ import { IconRefresh, IconEdit, IconFolderAdd, - IconStop + } from '@arco-design/web-vue/es/icon'; import { Message, Modal } from '@arco-design/web-vue'; -import axios from 'axios'; + // 导入API import { @@ -603,7 +277,7 @@ import { deleteFolderApi, deleteFileApi, downloadFileApi, - uploadFileApi, + updateFileNameApi, renameFileApi, previewFileApi @@ -656,47 +330,10 @@ const folderRules = { ] }; -// 上传相关状态 -const uploadForm = reactive({ - folderId: '' -}); -const fileListTemp = ref([]); const folderFormRef = ref(null); -const uploadFormRef = ref(null); -const uploadRef = ref(null); const folderColor = 'var(--color-primary)'; const refreshing = ref(false); const folderSubmitting = ref(false); -const uploading = ref(false); -const allowedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt,.jpg,.jpeg,.png,.gif,.bmp,.webp'; -const allowedFileTypesText = 'PDF, Word, Excel, PPT, 压缩文件, 文本文件, 图片文件'; -const maxFileSize = 1000 * 1024 * 1024; // 1000MB -const maxFileSizeText = '1000MB'; -const cancelTokens = ref({}); - -// 计算属性:是否有文件可上传 -const hasFiles = computed(() => { - console.log('=== hasFiles计算属性执行 ==='); - console.log('原始fileListTemp:', fileListTemp.value); - console.log('fileListTemp长度:', fileListTemp.value.length); - - const validFiles = fileListTemp.value.filter(file => { - const isValid = !file.error && file.status !== 'removed' && file.status !== 'canceled'; - console.log(`文件 ${file.name}: error=${file.error}, status=${file.status}, isValid=${isValid}`); - return isValid; - }); - - console.log('过滤后的有效文件:', validFiles); - console.log('有效文件数量:', validFiles.length); - console.log('hasFiles结果:', validFiles.length > 0); - - return validFiles.length > 0; -}); - -// 计算属性:是否可以上传 -const canUpload = computed(() => { - return hasFiles.value && !uploading.value && uploadForm.folderId; -}); // 计算属性:将平铺的文件夹数据转换为树形结构 const folderTreeData = computed(() => { @@ -1031,8 +668,12 @@ const loadFiles = async (folderId) => { if (sortField.value && sortOrder.value) { apiParams.sortField = sortField.value; apiParams.sortOrder = sortOrder.value; + console.log('📤 发送排序参数:', apiParams.sortField, apiParams.sortOrder); + } else { + console.log('📤 未发送排序参数'); } + console.log('📤 发送API参数:', apiParams); const res = await getFilesApi(apiParams); // 根据后端返回的数据结构处理 @@ -1057,21 +698,34 @@ const loadFiles = async (folderId) => { // 排序处理函数 const handleSortChange = (field) => { - const backendField = sortFieldMap[field]; + console.log('=== 排序处理函数被调用 ==='); + console.log('传入的字段:', field); - if (!backendField) return; + const backendField = sortFieldMap[field]; + console.log('映射后的后端字段:', backendField); + + if (!backendField) { + console.log('❌ 找不到对应的后端字段,退出'); + return; + } + + console.log('当前排序字段:', sortField.value); + console.log('当前排序方向:', sortOrder.value); // 切换排序方向 if (sortField.value === backendField) { sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; + console.log('✅ 切换排序方向为:', sortOrder.value); } else { // 新字段,默认降序 sortField.value = backendField; sortOrder.value = 'desc'; + console.log('✅ 设置新排序字段:', sortField.value, '排序方向:', sortOrder.value); } // 重新加载文件列表 if (currentFolderId.value) { + console.log('🔄 重新加载文件列表,文件夹ID:', currentFolderId.value); loadFiles(currentFolderId.value); } }; @@ -1249,7 +903,7 @@ const handleRenameFolder = (folder) => { } const folderId = folder.key || folder.id; - const currentName = folder.title || folder.name; + let currentName = folder.title || folder.name; if (!folderId) { Message.error('文件夹ID不能为空'); @@ -1426,159 +1080,9 @@ const submitFolderForm = async () => { } }; -// 格式化上传时间 -const formatUploadTime = (timeStr) => { - if (!timeStr) return '未知时间'; - const date = new Date(timeStr); - return date.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }); -}; -// 文件变化处理 -const handleFileChange = (info) => { - console.log('=== 文件变化事件 ==='); - console.log('完整info对象:', info); - - // 安全检查:确保 info 存在且是数组 - if (!info || !Array.isArray(info)) { - console.log('❌ info 不存在或不是数组,跳过处理'); - return; - } - - const fileList = info; - console.log('文件列表:', fileList); - console.log('文件列表长度:', fileList.length); - - // 检查是否是组件内部状态触发的(可能是之前的状态) - if (fileList.length === 0) { - console.log('⚠️ 文件列表为空,可能是组件内部状态,跳过处理'); - return; - } - - // 检查是否是对话框刚打开时的触发(可能是之前的状态) - if (!uploadDialogVisible.value) { - console.log('⚠️ 对话框未显示,可能是之前的状态触发,跳过处理'); - return; - } - - // 获取当前已存在的文件UID列表,用于去重 - const existingUids = fileListTemp.value.map(f => f.uid); - console.log('已存在的文件UID:', existingUids); - - // 获取当前文件夹中已存在的文件名列表,用于检查重复 - const currentFolderFiles = fileList.value || []; - const existingFileNames = currentFolderFiles.map(f => f.fileName || f.name); - console.log('当前文件夹中的文件:', existingFileNames); - - // 强制重置上传组件状态 - if (uploadRef.value) { - try { - uploadRef.value.reset(); - console.log('已强制重置上传组件'); - } catch (error) { - console.log('重置上传组件时出错:', error); - } - } - - // 处理新选择的文件 - 支持多文件选择,同时避免重复 - fileList.forEach((file, index) => { - console.log(`处理第${index + 1}个文件:`, file); - console.log('文件名称:', file.name); - console.log('文件大小:', file.size); - console.log('文件UID:', file.uid); - console.log('文件对象结构:', Object.keys(file)); - - // 检查文件是否已存在(去重) - if (existingUids.includes(file.uid)) { - console.log('⚠️ 文件已存在,跳过:', file.name); - return; - } - - // 确保文件对象有正确的属性 - const fileObj = { - uid: file.uid, - name: file.name, - size: file.size || file.file?.size || 0, - type: file.type || file.file?.type || '', - status: 'ready', - error: '', - originFileObj: file.file || file // 保存原始File对象 - }; - - console.log('开始验证新文件...'); - - // 检查文件是否已存在于当前文件夹中 - if (existingFileNames.includes(fileObj.name)) { - fileObj.error = '文件已存在于当前文件夹中'; - console.log('⚠️ 文件已存在于文件夹中:', fileObj.name); - // 显示友好的提示信息 - Message.warning(`文件 "${fileObj.name}" 已存在于当前文件夹中,已跳过`); - return; - } - - // 验证文件 - const isValid = validateFile(fileObj); - console.log('文件验证结果:', isValid); - - if (isValid) { - // 支持多文件:添加到列表 - fileListTemp.value.push(fileObj); - console.log('✅ 成功添加文件到列表:', fileObj.name); - } else { - console.log('❌ 文件验证失败:', fileObj.name, '错误:', fileObj.error); - } - }); - - console.log('=== 当前文件列表状态 ==='); - console.log('fileListTemp长度:', fileListTemp.value.length); - console.log('fileListTemp内容:', fileListTemp.value); - console.log('hasFiles计算结果:', hasFiles.value); -}; -// 文件验证 -const validateFile = (file) => { - console.log('=== 开始验证文件 ==='); - console.log('验证文件:', file.name); - console.log('文件大小:', file.size); - - // 清除之前的错误 - file.error = ''; - - // 验证文件类型 - const ext = getFileExtension(file.name).toLowerCase(); - console.log('文件扩展名:', ext); - - const allowedExts = allowedFileTypes - .split(',') - .map(type => type.toLowerCase().replace(/^\./, '')); - - console.log('允许的扩展名:', allowedExts); - console.log('扩展名是否匹配:', allowedExts.includes(ext)); - - if (!allowedExts.includes(ext)) { - file.error = `不支持的文件类型,支持: ${allowedFileTypesText}`; - console.log('❌ 文件类型验证失败:', file.error); - return false; - } - - // 验证文件大小 - console.log('文件大小验证:', file.size, '<=', maxFileSize); - if (file.size > maxFileSize) { - file.error = `文件过大,最大支持 ${maxFileSizeText}`; - console.log('❌ 文件大小验证失败:', file.error); - return false; - } - - console.log('✅ 文件验证通过'); - return true; -}; + // 获取文件扩展名 const getFileExtension = (fileName) => { @@ -1586,157 +1090,6 @@ const getFileExtension = (fileName) => { return lastDotIndex > 0 ? fileName.slice(lastDotIndex + 1) : ''; }; -// 获取文件图标颜色 -const fileColor = (extension) => { - const colorMap = { - pdf: '#ff4d4f', - doc: '#1890ff', - docx: '#1890ff', - xls: '#52c41a', - xlsx: '#52c41a', - ppt: '#faad14', - pptx: '#faad14', - zip: '#722ed1', - txt: '#8c8c8c', - // 图片格式颜色 - jpg: '#52c41a', - jpeg: '#52c41a', - png: '#1890ff', - gif: '#faad14', - bmp: '#722ed1', - webp: '#13c2c2' - }; - return colorMap[extension.toLowerCase()] || 'var(--color-text-3)'; -}; - - - -// 移除文件 -const removeFile = (file) => { - fileListTemp.value = fileListTemp.value.filter(f => f.uid !== file.uid); - - // 如果是正在上传的文件,取消请求 - if (file.status === 'uploading' && cancelTokens.value[file.uid]) { - cancelTokens.value[file.uid].cancel('上传已取消'); - delete cancelTokens.value[file.uid]; - } -}; - -// 取消上传 -const cancelUpload = (file) => { - if (cancelTokens.value[file.uid]) { - cancelTokens.value[file.uid].cancel('上传已取消'); - file.status = 'canceled'; - } -}; - -// 提交上传 -const handleUploadSubmit = async () => { - // 过滤有效文件 - const validFiles = fileListTemp.value.filter(file => - !file.error && file.status !== 'removed' && file.status !== 'canceled' - ); - - if (validFiles.length === 0) { - Message.warning('请选择有效的文件'); - return; - } - - // 验证文件夹ID - if (!uploadForm.folderId) { - Message.warning('请选择目标文件夹'); - return; - } - - uploading.value = true; - let hasError = false; - let hasFileExists = false; - - for (const fileItem of validFiles) { - // 获取原始File对象 - const realFile = fileItem.originFileObj || fileItem; - - if (!realFile) { - hasError = true; - continue; - } - - fileItem.status = 'uploading'; - fileItem.percent = 0; - - // 创建取消令牌 - const source = axios.CancelToken.source(); - cancelTokens.value[fileItem.uid] = source; - - // 调用API - const result = await uploadFileApi( - realFile, - Number(uploadForm.folderId), - (progressEvent) => { - if (progressEvent.lengthComputable) { - fileItem.percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); - } - }, - source.token - ); - - // 检查上传结果 - if (result.code === 200) { - fileItem.status = 'success'; - fileItem.percent = 100; - } else if (result.code === 400 && result.msg && result.msg.includes('已存在')) { - // 文件已存在的情况 - fileItem.status = 'error'; - fileItem.error = '文件已存在'; - hasFileExists = true; - } else { - fileItem.status = 'error'; - fileItem.error = result.msg || '上传失败'; - hasError = true; - } - } - - // 根据结果显示相应的消息 - if (hasFileExists && !hasError) { - Message.warning('文件已存在'); - } else if (hasError) { - Message.error('上传失败'); - } else { - Message.success('上传成功'); - // 刷新当前文件夹文件列表 - if (currentFolderId.value === uploadForm.folderId) { - loadFiles(currentFolderId.value); - } - } - - resetUpload(); -}; - -// 重置上传表单 -const resetUpload = () => { - console.log('=== 重置上传表单 ==='); - - // 取消所有正在进行的上传 - Object.values(cancelTokens.value).forEach(source => { - source.cancel('上传已取消'); - }); - - // 重置所有状态 - uploadDialogVisible.value = false; - uploadForm.folderId = currentFolderId.value || ''; - fileListTemp.value = []; - cancelTokens.value = {}; - uploading.value = false; - - // 清空上传组件 - if (uploadRef.value) { - uploadRef.value.reset(); - console.log('已重置上传组件'); - } - - console.log('上传表单重置完成'); -}; - // 预览文件 const handlePreview = async (file) => { try { @@ -2307,60 +1660,96 @@ const handleDelete = (file) => { }); }; -// 格式化文件大小 -const formatFileSize = (fileSize) => { - const size = Number(fileSize); - if (isNaN(size) || size < 0) return '未知'; - - 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 sidebarCollapsed = ref(false); + +// 侧边栏宽度控制 +const sidebarWidth = ref(260); // 默认宽度 +const isResizing = ref(false); +const startX = ref(0); +const startWidth = ref(0); + +// 从localStorage加载保存的宽度 +const loadSavedWidth = () => { + try { + const savedWidth = localStorage.getItem('bussiness-sidebar-width'); + if (savedWidth) { + const width = parseInt(savedWidth); + if (width >= 200 && width <= 500) { + sidebarWidth.value = width; + } + } + } catch (error) { + console.warn('加载侧边栏宽度失败:', error); + } }; -// 专门用于文件列表的格式化函数(假设后端返回的是KB单位) -const formatFileListSize = (fileSize) => { - const size = Number(fileSize); - if (isNaN(size) || size < 0) return '未知'; - - // 假设后端返回的是KB单位 - if (size < 1024) { - return `${size} KB`; - } else if (size < 1024 * 1024) { - return `${(size / 1024).toFixed(1)} MB`; - } else { - return `${(size / (1024 * 1024)).toFixed(1)} GB`; +// 保存宽度到localStorage +const saveWidth = (width) => { + try { + localStorage.setItem('bussiness-sidebar-width', width.toString()); + } catch (error) { + console.warn('保存侧边栏宽度失败:', error); } }; -const fileTypeText = (type) => { - const types = { - pdf: 'PDF文档', - doc: 'Word文档', - docx: 'Word文档', - xls: 'Excel表格', - xlsx: 'Excel表格', - ppt: 'PPT演示', - pptx: 'PPT演示', - zip: '压缩文件', - txt: '文本文件', - // 图片格式 - jpg: 'JPG图片', - jpeg: 'JPEG图片', - png: 'PNG图片', - gif: 'GIF图片', - bmp: 'BMP图片', - webp: 'WebP图片', - unknown: '未知类型' - }; +// 开始拖拽调整大小 +const startResize = (event) => { + event.preventDefault(); + isResizing.value = true; + startX.value = event.type === 'mousedown' ? event.clientX : event.touches[0].clientX; + startWidth.value = sidebarWidth.value; - return types[type] || type; + // 添加事件监听器 + if (event.type === 'mousedown') { + document.addEventListener('mousemove', handleResize); + document.addEventListener('mouseup', stopResize); + } else { + document.addEventListener('touchmove', handleResize); + document.addEventListener('touchend', stopResize); + } + + // 添加样式 + document.body.classList.add('resizing'); }; -// 侧边栏控制 -const sidebarCollapsed = ref(false); +// 处理拖拽调整 +const handleResize = (event) => { + if (!isResizing.value) return; + + const currentX = event.type === 'mousemove' ? event.clientX : event.touches[0].clientX; + const deltaX = currentX - startX.value; + const newWidth = Math.max(200, Math.min(500, startWidth.value + deltaX)); + + sidebarWidth.value = newWidth; +}; + +// 停止拖拽调整 +const stopResize = () => { + isResizing.value = false; + + // 移除事件监听器 + document.removeEventListener('mousemove', handleResize); + document.removeEventListener('mouseup', stopResize); + document.removeEventListener('touchmove', handleResize); + document.removeEventListener('touchend', stopResize); + + // 移除样式 + document.body.classList.remove('resizing'); + + // 保存宽度 + saveWidth(sidebarWidth.value); +}; // 打开新建文件夹对话框 const handleCreateFolder = () => { @@ -2372,29 +1761,17 @@ const handleCreateFolder = () => { // 打开上传文件对话框 const handleUploadFile = () => { - // 清空文件列表,避免显示之前上传的文件 - fileListTemp.value = []; - - // 重置上传组件状态 - if (uploadRef.value) { - try { - uploadRef.value.reset(); - // 强制清空组件的内部文件列表 - if (uploadRef.value.fileList) { - uploadRef.value.fileList = []; - } - if (uploadRef.value.fileListTemp) { - uploadRef.value.fileListTemp = []; - } - } catch (error) { - console.log('重置上传组件时出错:', error); - } - } - - uploadForm.folderId = currentFolderId.value || ''; uploadDialogVisible.value = true; }; +// 上传成功回调 +const handleUploadSuccess = () => { + // 刷新当前文件夹文件列表 + if (currentFolderId.value) { + loadFiles(currentFolderId.value); + } +}; + // 侧边栏控制函数 const handleSidebarCollapse = (collapsed) => { sidebarCollapsed.value = collapsed; @@ -2411,43 +1788,13 @@ watch(currentFolderId, (newId) => { } }); -// 监听上传对话框显示状态,确保文件列表清空 -watch(uploadDialogVisible, (visible) => { - if (visible) { - console.log('=== 上传对话框已显示,确保文件列表清空 ==='); - // 立即清空文件列表 - fileListTemp.value = []; - console.log('✅ 已清空文件列表'); - - // 强制重置上传组件 - if (uploadRef.value) { - try { - uploadRef.value.reset(); - // 强制清空组件的内部文件列表 - if (uploadRef.value.fileList) { - uploadRef.value.fileList = []; - } - if (uploadRef.value.fileListTemp) { - uploadRef.value.fileListTemp = []; - } - console.log('✅ 已重置上传组件'); - } catch (error) { - console.log('❌ 重置上传组件时出错:', error); - } - } - - // 延迟再次清空,确保处理完所有可能的触发 - setTimeout(() => { - fileListTemp.value = []; - console.log('✅ 延迟清空文件列表'); - }, 100); - } -}); + // 初始化加载 onMounted(() => { + loadSavedWidth(); // 加载保存的侧边栏宽度 initData(); }); @@ -2603,22 +1950,7 @@ onMounted(() => { /* 删除旧的span样式,因为现在使用.folder-name */ -/* 顶部导航样式 */ -.file-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 24px; - background: var(--color-bg-1); - border-bottom: 1px solid var(--color-border); - height: 64px; -} -.breadcrumbs { - display: flex; - align-items: center; - gap: 8px; -} /* 文件内容区域样式 */ .file-content { @@ -2647,360 +1979,13 @@ onMounted(() => { padding-bottom: 80px; /* 为分页器留出空间 */ } -/* 表格容器 */ -.file-grid-container { - flex: 1; - width: 100%; - margin-top: 16px; - border-radius: 8px; - border: 1px solid var(--color-border); - overflow-y: auto; - background-color: var(--color-bg-1); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - margin-bottom: 0; - min-height: 300px; - max-height: calc(100vh - 380px); /* 调整高度为分页器留出空间 */ -} -/* 表头行样式 */ -.table-header-row { - padding: 0 16px; - height: 48px; - line-height: 48px; - background-color: var(--color-fill-1); - border-bottom: 1px solid var(--color-border); - font-size: 13px; - color: var(--color-text-3); - font-weight: 500; -} -/* 数据行样式 */ -.table-data-row { - display: flex; - padding: 0 16px; - height: 64px; - align-items: center; - border-bottom: 1px solid var(--color-border); - transition: all 0.25s ease; - cursor: pointer; - background-color: var(--color-bg-1); - - &:last-child { - border-bottom: none; - } - - &:hover { - background-color: rgba(22, 93, 255, 0.1); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); - } -} -/* 通用列样式 */ -.table-column { - height: 100%; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - white-space: nowrap; - padding: 0 8px; -} -.cell-content { - display: inline-block; - width: 100%; - text-align: center; - justify-content: center; - align-items: center; -} -.name-column { - padding: 0 14px; - justify-content: flex-start !important; -} -.file-info { - display: flex; - align-items: center; - width: 100%; -} -.file-icon { - font-size: 20px; - margin-right: 12px; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -.folder-icon { - color: var(--color-primary); - background-color: var(--color-primary-light-1); -} - -.file-name { - font-size: 14px; - font-weight: 500; - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - transition: color 0.2s ease; -} - -.table-data-row:hover .file-name { - color: var(--color-primary); -} - -.type-column, .size-column, .time-column { - color: var(--color-text-3); - font-size: 14px; - justify-content: center; - align-items: center; - padding: 4px; -} - -.action-column { - justify-content: center; -} - -.file-main { - display: flex; - align-items: center; - width: 100%; -} - -.file-icon-large { - font-size: 24px; - margin-right: 12px; - flex-shrink: 0; -} - -.file-name-wrap { - flex: 1; - overflow: hidden; - min-width: 0; - display: flex; - justify-content: center; -} - -.file-name { - margin: 0; - font-size: 14px; - color: var(--color-text-1); - transition: color 0.2s; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - .table-data-row:hover & { - color: var(--color-primary); - } -} - -.file-name-small { - font-size: 14px; - color: var(--color-text-4); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - width: 100%; -} - -/* 操作按钮区域 */ -.file-actions { - display: flex; - gap: 4px; - justify-content: center; -} - -.action-btn { - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - background: transparent; - border: none; - cursor: pointer; - transition: all 0.2s ease; - color: var(--color-text-3); - - &:hover { - background: var(--color-fill-3); - color: var(--color-primary); - } -} - -.delete-btn { - &:hover { - color: var(--color-danger); - background-color: rgba(255, 77, 77, 0.05); - } -} - -/* 响应式调整 */ -@media (max-width: 1200px) { - .name-column { - flex: 0 0 35% !important; - max-width: 35% !important; - } - .time-column { - flex: 0 0 20% !important; - max-width: 20% !important; - } -} - -@media (max-width: 992px) { - .name-column { - flex: 0 0 45% !important; - max-width: 45% !important; - } - .type-column { - flex: 0 0 20% !important; - max-width: 20% !important; - } - .time-column { - display: none; - } - .action-column { - flex: 0 0 35% !important; - max-width: 35% !important; - } - .file-actions { - justify-content: flex-end; - } -} - -@media (max-width: 768px) { - .size-column, .time-column { - display: none; - } - .type-column { - flex: 0 0 25% !important; - max-width: 25% !important; - } - .name-column { - flex: 0 0 45% !important; - max-width: 45% !important; - } - .action-column { - flex: 0 0 30% !important; - max-width: 30% !important; - } - .file-content { - padding: 12px; - } - .file-grid-container { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} - -@media (max-width: 576px) { - .type-column { - display: none; - } - .name-column { - flex: 0 0 60% !important; - max-width: 60% !important; - } - .action-column { - flex: 0 0 40% !important; - max-width: 40% !important; - } - .file-header { - padding: 0 12px; - flex-wrap: wrap; - } - .breadcrumbs { - margin-bottom: 8px; - width: 100%; - } - .file-card { - min-height: auto; - } -} - -/* 浏览器缩放调整 */ -@media (max-height: 800px) { - .folder-content { - max-height: calc(100vh - 180px); - } - - .file-content { - max-height: calc(100vh - 100px); - } - - .file-grid-container { - max-height: calc(100vh - 280px); - } -} - -@media (max-height: 600px) { - .folder-content { - max-height: calc(100vh - 160px); - } - - .file-content { - max-height: calc(100vh - 80px); - } - - .file-grid-container { - max-height: calc(100vh - 260px); - } -} - -/* 空状态样式 */ -.initial-state, .empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 64px 0; - color: var(--color-text-3); - background-color: var(--color-fill-1); - border-radius: 8px; - text-align: center; -} - -.initial-icon { - font-size: 48px; - margin-bottom: 16px; - color: var(--color-text-4); -} - -:deep(.empty-state .arco-btn) { - margin-top: 16px; - padding: 8px 16px; - background-color: var(--color-primary); - color: white; - border-radius: 4px; - border: none; - cursor: pointer; - transition: all 0.2s ease; - font-weight: 500; -} - -:deep(.empty-state .arco-btn:hover) { - background-color: var(--color-primary-dark-1); - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -:deep(.empty-state .arco-btn:active) { - transform: translateY(0); -} /* 上传区域样式 */ .upload-area { @@ -3030,141 +2015,9 @@ onMounted(() => { color: var(--color-text-3); } -.upload-hint { - margin-top: 8px; - font-size: 12px; - color: var(--color-text-4); -} -/* 上传相关样式 */ -.upload-file-list { - margin-top: 16px; - border-radius: 4px; - border: 1px solid var(--color-border); - overflow: hidden; -} -.upload-file-item { - display: flex; - align-items: center; - padding: 12px; - border-bottom: 1px solid var(--color-border); - - &:last-child { - border-bottom: none; - } - - &:hover { - background-color: var(--color-fill-1); - } -} -.file-info { - display: flex; - align-items: center; - flex: 1; - min-width: 0; -} - -.file-icon { - font-size: 20px; - margin-right: 12px; - flex-shrink: 0; -} - -.file-name { - flex: 1; - min-width: 0; -} - -.name-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 14px; -} - -.file-meta { - font-size: 12px; - color: var(--color-text-4); - margin-top: 4px; -} - -.file-error { - color: var(--color-danger); - margin-left: 8px; -} - -.file-progress { - flex: 1; - margin: 0 16px; -} - -.file-actions { - flex-shrink: 0; -} - -/* 分页样式 */ -.pagination-container { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: var(--color-bg-1); - padding: 16px 24px; - border-top: 1px solid var(--color-border); - display: flex; - justify-content: flex-end; - align-items: center; - z-index: 10; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06); - margin-top: 0; - - .arco-pagination { - margin: 0; - - .arco-pagination-item { - border-radius: 6px; - margin: 0 4px; - transition: all 0.2s ease; - - &:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } - - &.arco-pagination-item-active { - background: var(--color-primary); - border-color: var(--color-primary); - color: white; - } - } - - .arco-pagination-prev, - .arco-pagination-next { - border-radius: 6px; - transition: all 0.2s ease; - - &:hover { - border-color: var(--color-primary); - color: var(--color-primary); - } - } - - .arco-pagination-size-changer { - margin-left: 16px; - } - - .arco-pagination-jumper { - margin-left: 16px; - } - - .arco-pagination-total { - color: var(--color-text-2); - font-size: 14px; - } - } -} @@ -3454,228 +2307,14 @@ onMounted(() => { } } -/* 上传文件相关样式 */ -.upload-container { - display: flex; - flex-direction: column; - gap: 12px; -} -.upload-btn { - align-self: flex-start; - border-radius: 8px; - font-weight: 500; - transition: all 0.3s ease; - - &:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); - } -} - -.upload-hint { - color: var(--color-text-3); - font-size: 12px; - line-height: 1.4; - padding: 8px 12px; - background: var(--color-fill-2); - border-radius: 6px; - border-left: 3px solid var(--color-primary-light-3); -} - -.upload-file-list { - margin-top: 16px; - border: 1px solid var(--color-border); - border-radius: 8px; - background: var(--color-bg-1); - max-height: 300px; - overflow-y: auto; -} - -.upload-file-item { - display: flex; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid var(--color-border); - transition: all 0.2s ease; - - &:last-child { - border-bottom: none; - } - - &:hover { - background: var(--color-fill-1); - } - - &.file-error { - background: rgba(255, 77, 79, 0.05); - border-left: 3px solid #ff4d4f; - } -} - -.file-info { - display: flex; - align-items: center; - flex: 1; - gap: 12px; - min-width: 0; -} - -.file-icon { - font-size: 20px; - flex-shrink: 0; -} - -.file-details { - flex: 1; - min-width: 0; -} - -.file-name { - font-weight: 500; - color: var(--color-text-1); - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-meta { - font-size: 12px; - color: var(--color-text-3); - display: flex; - align-items: center; - gap: 8px; -} - -.error-text { - color: #ff4d4f; - font-weight: 500; -} - -.file-progress { - margin: 0 16px; - min-width: 120px; -} - -.file-actions { - display: flex; - gap: 4px; -} - -.remove-btn { - color: var(--color-text-3); - - &:hover { - color: #ff4d4f; - background: rgba(255, 77, 79, 0.1); - } -} - -.cancel-btn { - color: var(--color-text-3); - - &:hover { - color: #faad14; - background: rgba(250, 173, 20, 0.1); - } -} @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } -/* 文件头部容器样式 */ -.file-header-container { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding: 16px 0; -} -.file-title { - display: flex; - align-items: center; -} - -.file-list-title { - font-size: 16px; - font-weight: 600; - color: var(--color-text-1); - margin: 0; -} - -/* 文件搜索样式 */ -.file-search-container { - display: flex; - align-items: center; - gap: 12px; -} - -.file-search-input { - max-width: 300px; - transition: all 0.3s ease; - - &:focus-within { - box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1); - } -} - -/* 可排序表头样式 */ -.sortable-header { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - cursor: pointer; - padding: 4px 8px; - border-radius: 4px; - transition: all 0.2s ease; - user-select: none; -} - -.sortable-header:hover { - background: var(--color-fill-2); - color: var(--color-primary); -} - -.sort-indicator { - display: flex; - flex-direction: column; - gap: 1px; - margin-left: 4px; -} - -.sort-arrow { - width: 0; - height: 0; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - transition: all 0.2s ease; -} - -.sort-arrow.up { - border-bottom: 3px solid var(--color-text-4); -} - -.sort-arrow.down { - border-top: 3px solid var(--color-text-4); -} - -.sort-arrow.active { - border-bottom-color: var(--color-primary); - border-top-color: var(--color-primary); -} - -.sortable-header:hover .sort-arrow.up { - border-bottom-color: var(--color-primary); -} - -.sortable-header:hover .sort-arrow.down { - border-top-color: var(--color-primary); -} @@ -3784,4 +2423,52 @@ onMounted(() => { } } } + +/* 拖拽分隔线样式 */ +.sidebar-resizer { + position: absolute; + top: 0; + right: 0; + width: 6px; + height: 100%; + cursor: col-resize; + background: transparent; + transition: background-color 0.2s ease; + z-index: 10; + + &:hover { + background: rgba(var(--color-primary-6), 0.1); + } + + &:active { + background: rgba(var(--color-primary-6), 0.2); + } +} + +.resizer-handle { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 40px; + background: var(--color-primary); + border-radius: 1px; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.sidebar-resizer:hover .resizer-handle { + opacity: 1; +} + +/* 拖拽时的全局样式 */ +body.resizing { + cursor: col-resize !important; + user-select: none !important; +} + +body.resizing * { + cursor: col-resize !important; +} diff --git a/src/views/bussiness-data/components/FileHeader.vue b/src/views/bussiness-data/components/FileHeader.vue new file mode 100644 index 0000000..6d56a3f --- /dev/null +++ b/src/views/bussiness-data/components/FileHeader.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/views/bussiness-data/components/FileList.vue b/src/views/bussiness-data/components/FileList.vue new file mode 100644 index 0000000..4901a88 --- /dev/null +++ b/src/views/bussiness-data/components/FileList.vue @@ -0,0 +1,669 @@ + + + + + diff --git a/src/views/bussiness-data/components/FilePagination.vue b/src/views/bussiness-data/components/FilePagination.vue new file mode 100644 index 0000000..915a632 --- /dev/null +++ b/src/views/bussiness-data/components/FilePagination.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/src/views/bussiness-data/components/FileUpload.vue b/src/views/bussiness-data/components/FileUpload.vue new file mode 100644 index 0000000..4802274 --- /dev/null +++ b/src/views/bussiness-data/components/FileUpload.vue @@ -0,0 +1,557 @@ + + + + + diff --git a/src/views/operation-platform/data-processing/data-storage/index.vue b/src/views/operation-platform/data-processing/data-storage/index.vue index 23de01b..a15755d 100644 --- a/src/views/operation-platform/data-processing/data-storage/index.vue +++ b/src/views/operation-platform/data-processing/data-storage/index.vue @@ -73,18 +73,8 @@ - -
- - - -
-
- - - @@ -107,8 +97,6 @@ import { } from '@/apis/industrial-image' import DeformationTap from './components/DeformationTap.vue' -// 预览弹窗引用(待重新设计) -// const previewModal = ref() // 活动选项卡 const activeTab = ref('image') diff --git a/src/views/project/detail/index.vue b/src/views/project/detail/index.vue index ed7aa2d..486f420 100644 --- a/src/views/project/detail/index.vue +++ b/src/views/project/detail/index.vue @@ -7,9 +7,9 @@

{{ projectTitle }}

- {{ - projectData.status - }} + + {{ projectData.statusLabel ?? projectData.status }} +
@@ -198,8 +198,8 @@ import { ref, reactive, computed, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Message, Modal } from '@arco-design/web-vue' -import { getProject, deleteProject } from '@/apis/project' -import { addTask, addTaskGroup, listTask, updateTaskProgress } from '@/apis/project/task' +import { getProjectDetail, deleteProject } from '@/apis/project' +import { addTask, addTaskGroup, updateTaskProgress } from '@/apis/project/task' import dayjs from 'dayjs' defineOptions({ name: 'ProjectDetail' }) @@ -257,10 +257,15 @@ const projectTitle = computed(() => { const projectInfos = computed(() => [ { label: '项目编号', value: projectData.value?.projectCode }, - { label: '项目负责人', value: projectData.value?.projectManager }, - { label: '参与人', value: projectData.value?.projectStaff?.join(', ') }, - { label: '项目周期', value: projectData.value?.projectPeriod ? `${projectData.value.projectPeriod[0]} 至 ${projectData.value.projectPeriod[1]}` : '' }, - { label: '客户', value: projectData.value?.commissionUnit }, + { label: '项目负责人', value: projectData.value?.projectManagerName || projectData.value?.projectManager }, + { label: '项目来源', value: projectData.value?.projectOrigin }, + { label: '风场名称', value: projectData.value?.farmName }, + { label: '风场地址', value: projectData.value?.farmAddress }, + { label: '开始时间', value: projectData.value?.startDate }, + { label: '结束时间', value: projectData.value?.endDate }, + { label: '项目规模', value: projectData.value?.scale }, + { label: '状态', value: (statusMap as any)[Number(projectData.value?.status)]?.label || projectData.value?.statusLabel }, + { label: '客户', value: projectData.value?.client }, { label: '备注', value: projectData.value?.projectIntro || '无' } ]) @@ -284,6 +289,15 @@ const taskDetailInfos = computed(() => { { label: '状态', value: currentTask.value.status }, { label: '描述', value: currentTask.value.description || '无' } ] + +const statusMap: Record = { + 0: { label: '待施工', color: 'gray' }, + 1: { label: '施工中', color: 'blue' }, + 2: { label: '已完工', color: 'green' }, + 3: { label: '已审核', color: 'orange' }, + 4: { label: '已验收', color: 'arcoblue' }, +} + }) const getStatusColor = (status: string) => { @@ -322,8 +336,14 @@ const formatDate = (date: string) => { const fetchProjectData = async () => { loading.value = true try { - const res = await getProject(projectId.value) - projectData.value = res.data + const res = await getProjectDetail(projectId.value) + const detail = (res as any).data || res + // 如果status是数字,补充statusLabel用于页面展示 + if (typeof detail.status === 'number' && !detail.statusLabel) { + const mapper = (statusMap as any)[detail.status] + if (mapper) detail.statusLabel = mapper.label + } + projectData.value = detail } catch (error) { console.error(error) Message.error('获取项目详情失败') @@ -332,34 +352,39 @@ const fetchProjectData = async () => { } } -const fetchTaskData = async () => { - try { - const res = await listTask({ - projectId: projectId.value, - page: 1, - size: 100 - }) +// 若任务未返回状态,依据计划时间简单推断状态 +const inferTaskStatus = (task: any): string => { + if (task.status) return task.status + const now = dayjs() + const start = task.planStartDate ? dayjs(task.planStartDate) : null + const end = task.planEndDate ? dayjs(task.planEndDate) : null + if (end && end.isBefore(now)) return '已完成' + if (start && start.isAfter(now)) return '计划中' + if (start && (!end || end.isAfter(now))) return '正在做' + return '其他' +} - // 重置任务列表 - taskColumns.value.forEach(column => { - column.tasks = [] - }) - const tasks = res.data?.list || [] +const fetchTaskData = () => { + // 使用详情接口返回的任务列表 + const detail = projectData.value || {} + const tasks = (detail.tasks || []) as any[] - // 分配任务到对应的列 - tasks.forEach((task: any) => { - const column = taskColumns.value.find(col => col.status === task.status) - if (column) { - column.tasks.push(task) - } else { - taskColumns.value.find(col => col.status === '其他')?.tasks.push(task) - } - }) - } catch (error) { - console.error(error) - Message.error('获取任务数据失败') - } + // 重置任务列表 + taskColumns.value.forEach(column => { + column.tasks = [] + }) + + // 分配任务到对应的列(按状态或推断状态) + tasks.forEach((task: any) => { + const st = inferTaskStatus(task) + const column = taskColumns.value.find(col => col.status === st) + if (column) { + column.tasks.push(task) + } else { + taskColumns.value.find(col => col.status === '其他')?.tasks.push(task) + } + }) } const goBack = () => { @@ -491,8 +516,7 @@ const submitProgressUpdate = async () => { } onMounted(() => { - fetchProjectData() - fetchTaskData() + fetchProjectData().then(() => fetchTaskData()) }) diff --git a/src/views/project/index.vue b/src/views/project/index.vue index b523e17..3c7d76f 100644 --- a/src/views/project/index.vue +++ b/src/views/project/index.vue @@ -2,19 +2,21 @@ 项目管理页面 已完成接口对接: 1. 项目列表查询 (listProject) - 支持分页和条件查询 - 2. 项目新增 (addProject) + 2. 项目新增 (addProject) 3. 项目修改 (updateProject) 4. 项目删除 (deleteProject) 5. 项目导出 (exportProject) 6. 项目导入 (importProject) - + 所有API调用都已添加错误处理和类型安全检查 --> @@ -333,6 +347,8 @@ import ProcurementSearch from './components/ProcurementSearch.vue' import ProcurementApplicationModal from './components/ProcurementApplicationModal.vue' import ReceiptDetailModal from './components/ReceiptDetailModal.vue' import PaymentDetailModal from './components/PaymentDetailModal.vue' +import ReceiptModal from './components/ReceiptModal.vue' +import PaymentModal from './components/PaymentModal.vue' import { equipmentProcurementApi } from '@/apis/equipment/procurement' import { equipmentApprovalApi } from '@/apis/equipment/approval' import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type' @@ -377,6 +393,10 @@ const currentReceiptData = ref(null) const paymentDetailModalVisible = ref(false) const currentPaymentData = ref(null) +// 收货弹窗控制 +const receiptModalVisible = ref(false) +const paymentModalVisible = ref(false) + // 表格选择 const selectedRowKeys = ref([]) const rowSelection = reactive({ @@ -988,17 +1008,8 @@ const canReceiveGoods = (record: EquipmentResp) => { // 收货操作 const handleReceiveGoods = async (record: EquipmentResp) => { - try { - // 暂时模拟API调用 - // await equipmentProcurementApi.receiveGoods(record.equipmentId) - message.success('收货成功') - // 模拟状态更新 - (record as any).receiptStatus = 'RECEIVED' - loadData(currentSearchParams.value) - } catch (error: any) { - console.error('收货失败:', error) - message.error(error?.message || '收货失败') - } + currentReceiptData.value = { ...record } + receiptModalVisible.value = true } // 查看收货详情 @@ -1007,6 +1018,12 @@ const handleViewReceipt = (record: EquipmentResp) => { receiptDetailModalVisible.value = true } +// 收货成功回调 +const handleReceiptSuccess = () => { + receiptModalVisible.value = false + loadData(currentSearchParams.value) +} + // 检查是否可以付款 const canMakePayment = (record: EquipmentResp) => { const paymentStatus = (record as any).paymentStatus @@ -1015,17 +1032,8 @@ const canMakePayment = (record: EquipmentResp) => { // 付款操作 const handleMakePayment = async (record: EquipmentResp) => { - try { - // 暂时模拟API调用 - // await equipmentProcurementApi.makePayment(record.equipmentId) - message.success('付款成功') - // 模拟状态更新 - (record as any).paymentStatus = 'PAID' - loadData(currentSearchParams.value) - } catch (error: any) { - console.error('付款失败:', error) - message.error(error?.message || '付款失败') - } + currentPaymentData.value = { ...record } + paymentModalVisible.value = true } // 查看支付详情 @@ -1034,6 +1042,12 @@ const handleViewPayment = (record: EquipmentResp) => { paymentDetailModalVisible.value = true } +// 支付成功回调 +const handlePaymentSuccess = () => { + paymentModalVisible.value = false + loadData(currentSearchParams.value) +} + // 获取审批状态颜色 const getApprovalStatusColor = (status: string) => { const colorMap: Record = { diff --git a/src/views/task/task-approval/TaskApproval.vue b/src/views/task/task-approval/TaskApproval.vue index c41d446..8defe5c 100644 --- a/src/views/task/task-approval/TaskApproval.vue +++ b/src/views/task/task-approval/TaskApproval.vue @@ -200,47 +200,51 @@ const handleReject = (taskId: string) => { /* 待审批状态样式 */ .status-item.pending { - border-top-color: #f59e0b; - background-color: rgba(245, 158, 11, 0.1); /* 浅橙色背景 */ + background-color: #f59e0b; + background-image: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); } .status-item.pending .count { - color: #f59e0b; + color: #fff; } /* 已通过状态样式 */ .status-item.approved { - border-top-color: #10b981; - background-color: rgba(16, 185, 129, 0.1); /* 浅绿色背景 */ + background-color: #10b981; + background-image: linear-gradient(135deg, #10b981 0%, #059669 100%); } .status-item.approved .count { - color: #10b981; + color: #fff; } /* 已拒绝状态样式 */ .status-item.rejected { - border-top-color: #ef4444; - background-color: rgba(239, 68, 68, 0.1); /* 浅红色背景 */ + background-color: #ef4444; + background-image: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); } .status-item.rejected .count { - color: #ef4444; + color: #fff; } /* 状态文字样式 */ .status-text { - font-size: 14px; - color: #333; + font-size: 16px; + font-weight: 500; + color: #fff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } /* 数字样式 */ .count { - font-size: 20px; - font-weight: bold; + font-size: 28px; + font-weight: 700; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + letter-spacing: 0.5px; } /* 激活态样式(可选,点击后高亮) */ .status-item.active { transform: scale(1.02); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .content { diff --git a/src/views/task/task-progress/TaskProgress.vue b/src/views/task/task-progress/TaskProgress.vue index 3ff1392..861daff 100644 --- a/src/views/task/task-progress/TaskProgress.vue +++ b/src/views/task/task-progress/TaskProgress.vue @@ -1,38 +1,711 @@ \ No newline at end of file diff --git a/src/views/task/task-publish/TaskPublish.vue b/src/views/task/task-publish/TaskPublish.vue index caf5def..f6a35fe 100644 --- a/src/views/task/task-publish/TaskPublish.vue +++ b/src/views/task/task-publish/TaskPublish.vue @@ -173,6 +173,7 @@ diff --git a/src/views/task/task-publish/components/AssigneeSelector.vue b/src/views/task/task-publish/components/AssigneeSelector.vue index b3dc176..808c512 100644 --- a/src/views/task/task-publish/components/AssigneeSelector.vue +++ b/src/views/task/task-publish/components/AssigneeSelector.vue @@ -56,7 +56,7 @@ const filteredUsers = ref([]); const filterUsersByDepartment = () => { if (!selectedDepartment.value) { filteredUsers.value = []; - assignees.value.leader = ''; + assignees.value.leader = -1; return; } @@ -66,7 +66,7 @@ const filterUsersByDepartment = () => { // 如果当前选择的负责人不在筛选后的列表中,则清空选择 if (assignees.value.leader && !filteredUsers.value.some(u => u.id === assignees.value.leader)) { - assignees.value.leader = ''; + assignees.value.leader = 0; } }; @@ -80,7 +80,7 @@ const departments = ref([ // 分配数据 const assignees = ref({ - leader: '' + leader: 0 }); // 暴露分配数据给父组件 diff --git a/src/views/tower-monitoring/clearance.vue b/src/views/tower-monitoring/clearance.vue new file mode 100644 index 0000000..b3ff709 --- /dev/null +++ b/src/views/tower-monitoring/clearance.vue @@ -0,0 +1,498 @@ + + + + + + diff --git a/src/views/tower-monitoring/deformation.vue b/src/views/tower-monitoring/deformation.vue new file mode 100644 index 0000000..1152c74 --- /dev/null +++ b/src/views/tower-monitoring/deformation.vue @@ -0,0 +1,665 @@ + + + + + + \ No newline at end of file diff --git a/src/views/tower-monitoring/vibration.vue b/src/views/tower-monitoring/vibration.vue new file mode 100644 index 0000000..07d9e49 --- /dev/null +++ b/src/views/tower-monitoring/vibration.vue @@ -0,0 +1,497 @@ + + + + + + diff --git a/src/views/tower-monitoring/whistle.vue b/src/views/tower-monitoring/whistle.vue new file mode 100644 index 0000000..07d9e49 --- /dev/null +++ b/src/views/tower-monitoring/whistle.vue @@ -0,0 +1,497 @@ + + + + + +