diff --git a/frontend/package.json b/frontend/package.json index c0735c6..de02407 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "axios": "^1.7.3", "crypto-js": "^4.2.0", "dayjs": "^1.11.9", + "docx-preview": "^0.3.7", "driver.js": "^1.3.0", "echarts": "^5.4.3", "echarts-gl": "^2.0.9", diff --git a/frontend/src/api/aireport/clientUnit/index.ts b/frontend/src/api/aireport/clientUnit/index.ts new file mode 100644 index 0000000..0fbc623 --- /dev/null +++ b/frontend/src/api/aireport/clientUnit/index.ts @@ -0,0 +1,48 @@ +import http from '@/api' +import type { ClientUnit } from './interface' + +const buildClientUnitImportFormData = (file: File) => { + const formData = new FormData() + + formData.append('file', file) + + return formData +} + +export const listClientUnitsApi = (params: ClientUnit.ClientUnitListParams) => { + return http.post('/clientUnit/list', params) +} + +export const createClientUnitApi = (params: ClientUnit.ClientUnitSaveRequest) => { + return http.post('/clientUnit/add', params) +} + +export const updateClientUnitApi = (params: ClientUnit.ClientUnitSaveRequest) => { + return http.post('/clientUnit/update', params) +} + +export const getClientUnitDetailApi = (id: string) => { + return http.get('/clientUnit/getById', { id }) +} + +export const deleteClientUnitsApi = (ids: string[]) => { + return http.post('/clientUnit/delete', ids) +} + +export const exportClientUnitsApi = (params: ClientUnit.ClientUnitListParams) => { + return http.post('/clientUnit/export', params, { responseType: 'blob' }) +} + +export const downloadClientUnitImportTemplateApi = () => { + return http.get('/clientUnit/importTemplate', undefined, { responseType: 'blob' }) +} + +export const importClientUnitsApi = (file: File) => { + return http.post( + '/clientUnit/import', + buildClientUnitImportFormData(file), + { + headers: { 'Content-Type': 'multipart/form-data' } + } + ) +} diff --git a/frontend/src/api/aireport/clientUnit/interface/index.ts b/frontend/src/api/aireport/clientUnit/interface/index.ts new file mode 100644 index 0000000..b599c94 --- /dev/null +++ b/frontend/src/api/aireport/clientUnit/interface/index.ts @@ -0,0 +1,36 @@ +export namespace ClientUnit { + export interface ClientUnitRecord { + id?: string + name?: string | null + contactName?: string | null + phonenumber?: string | null + address?: string | null + createBy?: string | null + createTime?: string | null + updateBy?: string | null + updateTime?: string | null + } + + export interface ClientUnitListParams { + name?: string + searchBeginTime?: string + searchEndTime?: string + timeRange?: string[] + pageNum?: number + pageSize?: number + } + + export interface ClientUnitSaveRequest { + id?: string + name: string + contactName?: string | null + phonenumber?: string | null + address?: string | null + } + + export interface ClientUnitImportResult { + successCount?: number + failCount?: number + failReasons?: string[] + } +} diff --git a/frontend/src/api/aireport/reportApproval/index.ts b/frontend/src/api/aireport/reportApproval/index.ts new file mode 100644 index 0000000..a49fe12 --- /dev/null +++ b/frontend/src/api/aireport/reportApproval/index.ts @@ -0,0 +1,16 @@ +import http from '@/api' +import type { ReportApproval } from './interface' + +const REPORT_APPROVAL_BASE_URL = '/api/report-approval' + +export const listPendingReportApprovalsApi = (params: ReportApproval.PendingListParams) => + http.get(`${REPORT_APPROVAL_BASE_URL}/pending-page`, params) + +export const passReportApprovalApi = (params: ReportApproval.ApprovalActionRequest) => + http.post(`${REPORT_APPROVAL_BASE_URL}/pass`, params) + +export const rejectReportApprovalApi = (params: ReportApproval.ApprovalActionRequest) => + http.post(`${REPORT_APPROVAL_BASE_URL}/reject`, params) + +export const listReportApprovalLogsApi = (params: ReportApproval.LogListParams) => + http.get(`${REPORT_APPROVAL_BASE_URL}/log-page`, params) diff --git a/frontend/src/api/aireport/reportApproval/interface/index.ts b/frontend/src/api/aireport/reportApproval/interface/index.ts new file mode 100644 index 0000000..0673986 --- /dev/null +++ b/frontend/src/api/aireport/reportApproval/interface/index.ts @@ -0,0 +1,46 @@ +export namespace ReportApproval { + export type ReportType = 'REPORT_MODEL' | 'TEST_REPORT' | (string & {}) + export type ReportState = '01' | '02' | '03' | '04' | '05' | (string & {}) + export type ApprovalResult = '03' | '04' | (string & {}) + + export interface PendingRecord { + reportType?: ReportType + reportId?: string + reportName?: string | null + state?: ReportState + submitterId?: string | null + submitterName?: string | null + submitTime?: string | null + } + + export interface PendingListParams { + current?: number + size?: number + reportType?: ReportType | '' + } + + export interface ApprovalActionRequest { + reportType: ReportType + reportId: string + approvalReason: string + } + + export interface LogRecord { + id?: string + reportType?: ReportType + reportId?: string + reportName?: string | null + approvalResult?: ApprovalResult + approvalReason?: string | null + beforeState?: ReportState + afterState?: ReportState + approverName?: string | null + approvalTime?: string | null + } + + export interface LogListParams { + current?: number + size?: number + reportType?: ReportType | '' + } +} diff --git a/frontend/src/api/aireport/reportModel/index.ts b/frontend/src/api/aireport/reportModel/index.ts new file mode 100644 index 0000000..9d91475 --- /dev/null +++ b/frontend/src/api/aireport/reportModel/index.ts @@ -0,0 +1,49 @@ +import http from '@/api' +import type { AxiosResponse } from 'axios' +import type { ReportModel } from './interface' + +const REPORT_MODEL_BASE_URL = '/api/report-model' + +const buildReportModelFormData = (request: ReportModel.ReportModelSaveRequest, templateFile?: File | null) => { + const formData = new FormData() + + // 关键业务节点:后端按 @RequestPart("request") 读取 JSON 分段,字段名不能改成普通表单字段。 + formData.append('request', new Blob([JSON.stringify(request)], { type: 'application/json' })) + + if (templateFile) { + formData.append('templateFile', templateFile) + } + + return formData +} + +export const listReportModelsApi = (params: ReportModel.ReportModelListParams) => { + return http.post('/api/report-model/list', params) +} + +export const getReportModelDetailApi = (id: string) => { + return http.get(`${REPORT_MODEL_BASE_URL}/${id}`) +} + +export const createReportModelApi = (request: ReportModel.ReportModelSaveRequest, templateFile: File) => { + return http.post(`${REPORT_MODEL_BASE_URL}/add`, buildReportModelFormData(request, templateFile), { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export const updateReportModelApi = (request: ReportModel.ReportModelSaveRequest, templateFile?: File | null) => { + return http.post(`${REPORT_MODEL_BASE_URL}/update`, buildReportModelFormData(request, templateFile), { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export const deleteReportModelsApi = (ids: string[]) => { + return http.post(`${REPORT_MODEL_BASE_URL}/delete`, ids) +} + +export const downloadReportModelApi = (id: string): Promise> => + http.get(`${REPORT_MODEL_BASE_URL}/${id}/download`, undefined, { responseType: 'blob' }) as unknown as Promise> + +export const submitReportModelAuditApi = (params: ReportModel.ReportModelAuditRequest) => { + return http.post(`${REPORT_MODEL_BASE_URL}/submit-audit`, params) +} diff --git a/frontend/src/api/aireport/reportModel/interface/index.ts b/frontend/src/api/aireport/reportModel/interface/index.ts new file mode 100644 index 0000000..0c88107 --- /dev/null +++ b/frontend/src/api/aireport/reportModel/interface/index.ts @@ -0,0 +1,43 @@ +export namespace ReportModel { + export type ReportModelState = '01' | '02' | '03' | '04' | (string & {}) + + export interface ReportModelRecord { + id?: string + name?: string + state?: ReportModelState + checkId?: string | null + checkerName?: string | null + checkTime?: string | null + checkResult?: string | null + path?: string | null + fileName?: string | null + file_name?: string | null + status?: number + createBy?: string | null + createTime?: string | null + updateBy?: string | null + editorName?: string | null + updateTime?: string | null + } + + export interface ReportModelListParams { + name?: string + state?: ReportModelState | '' + searchBeginTime?: string + searchEndTime?: string + timeRange?: string[] + pageNum?: number + pageSize?: number + } + + export interface ReportModelSaveRequest { + id?: string + name: string + fileName?: string + } + + export interface ReportModelAuditRequest { + id: string + checkResult?: string + } +} diff --git a/frontend/src/api/aireport/testDevice/index.ts b/frontend/src/api/aireport/testDevice/index.ts new file mode 100644 index 0000000..af06627 --- /dev/null +++ b/frontend/src/api/aireport/testDevice/index.ts @@ -0,0 +1,77 @@ +import http from '@/api' +import type { TestDevice } from './interface' + +const TEST_DEVICE_BASE_URL = '/api/test-device' + +const buildTestDeviceFormData = (request: TestDevice.TestDeviceSaveRequest, reportFile?: File | null) => { + const formData = new FormData() + + // 关键业务节点:检测设备新增/编辑接口按 request JSON 分段读取业务字段,报告文件单独传 reportFile。 + formData.append('request', new Blob([JSON.stringify(request)], { type: 'application/json' })) + + if (reportFile) { + formData.append('reportFile', reportFile) + } + + return formData +} + +const buildTestDeviceImportFormData = (file: File, reportZip: File) => { + const formData = new FormData() + + formData.append('file', file) + formData.append('reportZip', reportZip) + + return formData +} + +export const listTestDevicesApi = (params: TestDevice.TestDeviceListParams) => { + return http.post(`${TEST_DEVICE_BASE_URL}/list`, params) +} + +export const createTestDeviceApi = (request: TestDevice.TestDeviceSaveRequest, reportFile: File) => { + return http.post(`${TEST_DEVICE_BASE_URL}/add`, buildTestDeviceFormData(request, reportFile), { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export const updateTestDeviceApi = (request: TestDevice.TestDeviceSaveRequest, reportFile?: File | null) => { + return http.post(`${TEST_DEVICE_BASE_URL}/update`, buildTestDeviceFormData(request, reportFile), { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export const deleteTestDevicesApi = (ids: string[]) => { + return http.post(`${TEST_DEVICE_BASE_URL}/delete`, ids) +} + +export const getTestDeviceDetailApi = (id: string) => { + return http.get(`${TEST_DEVICE_BASE_URL}/${id}`) +} + +export const downloadTestDeviceImportTemplateApi = () => { + return http.get(`${TEST_DEVICE_BASE_URL}/template`, undefined, { responseType: 'blob' }) +} + +export const importTestDevicesApi = (file: File, reportZip: File) => { + return http.post( + `${TEST_DEVICE_BASE_URL}/import`, + buildTestDeviceImportFormData(file, reportZip), + { + headers: { 'Content-Type': 'multipart/form-data' } + } + ) +} + +export const exportTestDevicesApi = (params: TestDevice.TestDeviceListParams) => { + return http.get(`${TEST_DEVICE_BASE_URL}/export`, params, { responseType: 'blob' }) +} + +export const previewTestDeviceReportApi = (id: string) => { + // 关键业务节点:test-device 当前仅提供 /{id}/report,预览与下载统一复用同一报告流,避免前端误调不存在的 preview 接口。 + return http.get(`${TEST_DEVICE_BASE_URL}/${id}/report`, undefined, { responseType: 'blob' }) +} + +export const downloadTestDeviceReportApi = (id: string) => { + return http.get(`${TEST_DEVICE_BASE_URL}/${id}/report`, undefined, { responseType: 'blob' }) +} diff --git a/frontend/src/api/aireport/testDevice/interface/index.ts b/frontend/src/api/aireport/testDevice/interface/index.ts new file mode 100644 index 0000000..787f150 --- /dev/null +++ b/frontend/src/api/aireport/testDevice/interface/index.ts @@ -0,0 +1,45 @@ +export namespace TestDevice { + export type TestDeviceState = '01' | '02' | (string & {}) + + export interface TestDeviceRecord { + id?: string + type?: string | null + typeName?: string | null + no?: string | null + validityPeriod?: string | null + validityReport?: string | null + validityReportName?: string | null + validityReportUrl?: string | null + state?: TestDeviceState + status?: number + createBy?: string | null + createByName?: string | null + createTime?: string | null + updateBy?: string | null + updateByName?: string | null + updateTime?: string | null + } + + export interface TestDeviceListParams { + keyword?: string + searchBeginTime?: string + searchEndTime?: string + timeRange?: string[] + pageNum?: number + pageSize?: number + } + + export interface TestDeviceSaveRequest { + id?: string + type: string + no: string + validityPeriod: string + state?: TestDeviceState + } + + export interface TestDeviceImportResult { + successCount?: number + failCount?: number + failReasons?: string[] + } +} diff --git a/frontend/src/api/aireport/testReport/index.ts b/frontend/src/api/aireport/testReport/index.ts new file mode 100644 index 0000000..13b0ec4 --- /dev/null +++ b/frontend/src/api/aireport/testReport/index.ts @@ -0,0 +1,32 @@ +import http from '@/api' +import type { AxiosResponse } from 'axios' +import type { ReportTask } from './interface' + +const REPORT_TASK_BASE_URL = '/api/test-report' + +export const listReportTasksApi = (params: ReportTask.ReportTaskListParams) => { + return http.post(`${REPORT_TASK_BASE_URL}/list`, params) +} + +export const createReportTaskApi = (params: ReportTask.ReportTaskSaveRequest) => { + return http.post(`${REPORT_TASK_BASE_URL}/add`, params) +} + +export const updateReportTaskApi = (params: ReportTask.ReportTaskSaveRequest) => { + return http.post(`${REPORT_TASK_BASE_URL}/update`, params) +} + +export const deleteReportTasksApi = (ids: string[]) => { + return http.post(`${REPORT_TASK_BASE_URL}/delete`, ids) +} + +export const getReportTaskDetailApi = (id: string) => { + return http.get(`${REPORT_TASK_BASE_URL}/${id}`) +} + +export const exportReportTasksApi = (params: ReportTask.ReportTaskListParams): Promise> => + http.get(`${REPORT_TASK_BASE_URL}/export`, params, { responseType: 'blob' }) as unknown as Promise> + +export const submitReportTaskAuditApi = (params: ReportTask.ReportTaskAuditRequest) => { + return http.post(`${REPORT_TASK_BASE_URL}/submit-audit`, params) +} diff --git a/frontend/src/api/aireport/testReport/interface/index.ts b/frontend/src/api/aireport/testReport/interface/index.ts new file mode 100644 index 0000000..14823e9 --- /dev/null +++ b/frontend/src/api/aireport/testReport/interface/index.ts @@ -0,0 +1,52 @@ +export namespace ReportTask { + export interface ReportTaskRecord { + id?: string + no?: string | null + clientUnitId?: string | null + clientUnitName?: string | null + reportModelId?: string | null + reportModelName?: string | null + createUnit?: string | null + contractNumber?: string | null + standard?: string[] | string | null + data?: string | null + phonenumber?: string | null + checkId?: string | null + checkerName?: string | null + checkTime?: string | null + checkResult?: string | null + status?: number + createBy?: string | null + createByName?: string | null + createTime?: string | null + updateBy?: string | null + updateByName?: string | null + updateTime?: string | null + } + + export interface ReportTaskListParams { + keyword?: string + searchBeginTime?: string + searchEndTime?: string + timeRange?: string[] + pageNum?: number + pageSize?: number + } + + export interface ReportTaskSaveRequest { + id?: string + no: string + clientUnitId: string + reportModelId: string + createUnit: string + contractNumber: string + standard: string[] + data: string + phonenumber?: string + } + + export interface ReportTaskAuditRequest { + id: string + checkResult?: string + } +} diff --git a/frontend/src/api/steady/steadyDataView/interface/index.ts b/frontend/src/api/steady/steadyDataView/interface/index.ts index 956de44..36a8087 100644 --- a/frontend/src/api/steady/steadyDataView/interface/index.ts +++ b/frontend/src/api/steady/steadyDataView/interface/index.ts @@ -97,8 +97,7 @@ export namespace SteadyDataView { } export interface SteadyChecksquareCreateParams { - lineId?: string - lineIds?: string[] + lineIds: string[] indicatorCodes: string[] timeStart: string timeEnd: string diff --git a/frontend/src/constants/dictCodes.ts b/frontend/src/constants/dictCodes.ts index 7293173..ba83cdd 100644 --- a/frontend/src/constants/dictCodes.ts +++ b/frontend/src/constants/dictCodes.ts @@ -1,6 +1,8 @@ export const DICT_CODES = { USER_STATE: 'state', EVENT_TYPE: 'event_type', + TEST_REPORT_COMPANY: 'test_report_company', + TEST_REPORT_STANDARD: 'test_report_standard', LEDGER_DEVICE_TYPE: 'ledger_device_type', LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type', LEDGER_TERMINAL_MODEL: 'Dev_Type', diff --git a/frontend/src/layouts/components/Main/check-keep-alive-contract.mjs b/frontend/src/layouts/components/Main/check-keep-alive-contract.mjs index 7a8f0b8..00eeba0 100644 --- a/frontend/src/layouts/components/Main/check-keep-alive-contract.mjs +++ b/frontend/src/layouts/components/Main/check-keep-alive-contract.mjs @@ -17,8 +17,8 @@ const routeContracts = [ ['toolAddData', 'src/views/tools/addData/index.vue'], ['toolAddLedger', 'src/views/tools/addLedger/index.vue'], ['eventList', 'src/views/event/eventList/index.vue'], - ['systemMonitor', 'src/views/systemMonitor/index.vue'], - ['diskMonitor', 'src/views/systemMonitor/diskMonitor/index.vue'] + ['system-monitor', 'src/views/system-monitor/index.vue'], + ['diskMonitor', 'src/views/system-monitor/diskMonitor/index.vue'] ] const extractRouteBlock = routeName => { diff --git a/frontend/src/routers/modules/check-dbms-route-contract.mjs b/frontend/src/routers/modules/check-dbms-route-contract.mjs index cdf5e19..aed063e 100644 --- a/frontend/src/routers/modules/check-dbms-route-contract.mjs +++ b/frontend/src/routers/modules/check-dbms-route-contract.mjs @@ -9,6 +9,12 @@ const staticRouterSource = read('routers/modules/staticRouter.ts') const authStoreSource = read('stores/modules/auth.ts') const expectedPaths = [ + '/system-monitor/dbms', + '/system-monitor/dbms/index', + '/system-monitor/databaseMonitor', + '/system-monitor/databaseMonitor/index', + '/system-monitor/database-monitor', + '/system-monitor/database-monitor/index', '/systemMonitor/dbms', '/systemMonitor/dbms/index', '/systemMonitor/databaseMonitor', diff --git a/frontend/src/routers/modules/dynamicRouter.ts b/frontend/src/routers/modules/dynamicRouter.ts index 07fe80a..6e72698 100644 --- a/frontend/src/routers/modules/dynamicRouter.ts +++ b/frontend/src/routers/modules/dynamicRouter.ts @@ -27,6 +27,12 @@ const COMPONENT_PATH_ALIASES: Record = { '/steady/check-square': '/steady/checksquare', '/steady/check-square/index': '/steady/checksquare/index', // 数据库监控菜单统一落到 system-ops/dbms 页面,兼容后端菜单常见 component 写法。 + '/system-monitor/dbms': '/system-ops/dbms', + '/system-monitor/dbms/index': '/system-ops/dbms/index', + '/system-monitor/databaseMonitor': '/system-ops/dbms', + '/system-monitor/databaseMonitor/index': '/system-ops/dbms/index', + '/system-monitor/database-monitor': '/system-ops/dbms', + '/system-monitor/database-monitor/index': '/system-ops/dbms/index', '/systemMonitor/dbms': '/system-ops/dbms', '/systemMonitor/dbms/index': '/system-ops/dbms/index', '/systemMonitor/databaseMonitor': '/system-ops/dbms', @@ -34,7 +40,20 @@ const COMPONENT_PATH_ALIASES: Record = { '/systemMonitor/database-monitor': '/system-ops/dbms', '/systemMonitor/database-monitor/index': '/system-ops/dbms/index', '/system-ops/database-monitor': '/system-ops/dbms', - '/system-ops/database-monitor/index': '/system-ops/dbms/index' + '/system-ops/database-monitor/index': '/system-ops/dbms/index', + // 报告菜单后端使用 aiReport,前端目录沿用既有 aireport 命名。 + '/aiReport/reportTask': '/aireport/testReport', + '/aiReport/reportTask/index': '/aireport/testReport/index', + '/aiReport/testReport': '/aireport/testReport', + '/aiReport/testReport/index': '/aireport/testReport/index', + '/aiReport/reportModel': '/aireport/reportModel', + '/aiReport/reportModel/index': '/aireport/reportModel/index', + '/aiReport/clientUnit': '/aireport/clientUnit', + '/aiReport/clientUnit/index': '/aireport/clientUnit/index', + '/aiReport/testDevice': '/aireport/testDevice', + '/aiReport/testDevice/index': '/aireport/testDevice/index', + '/aiReport/reportApproval': '/aireport/reportApproval', + '/aiReport/reportApproval/index': '/aireport/reportApproval/index' } const STATIC_ROUTE_NAMES = new Set([ 'layout', @@ -51,6 +70,7 @@ const STATIC_ROUTE_NAMES = new Set([ 'steadyDataView', 'steadyTrend', 'checksquare', + 'system-monitor', 'systemMonitor', 'diskMonitor', 'systemOpsDbms', diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index fa79233..6a924a5 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -195,24 +195,26 @@ export const staticRouter: RouteRecordRaw[] = [ } }, { - path: '/systemMonitor', - name: 'systemMonitor', - component: () => import('@/views/systemMonitor/index.vue'), + path: '/system-monitor', + name: 'system-monitor', + alias: ['/systemMonitor'], + component: () => import('@/views/system-monitor/index.vue'), meta: { cacheName: 'SystemMonitorPage', title: '系统监控' } }, { - path: '/systemMonitor/diskMonitor', + path: '/system-monitor/diskMonitor', name: 'diskMonitor', - component: () => import('@/views/systemMonitor/diskMonitor/index.vue'), + alias: ['/systemMonitor/diskMonitor'], + component: () => import('@/views/system-monitor/diskMonitor/index.vue'), meta: { cacheName: 'DiskMonitorPage', // 磁盘监控页复用系统监控主标签 - activeMenu: '/systemMonitor', + activeMenu: '/system-monitor', hideTab: true, - parentPath: '/systemMonitor', + parentPath: '/system-monitor', title: '磁盘监控' } }, @@ -220,6 +222,12 @@ export const staticRouter: RouteRecordRaw[] = [ path: '/system-ops/dbms', name: 'systemOpsDbms', alias: [ + '/system-monitor/dbms', + '/system-monitor/dbms/index', + '/system-monitor/databaseMonitor', + '/system-monitor/databaseMonitor/index', + '/system-monitor/database-monitor', + '/system-monitor/database-monitor/index', '/systemMonitor/dbms', '/systemMonitor/dbms/index', '/systemMonitor/databaseMonitor', diff --git a/frontend/src/views/aireport/clientUnit/components/ClientUnitDetailDialog.vue b/frontend/src/views/aireport/clientUnit/components/ClientUnitDetailDialog.vue new file mode 100644 index 0000000..a6c6e4a --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/components/ClientUnitDetailDialog.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/views/aireport/clientUnit/components/ClientUnitFormDialog.vue b/frontend/src/views/aireport/clientUnit/components/ClientUnitFormDialog.vue new file mode 100644 index 0000000..1378bb4 --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/components/ClientUnitFormDialog.vue @@ -0,0 +1,135 @@ + + + diff --git a/frontend/src/views/aireport/clientUnit/components/ClientUnitImportDialog.vue b/frontend/src/views/aireport/clientUnit/components/ClientUnitImportDialog.vue new file mode 100644 index 0000000..163ed06 --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/components/ClientUnitImportDialog.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/frontend/src/views/aireport/clientUnit/contracts/check-client-unit-page-contract.mjs b/frontend/src/views/aireport/clientUnit/contracts/check-client-unit-page-contract.mjs new file mode 100644 index 0000000..9381735 --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/contracts/check-client-unit-page-contract.mjs @@ -0,0 +1,102 @@ +import fs from 'fs' +import path from 'path' + +const rootDir = path.resolve(process.cwd(), 'frontend/src') +const pageFile = path.resolve(rootDir, 'views/aireport/clientUnit/index.vue') +const apiFile = path.resolve(rootDir, 'api/aireport/clientUnit/index.ts') +const formDialogFile = path.resolve(rootDir, 'views/aireport/clientUnit/components/ClientUnitFormDialog.vue') +const detailDialogFile = path.resolve(rootDir, 'views/aireport/clientUnit/components/ClientUnitDetailDialog.vue') +const importDialogFile = path.resolve(rootDir, 'views/aireport/clientUnit/components/ClientUnitImportDialog.vue') + +const read = file => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '') + +const pageSource = read(pageFile) +const apiSource = read(apiFile) +const formDialogSource = read(formDialogFile) +const detailDialogSource = read(detailDialogFile) +const importDialogSource = read(importDialogFile) + +const checks = [ + ['clientUnit page imports ProTable', () => /import\s+ProTable\s+from\s+'@\/components\/ProTable\/index\.vue'/.test(pageSource)], + ['clientUnit page uses ProTable request API', () => / + /[\s\S]*openCreateDialog[\s\S]*handleBatchDelete\(scope\.selectedListIds\)[\s\S]*handleExport[\s\S]*handleDownloadTemplate[\s\S]*openImportDialog/.test( + pageSource + ) + ], + [ + 'clientUnit page uses form detail and import dialogs', + () => / + /[\s\S]*openDetailDialog\(row\)[\s\S]*openEditDialog\(row\)[\s\S]*handleDelete\(row\)/.test( + pageSource + ) + ], + [ + 'clientUnit search uses unit name and update time range', + () => + /prop:\s*'name'[\s\S]*search:\s*\{[\s\S]*el:\s*'input'/.test(pageSource) && + /prop:\s*'timeRange'[\s\S]*label:\s*'更新时间'[\s\S]*el:\s*'date-picker'/.test(pageSource) + ], + [ + 'clientUnit page normalizes table params and empty text', + () => /normalizeClientUnitListParams/.test(pageSource) && /resolveClientUnitText/.test(pageSource) + ], + [ + 'clientUnit api uses list add update getById delete export importTemplate and import endpoints', + () => + /clientUnit\/list/.test(apiSource) && + /clientUnit\/add/.test(apiSource) && + /clientUnit\/update/.test(apiSource) && + /clientUnit\/getById/.test(apiSource) && + /clientUnit\/delete/.test(apiSource) && + /clientUnit\/export/.test(apiSource) && + /clientUnit\/importTemplate/.test(apiSource) && + /clientUnit\/import/.test(apiSource) + ], + [ + 'clientUnit form dialog validates required fields', + () => + /prop="name"/.test(formDialogSource) && + /prop="contactName"/.test(formDialogSource) && + /prop="phonenumber"/.test(formDialogSource) && + /formRules/.test(formDialogSource) + ], + [ + 'clientUnit form dialog sanitizes placeholder dash when filling edit form', + () => /sanitizeClientUnitFormText/.test(formDialogSource) && /fillForm[\s\S]*sanitizeClientUnitFormText/.test(formDialogSource) + ], + [ + 'clientUnit detail dialog displays core fields', + () => + /单位名称/.test(detailDialogSource) && + /联系人/.test(detailDialogSource) && + /联系方式/.test(detailDialogSource) && + /地址/.test(detailDialogSource) + ], + [ + 'clientUnit import dialog selects file and displays import result', + () => + /type="file"/.test(importDialogSource) && + /successCount/.test(importDialogSource) && + /failCount/.test(importDialogSource) && + /failReasons/.test(importDialogSource) + ] +] + +const failedChecks = checks.filter(([, predicate]) => !predicate()) + +if (failedChecks.length) { + console.error('clientUnit page contract failed:') + failedChecks.forEach(([label]) => { + console.error(`- ${label}`) + }) + process.exit(1) +} + +console.log('clientUnit page contract passed') diff --git a/frontend/src/views/aireport/clientUnit/index.vue b/frontend/src/views/aireport/clientUnit/index.vue new file mode 100644 index 0000000..67decf6 --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/index.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/frontend/src/views/aireport/clientUnit/utils/clientUnit.ts b/frontend/src/views/aireport/clientUnit/utils/clientUnit.ts new file mode 100644 index 0000000..ad1bd61 --- /dev/null +++ b/frontend/src/views/aireport/clientUnit/utils/clientUnit.ts @@ -0,0 +1,61 @@ +import type { ResultData, ResPage } from '@/api/interface' +import type { ClientUnit } from '@/api/aireport/clientUnit/interface' + +export const resolveClientUnitText = (value: unknown) => { + if (value === null || value === undefined) return '-' + + const text = String(value).trim() + return text || '-' +} + +export const sanitizeClientUnitFormText = (value: unknown) => { + if (value === null || value === undefined) return '' + + const text = String(value).trim() + return text === '-' ? '' : text +} + +export const normalizeClientUnitListParams = ( + params: ClientUnit.ClientUnitListParams = {} +): ClientUnit.ClientUnitListParams => { + const timeRange = Array.isArray(params.timeRange) ? params.timeRange : [] + const [searchBeginTime = '', searchEndTime = ''] = timeRange + + return { + name: String(params.name || '').trim() || undefined, + searchBeginTime: searchBeginTime || params.searchBeginTime || undefined, + searchEndTime: searchEndTime || params.searchEndTime || undefined, + pageNum: params.pageNum || 1, + pageSize: params.pageSize || 10 + } +} + +export const unwrapClientUnitPayload = (response: ResultData | T): T => { + if (response && typeof response === 'object' && 'data' in response) { + return (response as ResultData).data + } + + return response as T +} + +export const normalizeClientUnitPage = ( + response: ResultData> | ResPage | null | undefined +): ResPage => { + const payload = unwrapClientUnitPayload | null | undefined>(response as ResultData>) + + return { + records: payload?.records || [], + current: payload?.current || 1, + size: payload?.size || 10, + total: payload?.total || 0 + } +} + +export const getClientUnitErrorMessage = (error: unknown, fallback: string) => { + if (error instanceof Error && error.message) return error.message + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message?: unknown }).message || fallback) + } + + return fallback +} diff --git a/frontend/src/views/aireport/reportApproval/components/ReportApprovalActionDialog.vue b/frontend/src/views/aireport/reportApproval/components/ReportApprovalActionDialog.vue new file mode 100644 index 0000000..02df767 --- /dev/null +++ b/frontend/src/views/aireport/reportApproval/components/ReportApprovalActionDialog.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/frontend/src/views/aireport/reportApproval/components/ReportApprovalLogTable.vue b/frontend/src/views/aireport/reportApproval/components/ReportApprovalLogTable.vue new file mode 100644 index 0000000..2a9960d --- /dev/null +++ b/frontend/src/views/aireport/reportApproval/components/ReportApprovalLogTable.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/frontend/src/views/aireport/reportApproval/components/ReportApprovalPendingTable.vue b/frontend/src/views/aireport/reportApproval/components/ReportApprovalPendingTable.vue new file mode 100644 index 0000000..bb740e2 --- /dev/null +++ b/frontend/src/views/aireport/reportApproval/components/ReportApprovalPendingTable.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/frontend/src/views/aireport/reportApproval/contracts/check-report-approval-page-contract.mjs b/frontend/src/views/aireport/reportApproval/contracts/check-report-approval-page-contract.mjs new file mode 100644 index 0000000..20e0789 --- /dev/null +++ b/frontend/src/views/aireport/reportApproval/contracts/check-report-approval-page-contract.mjs @@ -0,0 +1,89 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const pageDir = path.resolve(__dirname, '..') +const srcDir = path.resolve(pageDir, '../../..') +const routerFile = path.resolve(srcDir, 'routers/modules/dynamicRouter.ts') +const pageFile = path.resolve(pageDir, 'index.vue') +const pendingTableFile = path.resolve(pageDir, 'components/ReportApprovalPendingTable.vue') +const logTableFile = path.resolve(pageDir, 'components/ReportApprovalLogTable.vue') +const actionDialogFile = path.resolve(pageDir, 'components/ReportApprovalActionDialog.vue') +const utilsFile = path.resolve(pageDir, 'utils/reportApproval.ts') +const apiFile = path.resolve(srcDir, 'api/aireport/reportApproval/index.ts') +const apiInterfaceFile = path.resolve(srcDir, 'api/aireport/reportApproval/interface/index.ts') + +const read = file => fs.readFileSync(file, 'utf8') +const assertFileIncludes = (file, snippets) => { + const content = read(file) + const missing = snippets.filter(snippet => !content.includes(snippet)) + + if (missing.length) { + throw new Error(`${path.relative(srcDir, file)} missing: ${missing.join(', ')}`) + } +} + +assertFileIncludes(routerFile, [ + "'/aiReport/reportApproval': '/aireport/reportApproval'", + "'/aiReport/reportApproval/index': '/aireport/reportApproval/index'" +]) + +assertFileIncludes(apiFile, [ + "const REPORT_APPROVAL_BASE_URL = '/api/report-approval'", + 'listPendingReportApprovalsApi', + 'passReportApprovalApi', + 'rejectReportApprovalApi', + 'listReportApprovalLogsApi' +]) + +assertFileIncludes(apiInterfaceFile, [ + 'export namespace ReportApproval', + 'PendingRecord', + 'PendingListParams', + 'ApprovalActionRequest', + 'LogRecord', + 'LogListParams' +]) + +assertFileIncludes(utilsFile, [ + 'REPORT_APPROVAL_TYPE_OPTIONS', + 'getReportApprovalStateText', + 'getReportApprovalResultText', + 'normalizePendingReportApprovalListParams', + 'normalizeReportApprovalLogListParams', + 'validateReportApprovalReason' +]) + +assertFileIncludes(actionDialogFile, [ + "name: 'ReportApprovalActionDialog'", + 'approvalReason', + 'validateReportApprovalReason' +]) + +assertFileIncludes(pendingTableFile, [ + "name: 'ReportApprovalPendingTable'", + ' +
+ + + + + + + + + + +
+ + + + + diff --git a/frontend/src/views/aireport/reportApproval/utils/reportApproval.ts b/frontend/src/views/aireport/reportApproval/utils/reportApproval.ts new file mode 100644 index 0000000..70384f9 --- /dev/null +++ b/frontend/src/views/aireport/reportApproval/utils/reportApproval.ts @@ -0,0 +1,103 @@ +import type { TagProps } from 'element-plus' +import type { ResultData, ResPage } from '@/api/interface' +import type { ReportApproval } from '@/api/aireport/reportApproval/interface' + +type TagType = TagProps['type'] + +const REPORT_APPROVAL_STATE_MAP: Record = { + '01': { text: '编制', tagType: 'info' }, + '02': { text: '待审', tagType: 'warning' }, + '03': { text: '通过', tagType: 'success' }, + '04': { text: '退回', tagType: 'danger' }, + '05': { text: '删除', tagType: 'info' } +} + +const REPORT_APPROVAL_TYPE_MAP: Record = { + REPORT_MODEL: '报告模板', + TEST_REPORT: '普测报告' +} + +const REPORT_APPROVAL_RESULT_MAP: Record = { + '03': '通过', + '04': '退回' +} + +export const REPORT_APPROVAL_REASON_MAX_LENGTH = 128 + +export const REPORT_APPROVAL_TYPE_OPTIONS: Array<{ label: string; value: ReportApproval.ReportType }> = [ + { label: '报告模板', value: 'REPORT_MODEL' }, + { label: '普测报告', value: 'TEST_REPORT' } +] + +export const resolveReportApprovalText = (value: unknown) => { + if (value === null || value === undefined) return '-' + + const text = String(value).trim() + return text || '-' +} + +export const getReportApprovalStateText = (state?: string) => REPORT_APPROVAL_STATE_MAP[state || '']?.text || '未知状态' + +export const getReportApprovalStateTagType = (state?: string): TagType => + REPORT_APPROVAL_STATE_MAP[state || '']?.tagType || 'info' + +export const getReportApprovalTypeText = (reportType?: string) => REPORT_APPROVAL_TYPE_MAP[reportType || ''] || reportType || '-' + +export const getReportApprovalResultText = (result?: string) => REPORT_APPROVAL_RESULT_MAP[result || ''] || result || '-' + +export const normalizePendingReportApprovalListParams = ( + params: ReportApproval.PendingListParams = {} +): ReportApproval.PendingListParams => ({ + current: params.current || 1, + size: params.size || 10, + reportType: params.reportType || undefined +}) + +export const normalizeReportApprovalLogListParams = ( + params: ReportApproval.LogListParams = {} +): ReportApproval.LogListParams => ({ + current: params.current || 1, + size: params.size || 10, + reportType: params.reportType || undefined +}) + +export const unwrapReportApprovalPayload = (response: ResultData | T): T => { + if (response && typeof response === 'object' && 'data' in response) { + return (response as ResultData).data + } + + return response as T +} + +export const normalizeReportApprovalPage = ( + response: ResultData> | ResPage | null | undefined +): ResPage => { + const payload = unwrapReportApprovalPayload | null | undefined>(response as ResultData>) + + return { + records: payload?.records || [], + current: payload?.current || 1, + size: payload?.size || 10, + total: payload?.total || 0 + } +} + +export const validateReportApprovalReason = (value: string) => { + const text = value.trim() + + if (!text) return '请输入审批原因' + if (text.length > REPORT_APPROVAL_REASON_MAX_LENGTH) { + return `审批原因不能超过 ${REPORT_APPROVAL_REASON_MAX_LENGTH} 个字符` + } + + return '' +} + +export const getReportApprovalErrorMessage = (error: unknown, fallback: string) => { + if (error instanceof Error && error.message) return error.message + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message?: unknown }).message || fallback) + } + + return fallback +} diff --git a/frontend/src/views/aireport/reportModel/components/ReportModelDetailDialog.vue b/frontend/src/views/aireport/reportModel/components/ReportModelDetailDialog.vue new file mode 100644 index 0000000..8d32c92 --- /dev/null +++ b/frontend/src/views/aireport/reportModel/components/ReportModelDetailDialog.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/views/aireport/reportModel/components/ReportModelFormDialog.vue b/frontend/src/views/aireport/reportModel/components/ReportModelFormDialog.vue new file mode 100644 index 0000000..ce51224 --- /dev/null +++ b/frontend/src/views/aireport/reportModel/components/ReportModelFormDialog.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/frontend/src/views/aireport/reportModel/components/ReportModelPreviewDialog.vue b/frontend/src/views/aireport/reportModel/components/ReportModelPreviewDialog.vue new file mode 100644 index 0000000..7d3134e --- /dev/null +++ b/frontend/src/views/aireport/reportModel/components/ReportModelPreviewDialog.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/views/aireport/reportModel/contracts/check-report-model-page-contract.mjs b/frontend/src/views/aireport/reportModel/contracts/check-report-model-page-contract.mjs new file mode 100644 index 0000000..420fd64 --- /dev/null +++ b/frontend/src/views/aireport/reportModel/contracts/check-report-model-page-contract.mjs @@ -0,0 +1,51 @@ +import fs from 'fs' +import path from 'path' + +const rootDir = path.resolve(process.cwd(), 'frontend/src') +const pageFile = path.resolve(rootDir, 'views/aireport/reportModel/index.vue') +const apiFile = path.resolve(rootDir, 'api/aireport/reportModel/index.ts') +const formFile = path.resolve(rootDir, 'views/aireport/reportModel/components/ReportModelFormDialog.vue') + +const pageSource = fs.readFileSync(pageFile, 'utf8') +const apiSource = fs.readFileSync(apiFile, 'utf8') +const formSource = fs.readFileSync(formFile, 'utf8') + +const checks = [ + ['reportModel page imports ProTable', () => /import\s+ProTable\s+from\s+'@\/components\/ProTable\/index\.vue'/.test(pageSource)], + ['reportModel page uses ProTable request API', () => / /[\s\S]*openCreateDialog[\s\S]*handleBatchDelete\(scope\.selectedListIds\)/.test(pageSource)], + ['reportModel page uses form detail and preview dialogs', () => / /[\s\S]*openDetailDialog\(row\)[\s\S]*openEditDialog\(row\)[\s\S]*handleDownload\(row\)[\s\S]*openAuditDialog\(row\)[\s\S]*handleDelete\(row\)/.test(pageSource)], + ['reportModel search uses update time label', () => /prop:\s*'timeRange'[\s\S]*label:\s*'更新时间'/.test(pageSource) && /startPlaceholder:\s*'开始更新时间'/.test(pageSource) && /endPlaceholder:\s*'结束更新时间'/.test(pageSource)], + [ + 'reportModel list shows template file after name and supports file-name preview', + () => + /prop:\s*'name'[\s\S]*prop:\s*'path'[\s\S]*label:\s*'模版文件'[\s\S]*resolveReportModelFileName\(row\)[\s\S]*openPreviewDialog\(row\)/.test( + pageSource + ) + ], + [ + 'reportModel list columns keep index name path state editor update checker check-time check-result order', + () => + /type:\s*'index'[\s\S]*prop:\s*'name'[\s\S]*prop:\s*'path'[\s\S]*prop:\s*'state'[\s\S]*prop:\s*'editorName'[\s\S]*prop:\s*'updateTime'[\s\S]*prop:\s*'checkerName'[\s\S]*prop:\s*'checkTime'[\s\S]*prop:\s*'checkResult'/.test( + pageSource + ) + ], + ['reportModel list hides create time and keeps update time empty fallback', () => /prop:\s*'updateTime'[\s\S]*render:\s*\(\{\s*row\s*\}\)\s*=>\s*resolveReportModelText\(row\.updateTime\)/.test(pageSource) && !/prop:\s*'createTime'/.test(pageSource)], + ['reportModel api uses list add update delete detail download submit-audit endpoints', () => /report-model\/list/.test(apiSource) && /\/add/.test(apiSource) && /\/update/.test(apiSource) && /\/delete/.test(apiSource) && /submit-audit/.test(apiSource) && /\/download/.test(apiSource)], + ['reportModel api keeps multipart request part name', () => /formData\.append\('request'/.test(apiSource) && /formData\.append\('templateFile'/.test(apiSource)], + ['reportModel form only accepts Word documents', () => /accept="\.doc,\.docx"/.test(formSource)], + ['reportModel form keeps template name disabled while editing', () => /:disabled="mode === 'edit'"/.test(formSource)] +] + +const failedChecks = checks.filter(([, predicate]) => !predicate()) + +if (failedChecks.length) { + console.error('reportModel page contract failed:') + failedChecks.forEach(([label]) => { + console.error(`- ${label}`) + }) + process.exit(1) +} + +console.log('reportModel page contract passed') diff --git a/frontend/src/views/aireport/reportModel/index.vue b/frontend/src/views/aireport/reportModel/index.vue new file mode 100644 index 0000000..bfddce9 --- /dev/null +++ b/frontend/src/views/aireport/reportModel/index.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/frontend/src/views/aireport/reportModel/utils/reportModel.ts b/frontend/src/views/aireport/reportModel/utils/reportModel.ts new file mode 100644 index 0000000..3470530 --- /dev/null +++ b/frontend/src/views/aireport/reportModel/utils/reportModel.ts @@ -0,0 +1,125 @@ +import type { TagProps } from 'element-plus' +import type { ResultData, ResPage } from '@/api/interface' +import type { ReportModel } from '@/api/aireport/reportModel/interface' + +type TagType = TagProps['type'] +type ResponseHeaders = Record + +const REPORT_MODEL_STATE_MAP: Record = { + '01': { text: '编制', tagType: 'info' }, + '02': { text: '审核中', tagType: 'warning' }, + '03': { text: '已发布', tagType: 'success' }, + '04': { text: '已删除', tagType: 'danger' } +} + +const REPORT_MODEL_WORD_EXTENSIONS = ['doc', 'docx'] + +export const REPORT_MODEL_TEMPLATE_ACCEPT = '.doc,.docx' + +export const getReportModelStateText = (state?: string) => REPORT_MODEL_STATE_MAP[state || '']?.text || '未知状态' + +export const getReportModelStateTagType = (state?: string): TagType => REPORT_MODEL_STATE_MAP[state || '']?.tagType || 'info' + +export const isDraftReportModel = (row?: ReportModel.ReportModelRecord | null) => row?.state === '01' + +export const resolveReportModelFileName = (row?: ReportModel.ReportModelRecord | null) => + row?.fileName || row?.file_name || row?.path || '' + +export const resolveReportModelText = (value: unknown) => { + if (value === null || value === undefined) return '-' + + const text = String(value).trim() + return text || '-' +} + +export const getReportModelFileExtension = (fileName?: string | null) => { + if (!fileName) return '' + + const segments = fileName.split('.') + return segments.length > 1 ? segments.pop()!.toLowerCase() : '' +} + +export const validateReportModelTemplateFile = (file: File) => { + if (!REPORT_MODEL_WORD_EXTENSIONS.includes(getReportModelFileExtension(file.name))) { + return '报告模板仅支持 Word 文档(.doc/.docx)' + } + + return '' +} + +const decodeReportModelFileName = (value: string) => { + try { + return decodeURIComponent(value) + } catch { + return value + } +} + +export const resolveReportModelDownloadFileName = ( + headers?: ResponseHeaders | null, + fallbackName = 'report-model', + fallbackExtension = '.docx' +) => { + const contentDisposition = String(headers?.['content-disposition'] || headers?.['Content-Disposition'] || '') + const utf8Match = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(contentDisposition) + + if (utf8Match?.[1]) { + return decodeReportModelFileName(utf8Match[1].trim()) + } + + const fallbackMatch = /filename\s*=\s*([^;]+)/i.exec(contentDisposition) + + if (fallbackMatch?.[1]) { + return decodeReportModelFileName(fallbackMatch[1].trim().replace(/^["']|["']$/g, '')) + } + + return `${fallbackName}${fallbackExtension}` +} + +export const canPreviewReportModelFile = (fileName?: string | null) => getReportModelFileExtension(fileName) === 'docx' + +export const normalizeReportModelListParams = ( + params: ReportModel.ReportModelListParams = {} +): ReportModel.ReportModelListParams => { + const timeRange = Array.isArray(params.timeRange) ? params.timeRange : [] + const [searchBeginTime = '', searchEndTime = ''] = timeRange + + return { + name: String(params.name || '').trim() || undefined, + state: params.state || undefined, + searchBeginTime: searchBeginTime || params.searchBeginTime || undefined, + searchEndTime: searchEndTime || params.searchEndTime || undefined, + pageNum: params.pageNum || 1, + pageSize: params.pageSize || 10 + } +} + +export const unwrapApiPayload = (response: ResultData | T): T => { + if (response && typeof response === 'object' && 'data' in response) { + return (response as ResultData).data + } + + return response as T +} + +export const normalizeReportModelPage = ( + response: ResultData> | ResPage | null | undefined +): ResPage => { + const payload = unwrapApiPayload | null | undefined>(response as ResultData>) + + return { + records: payload?.records || [], + current: payload?.current || 1, + size: payload?.size || 10, + total: payload?.total || 0 + } +} + +export const getReportModelErrorMessage = (error: unknown, fallback: string) => { + if (error instanceof Error && error.message) return error.message + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message?: unknown }).message || fallback) + } + + return fallback +} diff --git a/frontend/src/views/aireport/testDevice/components/TestDeviceDetailDialog.vue b/frontend/src/views/aireport/testDevice/components/TestDeviceDetailDialog.vue new file mode 100644 index 0000000..1519568 --- /dev/null +++ b/frontend/src/views/aireport/testDevice/components/TestDeviceDetailDialog.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/views/aireport/testDevice/components/TestDeviceFormDialog.vue b/frontend/src/views/aireport/testDevice/components/TestDeviceFormDialog.vue new file mode 100644 index 0000000..322330d --- /dev/null +++ b/frontend/src/views/aireport/testDevice/components/TestDeviceFormDialog.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/frontend/src/views/aireport/testDevice/components/TestDeviceImportDialog.vue b/frontend/src/views/aireport/testDevice/components/TestDeviceImportDialog.vue new file mode 100644 index 0000000..98f3811 --- /dev/null +++ b/frontend/src/views/aireport/testDevice/components/TestDeviceImportDialog.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/frontend/src/views/aireport/testDevice/components/TestDeviceReportPreviewDialog.vue b/frontend/src/views/aireport/testDevice/components/TestDeviceReportPreviewDialog.vue new file mode 100644 index 0000000..f94715f --- /dev/null +++ b/frontend/src/views/aireport/testDevice/components/TestDeviceReportPreviewDialog.vue @@ -0,0 +1,107 @@ +