refactor(steady-checksquare): 更新稳态校验功能的数据结构和接口规范
- 移除 lineId 字段,统一使用 lineIds 数组字段 - 更新创建任务请求体构建逻辑,不再包含 lineId 属性 - 修改任务详情和检测项明细的接口契约验证 - 更新稳态校验 API 文档说明,移除对 lineId 的支持 - 重构稳态校验表单项验证规则 - 移除磁盘监控作业详情抽屉组件 - 移除磁盘监控作业表格组件 - 更新路由配置以支持新的系统监控路径 - 添加测试报告相关字典代码常量 - 更新 ICD 路径表单和结果面板功能
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
"driver.js": "^1.3.0",
|
"driver.js": "^1.3.0",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
|
|||||||
48
frontend/src/api/aireport/clientUnit/index.ts
Normal file
48
frontend/src/api/aireport/clientUnit/index.ts
Normal file
@@ -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<boolean>('/clientUnit/add', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateClientUnitApi = (params: ClientUnit.ClientUnitSaveRequest) => {
|
||||||
|
return http.post<boolean>('/clientUnit/update', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getClientUnitDetailApi = (id: string) => {
|
||||||
|
return http.get<ClientUnit.ClientUnitRecord>('/clientUnit/getById', { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteClientUnitsApi = (ids: string[]) => {
|
||||||
|
return http.post<boolean>('/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.ClientUnitImportResult>(
|
||||||
|
'/clientUnit/import',
|
||||||
|
buildClientUnitImportFormData(file),
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
36
frontend/src/api/aireport/clientUnit/interface/index.ts
Normal file
36
frontend/src/api/aireport/clientUnit/interface/index.ts
Normal file
@@ -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[]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/src/api/aireport/reportApproval/index.ts
Normal file
16
frontend/src/api/aireport/reportApproval/index.ts
Normal file
@@ -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<boolean>(`${REPORT_APPROVAL_BASE_URL}/pass`, params)
|
||||||
|
|
||||||
|
export const rejectReportApprovalApi = (params: ReportApproval.ApprovalActionRequest) =>
|
||||||
|
http.post<boolean>(`${REPORT_APPROVAL_BASE_URL}/reject`, params)
|
||||||
|
|
||||||
|
export const listReportApprovalLogsApi = (params: ReportApproval.LogListParams) =>
|
||||||
|
http.get(`${REPORT_APPROVAL_BASE_URL}/log-page`, params)
|
||||||
46
frontend/src/api/aireport/reportApproval/interface/index.ts
Normal file
46
frontend/src/api/aireport/reportApproval/interface/index.ts
Normal file
@@ -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 | ''
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/src/api/aireport/reportModel/index.ts
Normal file
49
frontend/src/api/aireport/reportModel/index.ts
Normal file
@@ -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<ReportModel.ReportModelRecord>(`${REPORT_MODEL_BASE_URL}/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createReportModelApi = (request: ReportModel.ReportModelSaveRequest, templateFile: File) => {
|
||||||
|
return http.post<boolean>(`${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<boolean>(`${REPORT_MODEL_BASE_URL}/update`, buildReportModelFormData(request, templateFile), {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteReportModelsApi = (ids: string[]) => {
|
||||||
|
return http.post<boolean>(`${REPORT_MODEL_BASE_URL}/delete`, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadReportModelApi = (id: string): Promise<AxiosResponse<Blob>> =>
|
||||||
|
http.get(`${REPORT_MODEL_BASE_URL}/${id}/download`, undefined, { responseType: 'blob' }) as unknown as Promise<AxiosResponse<Blob>>
|
||||||
|
|
||||||
|
export const submitReportModelAuditApi = (params: ReportModel.ReportModelAuditRequest) => {
|
||||||
|
return http.post<boolean>(`${REPORT_MODEL_BASE_URL}/submit-audit`, params)
|
||||||
|
}
|
||||||
43
frontend/src/api/aireport/reportModel/interface/index.ts
Normal file
43
frontend/src/api/aireport/reportModel/interface/index.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
77
frontend/src/api/aireport/testDevice/index.ts
Normal file
77
frontend/src/api/aireport/testDevice/index.ts
Normal file
@@ -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<boolean>(`${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<boolean>(`${TEST_DEVICE_BASE_URL}/update`, buildTestDeviceFormData(request, reportFile), {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteTestDevicesApi = (ids: string[]) => {
|
||||||
|
return http.post<boolean>(`${TEST_DEVICE_BASE_URL}/delete`, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTestDeviceDetailApi = (id: string) => {
|
||||||
|
return http.get<TestDevice.TestDeviceRecord>(`${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<TestDevice.TestDeviceImportResult>(
|
||||||
|
`${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' })
|
||||||
|
}
|
||||||
45
frontend/src/api/aireport/testDevice/interface/index.ts
Normal file
45
frontend/src/api/aireport/testDevice/interface/index.ts
Normal file
@@ -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[]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
frontend/src/api/aireport/testReport/index.ts
Normal file
32
frontend/src/api/aireport/testReport/index.ts
Normal file
@@ -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<boolean>(`${REPORT_TASK_BASE_URL}/add`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateReportTaskApi = (params: ReportTask.ReportTaskSaveRequest) => {
|
||||||
|
return http.post<boolean>(`${REPORT_TASK_BASE_URL}/update`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteReportTasksApi = (ids: string[]) => {
|
||||||
|
return http.post<boolean>(`${REPORT_TASK_BASE_URL}/delete`, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getReportTaskDetailApi = (id: string) => {
|
||||||
|
return http.get<ReportTask.ReportTaskRecord>(`${REPORT_TASK_BASE_URL}/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exportReportTasksApi = (params: ReportTask.ReportTaskListParams): Promise<AxiosResponse<Blob>> =>
|
||||||
|
http.get(`${REPORT_TASK_BASE_URL}/export`, params, { responseType: 'blob' }) as unknown as Promise<AxiosResponse<Blob>>
|
||||||
|
|
||||||
|
export const submitReportTaskAuditApi = (params: ReportTask.ReportTaskAuditRequest) => {
|
||||||
|
return http.post<boolean>(`${REPORT_TASK_BASE_URL}/submit-audit`, params)
|
||||||
|
}
|
||||||
52
frontend/src/api/aireport/testReport/interface/index.ts
Normal file
52
frontend/src/api/aireport/testReport/interface/index.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,8 +97,7 @@ export namespace SteadyDataView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SteadyChecksquareCreateParams {
|
export interface SteadyChecksquareCreateParams {
|
||||||
lineId?: string
|
lineIds: string[]
|
||||||
lineIds?: string[]
|
|
||||||
indicatorCodes: string[]
|
indicatorCodes: string[]
|
||||||
timeStart: string
|
timeStart: string
|
||||||
timeEnd: string
|
timeEnd: string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export const DICT_CODES = {
|
export const DICT_CODES = {
|
||||||
USER_STATE: 'state',
|
USER_STATE: 'state',
|
||||||
EVENT_TYPE: 'event_type',
|
EVENT_TYPE: 'event_type',
|
||||||
|
TEST_REPORT_COMPANY: 'test_report_company',
|
||||||
|
TEST_REPORT_STANDARD: 'test_report_standard',
|
||||||
LEDGER_DEVICE_TYPE: 'ledger_device_type',
|
LEDGER_DEVICE_TYPE: 'ledger_device_type',
|
||||||
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type',
|
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type',
|
||||||
LEDGER_TERMINAL_MODEL: 'Dev_Type',
|
LEDGER_TERMINAL_MODEL: 'Dev_Type',
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const routeContracts = [
|
|||||||
['toolAddData', 'src/views/tools/addData/index.vue'],
|
['toolAddData', 'src/views/tools/addData/index.vue'],
|
||||||
['toolAddLedger', 'src/views/tools/addLedger/index.vue'],
|
['toolAddLedger', 'src/views/tools/addLedger/index.vue'],
|
||||||
['eventList', 'src/views/event/eventList/index.vue'],
|
['eventList', 'src/views/event/eventList/index.vue'],
|
||||||
['systemMonitor', 'src/views/systemMonitor/index.vue'],
|
['system-monitor', 'src/views/system-monitor/index.vue'],
|
||||||
['diskMonitor', 'src/views/systemMonitor/diskMonitor/index.vue']
|
['diskMonitor', 'src/views/system-monitor/diskMonitor/index.vue']
|
||||||
]
|
]
|
||||||
|
|
||||||
const extractRouteBlock = routeName => {
|
const extractRouteBlock = routeName => {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ const staticRouterSource = read('routers/modules/staticRouter.ts')
|
|||||||
const authStoreSource = read('stores/modules/auth.ts')
|
const authStoreSource = read('stores/modules/auth.ts')
|
||||||
|
|
||||||
const expectedPaths = [
|
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',
|
||||||
'/systemMonitor/dbms/index',
|
'/systemMonitor/dbms/index',
|
||||||
'/systemMonitor/databaseMonitor',
|
'/systemMonitor/databaseMonitor',
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
|||||||
'/steady/check-square': '/steady/checksquare',
|
'/steady/check-square': '/steady/checksquare',
|
||||||
'/steady/check-square/index': '/steady/checksquare/index',
|
'/steady/check-square/index': '/steady/checksquare/index',
|
||||||
// 数据库监控菜单统一落到 system-ops/dbms 页面,兼容后端菜单常见 component 写法。
|
// 数据库监控菜单统一落到 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': '/system-ops/dbms',
|
||||||
'/systemMonitor/dbms/index': '/system-ops/dbms/index',
|
'/systemMonitor/dbms/index': '/system-ops/dbms/index',
|
||||||
'/systemMonitor/databaseMonitor': '/system-ops/dbms',
|
'/systemMonitor/databaseMonitor': '/system-ops/dbms',
|
||||||
@@ -34,7 +40,20 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
|||||||
'/systemMonitor/database-monitor': '/system-ops/dbms',
|
'/systemMonitor/database-monitor': '/system-ops/dbms',
|
||||||
'/systemMonitor/database-monitor/index': '/system-ops/dbms/index',
|
'/systemMonitor/database-monitor/index': '/system-ops/dbms/index',
|
||||||
'/system-ops/database-monitor': '/system-ops/dbms',
|
'/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([
|
const STATIC_ROUTE_NAMES = new Set([
|
||||||
'layout',
|
'layout',
|
||||||
@@ -51,6 +70,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
|||||||
'steadyDataView',
|
'steadyDataView',
|
||||||
'steadyTrend',
|
'steadyTrend',
|
||||||
'checksquare',
|
'checksquare',
|
||||||
|
'system-monitor',
|
||||||
'systemMonitor',
|
'systemMonitor',
|
||||||
'diskMonitor',
|
'diskMonitor',
|
||||||
'systemOpsDbms',
|
'systemOpsDbms',
|
||||||
|
|||||||
@@ -195,24 +195,26 @@ export const staticRouter: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/systemMonitor',
|
path: '/system-monitor',
|
||||||
name: 'systemMonitor',
|
name: 'system-monitor',
|
||||||
component: () => import('@/views/systemMonitor/index.vue'),
|
alias: ['/systemMonitor'],
|
||||||
|
component: () => import('@/views/system-monitor/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
cacheName: 'SystemMonitorPage',
|
cacheName: 'SystemMonitorPage',
|
||||||
title: '系统监控'
|
title: '系统监控'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/systemMonitor/diskMonitor',
|
path: '/system-monitor/diskMonitor',
|
||||||
name: 'diskMonitor',
|
name: 'diskMonitor',
|
||||||
component: () => import('@/views/systemMonitor/diskMonitor/index.vue'),
|
alias: ['/systemMonitor/diskMonitor'],
|
||||||
|
component: () => import('@/views/system-monitor/diskMonitor/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
cacheName: 'DiskMonitorPage',
|
cacheName: 'DiskMonitorPage',
|
||||||
// 磁盘监控页复用系统监控主标签
|
// 磁盘监控页复用系统监控主标签
|
||||||
activeMenu: '/systemMonitor',
|
activeMenu: '/system-monitor',
|
||||||
hideTab: true,
|
hideTab: true,
|
||||||
parentPath: '/systemMonitor',
|
parentPath: '/system-monitor',
|
||||||
title: '磁盘监控'
|
title: '磁盘监控'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -220,6 +222,12 @@ export const staticRouter: RouteRecordRaw[] = [
|
|||||||
path: '/system-ops/dbms',
|
path: '/system-ops/dbms',
|
||||||
name: 'systemOpsDbms',
|
name: 'systemOpsDbms',
|
||||||
alias: [
|
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',
|
||||||
'/systemMonitor/dbms/index',
|
'/systemMonitor/dbms/index',
|
||||||
'/systemMonitor/databaseMonitor',
|
'/systemMonitor/databaseMonitor',
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visibleProxy" title="委托单位详情" width="720px" append-to-body destroy-on-close>
|
||||||
|
<div v-loading="loading" class="client-unit-detail-dialog">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="单位名称">{{ resolveClientUnitText(detail?.name) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人">{{ resolveClientUnitText(detail?.contactName) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系方式">{{ resolveClientUnitText(detail?.phonenumber) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="地址">{{ resolveClientUnitText(detail?.address) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">{{ resolveClientUnitText(detail?.createBy) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ resolveClientUnitText(detail?.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新人">{{ resolveClientUnitText(detail?.updateBy) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ resolveClientUnitText(detail?.updateTime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ClientUnit } from '@/api/aireport/clientUnit/interface'
|
||||||
|
import { resolveClientUnitText } from '../utils/clientUnit'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ClientUnitDetailDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
detail: ClientUnit.ClientUnitRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.client-unit-detail-dialog {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="mode === 'create' ? '新增委托单位' : '编辑委托单位'"
|
||||||
|
width="620px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="96px">
|
||||||
|
<el-form-item label="单位名称" prop="name">
|
||||||
|
<el-input v-model="formModel.name" maxlength="32" show-word-limit placeholder="请输入单位名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系人" prop="contactName">
|
||||||
|
<el-input v-model="formModel.contactName" maxlength="200" show-word-limit placeholder="请输入联系人" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系方式" prop="phonenumber">
|
||||||
|
<el-input v-model="formModel.phonenumber" maxlength="32" show-word-limit placeholder="请输入联系方式" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="address">
|
||||||
|
<el-input
|
||||||
|
v-model="formModel.address"
|
||||||
|
type="textarea"
|
||||||
|
maxlength="128"
|
||||||
|
show-word-limit
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage, type FormInstance, type FormItemRule, type FormRules } from 'element-plus'
|
||||||
|
import type { ClientUnit } from '@/api/aireport/clientUnit/interface'
|
||||||
|
import { sanitizeClientUnitFormText } from '../utils/clientUnit'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ClientUnitFormDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
mode: 'create' | 'edit'
|
||||||
|
record: ClientUnit.ClientUnitRecord | null
|
||||||
|
saving: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: ClientUnit.ClientUnitSaveRequest): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const formModel = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
contactName: '',
|
||||||
|
phonenumber: '',
|
||||||
|
address: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const MOBILE_PHONE_REGEXP = /^1[3-9]\d{9}$/
|
||||||
|
|
||||||
|
const validatePhoneNumber: FormItemRule['validator'] = (_rule, value, callback) => {
|
||||||
|
const normalizedValue = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!normalizedValue || MOBILE_PHONE_REGEXP.test(normalizedValue)) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(new Error('请输入正确的手机号'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
name: [{ required: true, message: '请输入单位名称', trigger: 'blur' }],
|
||||||
|
phonenumber: [{ validator: validatePhoneNumber, trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
formModel.id = ''
|
||||||
|
formModel.name = ''
|
||||||
|
formModel.contactName = ''
|
||||||
|
formModel.phonenumber = ''
|
||||||
|
formModel.address = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillForm = () => {
|
||||||
|
formModel.id = props.record?.id || ''
|
||||||
|
formModel.name = sanitizeClientUnitFormText(props.record?.name)
|
||||||
|
formModel.contactName = sanitizeClientUnitFormText(props.record?.contactName)
|
||||||
|
formModel.phonenumber = sanitizeClientUnitFormText(props.record?.phonenumber)
|
||||||
|
formModel.address = sanitizeClientUnitFormText(props.record?.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.mode, props.record?.id],
|
||||||
|
([visible]) => {
|
||||||
|
if (visible) fillForm()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
if (props.mode === 'edit' && !formModel.id) {
|
||||||
|
ElMessage.warning('当前委托单位缺少 ID,无法保存编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
id: formModel.id || undefined,
|
||||||
|
name: formModel.name.trim(),
|
||||||
|
contactName: formModel.contactName.trim() || null,
|
||||||
|
phonenumber: formModel.phonenumber.trim() || null,
|
||||||
|
address: formModel.address.trim() || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visibleProxy" title="导入委托单位" width="640px" append-to-body destroy-on-close @closed="resetImport">
|
||||||
|
<div class="client-unit-import-dialog">
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input :model-value="selectedFileName" readonly placeholder="请选择 .xlsx 文件" />
|
||||||
|
<el-button type="primary" plain :icon="FolderOpened" :disabled="importing" @click="openFilePicker">
|
||||||
|
选择文件
|
||||||
|
</el-button>
|
||||||
|
<input ref="fileInputRef" class="hidden-file-input" type="file" accept=".xlsx" @change="handleFileChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="result"
|
||||||
|
class="import-result"
|
||||||
|
:title="`导入完成:成功 ${result.successCount || 0} 条,失败 ${result.failCount || 0} 条`"
|
||||||
|
:type="result.failCount ? 'warning' : 'success'"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table v-if="result?.failReasons?.length" class="fail-reason-table" :data="failReasonRows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column prop="reason" label="失败原因" min-width="220" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="importing" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="importing" @click="submitImport">开始导入</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { ClientUnit } from '@/api/aireport/clientUnit/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ClientUnitImportDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
importing: boolean
|
||||||
|
result: ClientUnit.ClientUnitImportResult | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: File): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedFileName = computed(() => selectedFile.value?.name || '')
|
||||||
|
const failReasonRows = computed(() => (props.result?.failReasons || []).map(reason => ({ reason })))
|
||||||
|
|
||||||
|
const openFilePicker = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0] || null
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
selectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetImport = () => {
|
||||||
|
selectedFile.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitImport = () => {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
ElMessage.warning('请选择要导入的文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', selectedFile.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.client-unit-import-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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', () => /<ProTable[\s\S]*:request-api="getTableList"/.test(pageSource)],
|
||||||
|
[
|
||||||
|
'clientUnit page has create batch delete export template and import header actions',
|
||||||
|
() =>
|
||||||
|
/<template\s+#tableHeader="scope">[\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',
|
||||||
|
() => /<ClientUnitFormDialog[\s\S]*<ClientUnitDetailDialog[\s\S]*<ClientUnitImportDialog/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'clientUnit row actions include detail edit and delete',
|
||||||
|
() =>
|
||||||
|
/<template\s+#operation="\{\s*row\s*\}">[\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')
|
||||||
311
frontend/src/views/aireport/clientUnit/index.vue
Normal file
311
frontend/src/views/aireport/clientUnit/index.vue
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box client-unit-page">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #tableHeader="scope">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleBatchDelete(scope.selectedListIds)"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Download" @click="handleExport">导出</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Document" @click="handleDownloadTemplate">下载模板</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Upload" @click="openImportDialog">导入</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button link type="primary" :icon="View" @click="openDetailDialog(row)">详情</el-button>
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
|
||||||
|
<ClientUnitFormDialog
|
||||||
|
v-model:visible="formDialogVisible"
|
||||||
|
:mode="formMode"
|
||||||
|
:record="currentRecord"
|
||||||
|
:saving="formSaving"
|
||||||
|
@submit="handleSaveClientUnit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClientUnitDetailDialog
|
||||||
|
v-model:visible="detailDialogVisible"
|
||||||
|
:loading="detailLoading"
|
||||||
|
:detail="detailRecord"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClientUnitImportDialog
|
||||||
|
v-model:visible="importDialogVisible"
|
||||||
|
:importing="importing"
|
||||||
|
:result="importResult"
|
||||||
|
@submit="handleImportClientUnits"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { Delete, Document, Download, Edit, Plus, Upload, View } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import type { ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import {
|
||||||
|
createClientUnitApi,
|
||||||
|
deleteClientUnitsApi,
|
||||||
|
downloadClientUnitImportTemplateApi,
|
||||||
|
exportClientUnitsApi,
|
||||||
|
getClientUnitDetailApi,
|
||||||
|
importClientUnitsApi,
|
||||||
|
listClientUnitsApi,
|
||||||
|
updateClientUnitApi
|
||||||
|
} from '@/api/aireport/clientUnit'
|
||||||
|
import type { ClientUnit } from '@/api/aireport/clientUnit/interface'
|
||||||
|
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
|
||||||
|
import ClientUnitDetailDialog from './components/ClientUnitDetailDialog.vue'
|
||||||
|
import ClientUnitFormDialog from './components/ClientUnitFormDialog.vue'
|
||||||
|
import ClientUnitImportDialog from './components/ClientUnitImportDialog.vue'
|
||||||
|
import {
|
||||||
|
getClientUnitErrorMessage,
|
||||||
|
normalizeClientUnitListParams,
|
||||||
|
normalizeClientUnitPage,
|
||||||
|
resolveClientUnitText,
|
||||||
|
unwrapClientUnitPayload
|
||||||
|
} from './utils/clientUnit'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ClientUnitPage'
|
||||||
|
})
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const formSaving = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const importing = ref(false)
|
||||||
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
|
const currentRecord = ref<ClientUnit.ClientUnitRecord | null>(null)
|
||||||
|
const detailRecord = ref<ClientUnit.ClientUnitRecord | null>(null)
|
||||||
|
const importResult = ref<ClientUnit.ClientUnitImportResult | null>(null)
|
||||||
|
|
||||||
|
const currentSearchParams = computed(() =>
|
||||||
|
normalizeClientUnitListParams((proTable.value?.searchParam || {}) as ClientUnit.ClientUnitListParams)
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ClientUnit.ClientUnitRecord>[]>([
|
||||||
|
{ type: 'selection', fixed: 'left', width: 70 },
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'name',
|
||||||
|
label: '单位名称',
|
||||||
|
minWidth: 180,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
search: {
|
||||||
|
el: 'input',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请输入单位名称'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: ({ row }) => resolveClientUnitText(row.name)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'timeRange',
|
||||||
|
label: '更新时间',
|
||||||
|
minWidth: 220,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'date-picker',
|
||||||
|
order: 2,
|
||||||
|
key: 'timeRange',
|
||||||
|
props: {
|
||||||
|
type: 'datetimerange',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
startPlaceholder: '开始更新时间',
|
||||||
|
endPlaceholder: '结束更新时间'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prop: 'contactName', label: '联系人', minWidth: 130, render: ({ row }) => resolveClientUnitText(row.contactName) },
|
||||||
|
{ prop: 'phonenumber', label: '联系方式', minWidth: 150, render: ({ row }) => resolveClientUnitText(row.phonenumber) },
|
||||||
|
{
|
||||||
|
prop: 'address',
|
||||||
|
label: '地址',
|
||||||
|
minWidth: 220,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => resolveClientUnitText(row.address)
|
||||||
|
},
|
||||||
|
{ prop: 'createBy', label: '创建人', width: 120, render: ({ row }) => resolveClientUnitText(row.createBy) },
|
||||||
|
{ prop: 'createTime', label: '创建时间', minWidth: 170, render: ({ row }) => resolveClientUnitText(row.createTime) },
|
||||||
|
{ prop: 'updateBy', label: '更新人', width: 120, render: ({ row }) => resolveClientUnitText(row.updateBy) },
|
||||||
|
{ prop: 'updateTime', label: '更新时间', minWidth: 170, render: ({ row }) => resolveClientUnitText(row.updateTime) },
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 260 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getTableList = async (params: ClientUnit.ClientUnitListParams = {}) => {
|
||||||
|
const response = await listClientUnitsApi(normalizeClientUnitListParams(params))
|
||||||
|
const pageData = normalizeClientUnitPage<ClientUnit.ClientUnitRecord>(
|
||||||
|
response as unknown as ResPage<ClientUnit.ClientUnitRecord>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshClientUnits = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getClientUnitErrorMessage(error, '委托单位列表查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
formMode.value = 'create'
|
||||||
|
currentRecord.value = null
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditDialog = (row: ClientUnit.ClientUnitRecord) => {
|
||||||
|
formMode.value = 'edit'
|
||||||
|
currentRecord.value = row
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetailDialog = async (row: ClientUnit.ClientUnitRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前委托单位缺少 ID,无法查看详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRecord.value = null
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 详情字段以 getById 为准,避免列表字段缺失时展示不完整。
|
||||||
|
const response = await getClientUnitDetailApi(row.id)
|
||||||
|
detailRecord.value = unwrapClientUnitPayload<ClientUnit.ClientUnitRecord>(response)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getClientUnitErrorMessage(error, '委托单位详情查询失败'))
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImportDialog = () => {
|
||||||
|
importResult.value = null
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveClientUnit = async (params: ClientUnit.ClientUnitSaveRequest) => {
|
||||||
|
formSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (formMode.value === 'create') {
|
||||||
|
await createClientUnitApi(params)
|
||||||
|
ElMessage.success('委托单位新增成功')
|
||||||
|
} else {
|
||||||
|
await updateClientUnitApi(params)
|
||||||
|
ElMessage.success('委托单位编辑成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
formDialogVisible.value = false
|
||||||
|
refreshClientUnits()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getClientUnitErrorMessage(error, '委托单位保存失败'))
|
||||||
|
} finally {
|
||||||
|
formSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteClientUnits = async (ids: string[], successMessage: string) => {
|
||||||
|
if (!ids.length) {
|
||||||
|
ElMessage.warning('请选择要删除的委托单位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('删除后委托单位将不可见,是否继续?', '删除委托单位', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteClientUnitsApi(ids)
|
||||||
|
ElMessage.success(successMessage)
|
||||||
|
refreshClientUnits()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getClientUnitErrorMessage(error, '委托单位删除失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row: ClientUnit.ClientUnitRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前委托单位缺少 ID,无法删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteClientUnits([row.id], '委托单位删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBatchDelete = async (ids: string[]) => {
|
||||||
|
await deleteClientUnits(ids, '委托单位批量删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
await useDownloadWithServerFileName(exportClientUnitsApi, '委托单位列表', currentSearchParams.value, false, '.xlsx')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadTemplate = async () => {
|
||||||
|
await useDownloadWithServerFileName(
|
||||||
|
() => downloadClientUnitImportTemplateApi(),
|
||||||
|
'委托单位导入模板',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'.xlsx'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportClientUnits = async (file: File) => {
|
||||||
|
importing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 导入接口支持部分成功,结果保留在弹窗内供用户核对失败原因。
|
||||||
|
const response = await importClientUnitsApi(file)
|
||||||
|
importResult.value = unwrapClientUnitPayload<ClientUnit.ClientUnitImportResult>(response)
|
||||||
|
ElMessage.success('委托单位导入完成')
|
||||||
|
refreshClientUnits()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getClientUnitErrorMessage(error, '委托单位导入失败'))
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.client-unit-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
frontend/src/views/aireport/clientUnit/utils/clientUnit.ts
Normal file
61
frontend/src/views/aireport/clientUnit/utils/clientUnit.ts
Normal file
@@ -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 = <T>(response: ResultData<T> | T): T => {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeClientUnitPage = <T>(
|
||||||
|
response: ResultData<ResPage<T>> | ResPage<T> | null | undefined
|
||||||
|
): ResPage<T> => {
|
||||||
|
const payload = unwrapClientUnitPayload<ResPage<T> | null | undefined>(response as ResultData<ResPage<T>>)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formState" :rules="formRules" label-width="88px">
|
||||||
|
<el-form-item label="报告名称">
|
||||||
|
<div class="report-approval-action-dialog__text">{{ resolveReportApprovalText(record?.reportName) }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报告类型">
|
||||||
|
<div class="report-approval-action-dialog__text">{{ getReportApprovalTypeText(record?.reportType) }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="审批原因" prop="approvalReason">
|
||||||
|
<el-input
|
||||||
|
v-model="formState.approvalReason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
maxlength="128"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入审批原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import type { ReportApproval } from '@/api/aireport/reportApproval/interface'
|
||||||
|
import {
|
||||||
|
getReportApprovalTypeText,
|
||||||
|
resolveReportApprovalText,
|
||||||
|
validateReportApprovalReason
|
||||||
|
} from '../utils/reportApproval'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportApprovalActionDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
mode: 'pass' | 'reject'
|
||||||
|
record: ReportApproval.PendingRecord | null
|
||||||
|
saving?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean]
|
||||||
|
submit: [payload: ReportApproval.ApprovalActionRequest]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const formState = reactive({
|
||||||
|
approvalReason: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => (props.mode === 'pass' ? '审批通过' : '审批退回'))
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
approvalReason: [
|
||||||
|
{
|
||||||
|
validator: (_rule, value: string, callback) => {
|
||||||
|
const errorMessage = validateReportApprovalReason(String(value || ''))
|
||||||
|
callback(errorMessage ? new Error(errorMessage) : undefined)
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
visible => {
|
||||||
|
if (!visible) {
|
||||||
|
formState.approvalReason = ''
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!props.record?.reportId || !props.record.reportType) return
|
||||||
|
|
||||||
|
await formRef.value?.validate()
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
reportId: props.record.reportId,
|
||||||
|
reportType: props.record.reportType,
|
||||||
|
approvalReason: formState.approvalReason.trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-approval-action-dialog__text {
|
||||||
|
min-height: 32px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 32px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box report-approval-log-table">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" plain :icon="Refresh" @click="refreshLogs">刷新</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, type TagProps } from 'element-plus'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import type { ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import { listReportApprovalLogsApi } from '@/api/aireport/reportApproval'
|
||||||
|
import type { ReportApproval } from '@/api/aireport/reportApproval/interface'
|
||||||
|
import {
|
||||||
|
REPORT_APPROVAL_TYPE_OPTIONS,
|
||||||
|
getReportApprovalErrorMessage,
|
||||||
|
getReportApprovalResultText,
|
||||||
|
getReportApprovalStateTagType,
|
||||||
|
getReportApprovalStateText,
|
||||||
|
getReportApprovalTypeText,
|
||||||
|
normalizeReportApprovalLogListParams,
|
||||||
|
normalizeReportApprovalPage,
|
||||||
|
resolveReportApprovalText
|
||||||
|
} from '../utils/reportApproval'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportApprovalLogTable'
|
||||||
|
})
|
||||||
|
|
||||||
|
type TagType = TagProps['type']
|
||||||
|
|
||||||
|
const RESULT_OPTIONS: Array<{ label: string; value: ReportApproval.ApprovalResult; tagType: TagType }> = [
|
||||||
|
{ label: '通过', value: '03', tagType: 'success' },
|
||||||
|
{ label: '退回', value: '04', tagType: 'danger' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ReportApproval.LogRecord>[]>([
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'reportType',
|
||||||
|
label: '报告类型',
|
||||||
|
width: 140,
|
||||||
|
enum: REPORT_APPROVAL_TYPE_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: ({ row }) => getReportApprovalTypeText(row.reportType),
|
||||||
|
search: {
|
||||||
|
el: 'select',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择报告类型'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'reportName',
|
||||||
|
label: '报告名称',
|
||||||
|
minWidth: 220,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => resolveReportApprovalText(row.reportName)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'approvalResult',
|
||||||
|
label: '审批结果',
|
||||||
|
width: 120,
|
||||||
|
enum: RESULT_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: ({ row }) => (
|
||||||
|
<el-tag type={getReportApprovalStateTagType(row.approvalResult)} effect="light">
|
||||||
|
{getReportApprovalResultText(row.approvalResult)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'approvalReason',
|
||||||
|
label: '审批原因',
|
||||||
|
minWidth: 260,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => resolveReportApprovalText(row.approvalReason)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'beforeState',
|
||||||
|
label: '审批前状态',
|
||||||
|
width: 120,
|
||||||
|
render: ({ row }) => (
|
||||||
|
<el-tag type={getReportApprovalStateTagType(row.beforeState)} effect="light">
|
||||||
|
{getReportApprovalStateText(row.beforeState)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'afterState',
|
||||||
|
label: '审批后状态',
|
||||||
|
width: 120,
|
||||||
|
render: ({ row }) => (
|
||||||
|
<el-tag type={getReportApprovalStateTagType(row.afterState)} effect="light">
|
||||||
|
{getReportApprovalStateText(row.afterState)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'approverName', label: '审批人', width: 140, render: ({ row }) => resolveReportApprovalText(row.approverName) },
|
||||||
|
{ prop: 'approvalTime', label: '审批时间', minWidth: 180, render: ({ row }) => resolveReportApprovalText(row.approvalTime) }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getTableList = async (params: ReportApproval.LogListParams = {}) => {
|
||||||
|
const response = await listReportApprovalLogsApi(normalizeReportApprovalLogListParams(params))
|
||||||
|
const pageData = normalizeReportApprovalPage<ReportApproval.LogRecord>(response as unknown as ResPage<ReportApproval.LogRecord>)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getReportApprovalErrorMessage(error, '审批日志查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshLogs = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshLogs
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-approval-log-table {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box report-approval-pending-table">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="reportId"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" plain :icon="Refresh" @click="refreshPendingReports">刷新</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button link type="primary" :icon="CircleCheck" @click="handlePass(row)">通过</el-button>
|
||||||
|
<el-button link type="danger" :icon="RefreshLeft" @click="handleReject(row)">退回</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { CircleCheck, Refresh, RefreshLeft } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, type TagProps } from 'element-plus'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import type { ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import { listPendingReportApprovalsApi } from '@/api/aireport/reportApproval'
|
||||||
|
import type { ReportApproval } from '@/api/aireport/reportApproval/interface'
|
||||||
|
import {
|
||||||
|
REPORT_APPROVAL_TYPE_OPTIONS,
|
||||||
|
getReportApprovalErrorMessage,
|
||||||
|
getReportApprovalStateTagType,
|
||||||
|
getReportApprovalStateText,
|
||||||
|
getReportApprovalTypeText,
|
||||||
|
normalizePendingReportApprovalListParams,
|
||||||
|
normalizeReportApprovalPage,
|
||||||
|
resolveReportApprovalText
|
||||||
|
} from '../utils/reportApproval'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportApprovalPendingTable'
|
||||||
|
})
|
||||||
|
|
||||||
|
type TagType = TagProps['type']
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pass: [record: ReportApproval.PendingRecord]
|
||||||
|
reject: [record: ReportApproval.PendingRecord]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const STATE_OPTIONS: Array<{ label: string; value: ReportApproval.ReportState; tagType: TagType }> = [
|
||||||
|
{ label: '待审', value: '02', tagType: 'warning' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ReportApproval.PendingRecord>[]>([
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'reportType',
|
||||||
|
label: '报告类型',
|
||||||
|
width: 140,
|
||||||
|
enum: REPORT_APPROVAL_TYPE_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: ({ row }) => getReportApprovalTypeText(row.reportType),
|
||||||
|
search: {
|
||||||
|
el: 'select',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择报告类型'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'reportName',
|
||||||
|
label: '报告名称',
|
||||||
|
minWidth: 220,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => resolveReportApprovalText(row.reportName)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'state',
|
||||||
|
label: '当前状态',
|
||||||
|
width: 120,
|
||||||
|
enum: STATE_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: ({ row }) => (
|
||||||
|
<el-tag type={getReportApprovalStateTagType(row.state)} effect="light">
|
||||||
|
{getReportApprovalStateText(row.state)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'submitterName', label: '提交人', width: 140, render: ({ row }) => resolveReportApprovalText(row.submitterName) },
|
||||||
|
{ prop: 'submitTime', label: '提交时间', minWidth: 180, render: ({ row }) => resolveReportApprovalText(row.submitTime) },
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 180 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getTableList = async (params: ReportApproval.PendingListParams = {}) => {
|
||||||
|
const response = await listPendingReportApprovalsApi(normalizePendingReportApprovalListParams(params))
|
||||||
|
const pageData = normalizeReportApprovalPage<ReportApproval.PendingRecord>(
|
||||||
|
response as unknown as ResPage<ReportApproval.PendingRecord>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getReportApprovalErrorMessage(error, '待审批报告查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPendingReports = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePass = (row: ReportApproval.PendingRecord) => {
|
||||||
|
emit('pass', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReject = (row: ReportApproval.PendingRecord) => {
|
||||||
|
emit('reject', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshPendingReports
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-approval-pending-table {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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'",
|
||||||
|
'<ProTable',
|
||||||
|
'tableHeader',
|
||||||
|
'handlePass(row)',
|
||||||
|
'handleReject(row)'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(logTableFile, [
|
||||||
|
"name: 'ReportApprovalLogTable'",
|
||||||
|
'<ProTable',
|
||||||
|
'tableHeader',
|
||||||
|
'listReportApprovalLogsApi'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(pageFile, [
|
||||||
|
"name: 'ReportApprovalPage'",
|
||||||
|
'el-tabs',
|
||||||
|
'ReportApprovalPendingTable',
|
||||||
|
'ReportApprovalLogTable',
|
||||||
|
'ReportApprovalActionDialog',
|
||||||
|
'passReportApprovalApi',
|
||||||
|
'rejectReportApprovalApi'
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('reportApproval page contract passed')
|
||||||
108
frontend/src/views/aireport/reportApproval/index.vue
Normal file
108
frontend/src/views/aireport/reportApproval/index.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box report-approval-page">
|
||||||
|
<el-tabs v-model="activeTab" class="report-approval-page__tabs">
|
||||||
|
<el-tab-pane label="待审批报告" name="pending">
|
||||||
|
<ReportApprovalPendingTable ref="pendingTableRef" @pass="openPassDialog" @reject="openRejectDialog" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="审批日志" name="logs">
|
||||||
|
<ReportApprovalLogTable ref="logTableRef" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<ReportApprovalActionDialog
|
||||||
|
:visible="actionDialogVisible"
|
||||||
|
:mode="actionMode"
|
||||||
|
:record="currentRecord"
|
||||||
|
:saving="actionSaving"
|
||||||
|
@update:visible="handleDialogVisibleChange"
|
||||||
|
@submit="handleSubmitAction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { passReportApprovalApi, rejectReportApprovalApi } from '@/api/aireport/reportApproval'
|
||||||
|
import type { ReportApproval } from '@/api/aireport/reportApproval/interface'
|
||||||
|
import ReportApprovalActionDialog from './components/ReportApprovalActionDialog.vue'
|
||||||
|
import ReportApprovalLogTable from './components/ReportApprovalLogTable.vue'
|
||||||
|
import ReportApprovalPendingTable from './components/ReportApprovalPendingTable.vue'
|
||||||
|
import { getReportApprovalErrorMessage } from './utils/reportApproval'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportApprovalPage'
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeTab = ref<'pending' | 'logs'>('pending')
|
||||||
|
const pendingTableRef = ref<InstanceType<typeof ReportApprovalPendingTable>>()
|
||||||
|
const logTableRef = ref<InstanceType<typeof ReportApprovalLogTable>>()
|
||||||
|
const actionDialogVisible = ref(false)
|
||||||
|
const actionSaving = ref(false)
|
||||||
|
const actionMode = ref<'pass' | 'reject'>('pass')
|
||||||
|
const currentRecord = ref<ReportApproval.PendingRecord | null>(null)
|
||||||
|
|
||||||
|
const openPassDialog = (record: ReportApproval.PendingRecord) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
actionMode.value = 'pass'
|
||||||
|
actionDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openRejectDialog = (record: ReportApproval.PendingRecord) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
actionMode.value = 'reject'
|
||||||
|
actionDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTables = () => {
|
||||||
|
pendingTableRef.value?.refreshPendingReports()
|
||||||
|
logTableRef.value?.refreshLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDialogVisibleChange = (visible: boolean) => {
|
||||||
|
actionDialogVisible.value = visible
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
currentRecord.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAction = async (payload: ReportApproval.ApprovalActionRequest) => {
|
||||||
|
actionSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:审批通过和审批退回共用同一弹窗收集原因,但必须按当前操作调用不同后端接口。
|
||||||
|
if (actionMode.value === 'pass') {
|
||||||
|
await passReportApprovalApi(payload)
|
||||||
|
ElMessage.success('报告审批通过成功')
|
||||||
|
} else {
|
||||||
|
await rejectReportApprovalApi(payload)
|
||||||
|
ElMessage.success('报告审批退回成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTab.value = 'logs'
|
||||||
|
handleDialogVisibleChange(false)
|
||||||
|
refreshTables()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportApprovalErrorMessage(error, actionMode.value === 'pass' ? '报告审批通过失败' : '报告审批退回失败'))
|
||||||
|
} finally {
|
||||||
|
actionSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-approval-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-approval-page__tabs {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-approval-page__tabs :deep(.el-tabs__content) {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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<string, { text: string; tagType: TagType }> = {
|
||||||
|
'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<string, string> = {
|
||||||
|
REPORT_MODEL: '报告模板',
|
||||||
|
TEST_REPORT: '普测报告'
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPORT_APPROVAL_RESULT_MAP: Record<string, string> = {
|
||||||
|
'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 = <T>(response: ResultData<T> | T): T => {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeReportApprovalPage = <T>(
|
||||||
|
response: ResultData<ResPage<T>> | ResPage<T> | null | undefined
|
||||||
|
): ResPage<T> => {
|
||||||
|
const payload = unwrapReportApprovalPayload<ResPage<T> | null | undefined>(response as ResultData<ResPage<T>>)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
title="模板详情"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="report-model-detail-dialog">
|
||||||
|
<el-descriptions class="detail-descriptions" :column="2" border>
|
||||||
|
<el-descriptions-item label="模板名称">{{ detail?.name || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="模板状态">
|
||||||
|
<el-tag :type="getReportModelStateTagType(detail?.state)" effect="light">
|
||||||
|
{{ getReportModelStateText(detail?.state) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核人">{{ detail?.checkerName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核时间">{{ detail?.checkTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="编辑人">{{ detail?.editorName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ detail?.updateTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">{{ detail?.createBy || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ detail?.createTime || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="模板文件" :span="2">{{ resolveReportModelFileName(detail) || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="模板路径" :span="2">{{ detail?.path || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核意见" :span="2">{{ detail?.checkResult || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ReportModel } from '@/api/aireport/reportModel/interface'
|
||||||
|
import { getReportModelStateTagType, getReportModelStateText, resolveReportModelFileName } from '../utils/reportModel'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportModelDetailDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
detail: ReportModel.ReportModelRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-model-detail-dialog {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-model-detail-dialog :deep(.detail-descriptions .el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-model-detail-dialog :deep(.detail-descriptions .el-descriptions__label.el-descriptions__cell.is-bordered-label) {
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="mode === 'create' ? '新增模板' : '编辑模板'"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="96px">
|
||||||
|
<el-form-item label="模板名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="formModel.name"
|
||||||
|
maxlength="32"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入模板名称"
|
||||||
|
:disabled="mode === 'edit'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模板文件" :required="mode === 'create'">
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input :model-value="selectedFileName" readonly placeholder="请选择 Word 模板文件" />
|
||||||
|
<el-button type="primary" plain :icon="FolderOpened" :disabled="saving" @click="openFilePicker">
|
||||||
|
选择文件
|
||||||
|
</el-button>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-file-input"
|
||||||
|
type="file"
|
||||||
|
accept=".doc,.docx"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="file-select-tip">
|
||||||
|
{{
|
||||||
|
mode === 'create'
|
||||||
|
? '新增模板时必须上传 Word 模板文件,支持 .doc/.docx'
|
||||||
|
: '编辑时不上传则保留原文件,新上传文件仅支持 .doc/.docx'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import type { ReportModel } from '@/api/aireport/reportModel/interface'
|
||||||
|
import { resolveReportModelFileName, validateReportModelTemplateFile } from '../utils/reportModel'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportModelFormDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
mode: 'create' | 'edit'
|
||||||
|
record: ReportModel.ReportModelRecord | null
|
||||||
|
saving: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: { request: ReportModel.ReportModelSaveRequest; file: File | null }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const formModel = reactive({
|
||||||
|
id: '',
|
||||||
|
name: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedFileName = computed(() => selectedFile.value?.name || resolveReportModelFileName(props.record))
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
selectedFile.value = null
|
||||||
|
formModel.id = ''
|
||||||
|
formModel.name = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillForm = () => {
|
||||||
|
formModel.id = props.record?.id || ''
|
||||||
|
formModel.name = props.record?.name || ''
|
||||||
|
selectedFile.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.mode, props.record?.id, props.record?.name, props.record?.path, props.record?.fileName, props.record?.file_name],
|
||||||
|
([visible]) => {
|
||||||
|
if (visible) fillForm()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const openFilePicker = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0] || null
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const validationMessage = validateReportModelTemplateFile(file)
|
||||||
|
if (validationMessage) {
|
||||||
|
ElMessage.warning(validationMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
if (props.mode === 'create' && !selectedFile.value) {
|
||||||
|
ElMessage.warning('新增模板时必须上传 Word 模板文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode === 'edit' && !formModel.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法保存编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
request: {
|
||||||
|
id: formModel.id || undefined,
|
||||||
|
name: formModel.name.trim(),
|
||||||
|
// 关键业务节点:新增/编辑都需要把模板文件框当前展示的文件名同步给后端 request.fileName。
|
||||||
|
fileName: selectedFileName.value || undefined
|
||||||
|
},
|
||||||
|
file: selectedFile.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.file-select-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="previewTitle"
|
||||||
|
width="960px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="clearPreview"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="report-model-preview-dialog">
|
||||||
|
<div v-if="activeErrorMessage" class="preview-error-wrapper">
|
||||||
|
<el-alert :title="activeErrorMessage" type="warning" :closable="false" show-icon />
|
||||||
|
</div>
|
||||||
|
<div v-else ref="previewContainer" class="preview-container"></div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { renderAsync } from 'docx-preview'
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportModelPreviewDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
fileBlob: Blob | null
|
||||||
|
fileName: string
|
||||||
|
errorMessage: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const previewContainer = ref<HTMLDivElement | null>(null)
|
||||||
|
const renderErrorMessage = ref('')
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const previewTitle = computed(() => (props.fileName ? `模板预览 - ${props.fileName}` : '模板预览'))
|
||||||
|
const activeErrorMessage = computed(() => props.errorMessage || renderErrorMessage.value)
|
||||||
|
|
||||||
|
const clearPreview = () => {
|
||||||
|
renderErrorMessage.value = ''
|
||||||
|
if (previewContainer.value) {
|
||||||
|
previewContainer.value.innerHTML = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPreview = async () => {
|
||||||
|
if (!props.visible || props.loading || !props.fileBlob || props.errorMessage || !previewContainer.value) return
|
||||||
|
|
||||||
|
clearPreview()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:仅对可预览的 docx 内容执行页面内渲染,渲染失败时保留明确提示,避免弹窗空白无反馈。
|
||||||
|
await renderAsync(props.fileBlob, previewContainer.value, undefined, {
|
||||||
|
className: 'report-model-docx-preview',
|
||||||
|
ignoreWidth: false,
|
||||||
|
ignoreHeight: true,
|
||||||
|
renderHeaders: true,
|
||||||
|
renderFooters: true,
|
||||||
|
useBase64URL: true
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
renderErrorMessage.value = error instanceof Error ? error.message : '模板预览失败,请稍后重试'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.loading, props.fileBlob, props.errorMessage],
|
||||||
|
async () => {
|
||||||
|
if (!props.visible) {
|
||||||
|
clearPreview()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderErrorMessage.value = ''
|
||||||
|
await nextTick()
|
||||||
|
await renderPreview()
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-model-preview-dialog {
|
||||||
|
min-height: 520px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
min-height: 520px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container:deep(.report-model-docx-preview-wrapper) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container:deep(section.docx) {
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
box-shadow: 0 8px 24px rgb(15 23 42 / 10%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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', () => /<ProTable[\s\S]*:request-api="getTableList"/.test(pageSource)],
|
||||||
|
['reportModel page has create and batch delete header actions', () => /<template\s+#tableHeader="scope">[\s\S]*openCreateDialog[\s\S]*handleBatchDelete\(scope\.selectedListIds\)/.test(pageSource)],
|
||||||
|
['reportModel page uses form detail and preview dialogs', () => /<ReportModelFormDialog[\s\S]*<ReportModelDetailDialog[\s\S]*<ReportModelPreviewDialog/.test(pageSource)],
|
||||||
|
['reportModel row actions include detail edit download submit audit and delete', () => /<template\s+#operation="\{\s*row\s*\}">[\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')
|
||||||
400
frontend/src/views/aireport/reportModel/index.vue
Normal file
400
frontend/src/views/aireport/reportModel/index.vue
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box report-model-page">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #tableHeader="scope">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleBatchDelete(scope.selectedListIds)"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button link type="primary" :icon="View" @click="openDetailDialog(row)">详情</el-button>
|
||||||
|
<el-button link type="primary" :icon="Edit" :disabled="!isDraftReportModel(row)" @click="openEditDialog(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" :icon="Download" :disabled="!row.id" @click="handleDownload(row)">下载</el-button>
|
||||||
|
<el-button link type="primary" :icon="Upload" :disabled="!isDraftReportModel(row)" @click="openAuditDialog(row)">
|
||||||
|
提交审核
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
|
||||||
|
<ReportModelFormDialog
|
||||||
|
v-model:visible="formDialogVisible"
|
||||||
|
:mode="formMode"
|
||||||
|
:record="currentRecord"
|
||||||
|
:saving="formSaving"
|
||||||
|
@submit="handleSaveReportModel"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReportModelDetailDialog v-model:visible="detailDialogVisible" :loading="detailLoading" :detail="detailRecord" />
|
||||||
|
|
||||||
|
<ReportModelPreviewDialog
|
||||||
|
v-model:visible="previewDialogVisible"
|
||||||
|
:loading="previewLoading"
|
||||||
|
:file-name="previewFileName"
|
||||||
|
:file-blob="previewFileBlob"
|
||||||
|
:error-message="previewErrorMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { Delete, Download, Edit, Plus, Upload, View } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox, type TagProps } from 'element-plus'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import type { ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import {
|
||||||
|
createReportModelApi,
|
||||||
|
deleteReportModelsApi,
|
||||||
|
downloadReportModelApi,
|
||||||
|
getReportModelDetailApi,
|
||||||
|
listReportModelsApi,
|
||||||
|
submitReportModelAuditApi,
|
||||||
|
updateReportModelApi
|
||||||
|
} from '@/api/aireport/reportModel'
|
||||||
|
import type { ReportModel } from '@/api/aireport/reportModel/interface'
|
||||||
|
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
|
||||||
|
import ReportModelDetailDialog from './components/ReportModelDetailDialog.vue'
|
||||||
|
import ReportModelFormDialog from './components/ReportModelFormDialog.vue'
|
||||||
|
import ReportModelPreviewDialog from './components/ReportModelPreviewDialog.vue'
|
||||||
|
import {
|
||||||
|
canPreviewReportModelFile,
|
||||||
|
getReportModelErrorMessage,
|
||||||
|
getReportModelStateTagType,
|
||||||
|
getReportModelStateText,
|
||||||
|
isDraftReportModel,
|
||||||
|
normalizeReportModelListParams,
|
||||||
|
normalizeReportModelPage,
|
||||||
|
resolveReportModelDownloadFileName,
|
||||||
|
resolveReportModelFileName,
|
||||||
|
resolveReportModelText,
|
||||||
|
unwrapApiPayload
|
||||||
|
} from './utils/reportModel'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportModelPage'
|
||||||
|
})
|
||||||
|
|
||||||
|
type TagType = TagProps['type']
|
||||||
|
|
||||||
|
const STATE_OPTIONS: Array<{ label: string; value: ReportModel.ReportModelState; tagType: TagType }> = [
|
||||||
|
{ label: '编制', value: '01', tagType: 'info' },
|
||||||
|
{ label: '审核中', value: '02', tagType: 'warning' },
|
||||||
|
{ label: '已发布', value: '03', tagType: 'success' },
|
||||||
|
{ label: '已删除', value: '04', tagType: 'danger' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const previewDialogVisible = ref(false)
|
||||||
|
const formSaving = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const auditSaving = ref(false)
|
||||||
|
const previewLoading = ref(false)
|
||||||
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
|
const currentRecord = ref<ReportModel.ReportModelRecord | null>(null)
|
||||||
|
const detailRecord = ref<ReportModel.ReportModelRecord | null>(null)
|
||||||
|
const previewFileBlob = ref<Blob | null>(null)
|
||||||
|
const previewFileName = ref('')
|
||||||
|
const previewErrorMessage = ref('')
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ReportModel.ReportModelRecord>[]>([
|
||||||
|
{ type: 'selection', fixed: 'left', width: 70 },
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
minWidth: 180,
|
||||||
|
search: {
|
||||||
|
el: 'input',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请输入模板名称'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'path',
|
||||||
|
label: '模版文件',
|
||||||
|
minWidth: 260,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) =>
|
||||||
|
resolveReportModelFileName(row) ? (
|
||||||
|
<el-button link type="primary" onClick={() => openPreviewDialog(row)}>
|
||||||
|
{resolveReportModelFileName(row)}
|
||||||
|
</el-button>
|
||||||
|
) : (
|
||||||
|
resolveReportModelText(resolveReportModelFileName(row))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'state',
|
||||||
|
label: '模板状态',
|
||||||
|
width: 120,
|
||||||
|
enum: STATE_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: scope => (
|
||||||
|
<el-tag type={getReportModelStateTagType(scope.row.state)} effect="light">
|
||||||
|
{getReportModelStateText(scope.row.state)}
|
||||||
|
</el-tag>
|
||||||
|
),
|
||||||
|
search: {
|
||||||
|
el: 'select',
|
||||||
|
order: 2,
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择模板状态'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'timeRange',
|
||||||
|
label: '更新时间',
|
||||||
|
minWidth: 220,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'date-picker',
|
||||||
|
order: 3,
|
||||||
|
key: 'timeRange',
|
||||||
|
props: {
|
||||||
|
type: 'datetimerange',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
startPlaceholder: '开始更新时间',
|
||||||
|
endPlaceholder: '结束更新时间'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prop: 'editorName', label: '编辑人', width: 120, render: ({ row }) => resolveReportModelText(row.editorName) },
|
||||||
|
{ prop: 'updateTime', label: '更新时间', minWidth: 170, render: ({ row }) => resolveReportModelText(row.updateTime) },
|
||||||
|
{ prop: 'checkerName', label: '审核人', width: 120 },
|
||||||
|
{ prop: 'checkTime', label: '审核时间', minWidth: 170, render: ({ row }) => resolveReportModelText(row.checkTime) },
|
||||||
|
{ prop: 'checkResult', label: '审核意见', minWidth: 220, showOverflowTooltip: true, render: ({ row }) => resolveReportModelText(row.checkResult) },
|
||||||
|
{ prop: 'createBy', label: '创建人', width: 120, isShow: false },
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 380 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getTableList = async (params: ReportModel.ReportModelListParams = {}) => {
|
||||||
|
const response = await listReportModelsApi(normalizeReportModelListParams(params))
|
||||||
|
const pageData = normalizeReportModelPage<ReportModel.ReportModelRecord>(response as unknown as ResPage<ReportModel.ReportModelRecord>)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshReportModels = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getReportModelErrorMessage(error, '模板列表查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPreviewState = () => {
|
||||||
|
previewLoading.value = false
|
||||||
|
previewFileBlob.value = null
|
||||||
|
previewFileName.value = ''
|
||||||
|
previewErrorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
formMode.value = 'create'
|
||||||
|
currentRecord.value = null
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditDialog = (row: ReportModel.ReportModelRecord) => {
|
||||||
|
formMode.value = 'edit'
|
||||||
|
currentRecord.value = row
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetailDialog = async (row: ReportModel.ReportModelRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法查看详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRecord.value = null
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:详情字段单独走 GET /{id},列表页不假设审核意见和模板路径一定完整返回。
|
||||||
|
const response = await getReportModelDetailApi(row.id)
|
||||||
|
detailRecord.value = unwrapApiPayload<ReportModel.ReportModelRecord>(response)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportModelErrorMessage(error, '模板详情查询失败'))
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPreviewDialog = async (row: ReportModel.ReportModelRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法预览')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPreviewState()
|
||||||
|
previewDialogVisible.value = true
|
||||||
|
previewLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:页面内预览复用现有下载接口,优先从响应头解析真实文件名,再决定是否可进入 docx 渲染。
|
||||||
|
const response = await downloadReportModelApi(row.id)
|
||||||
|
const fallbackExtension = row.path?.toLowerCase().endsWith('.doc') ? '.doc' : '.docx'
|
||||||
|
const fileName = resolveReportModelDownloadFileName(response.headers, row.name || 'report-model', fallbackExtension)
|
||||||
|
|
||||||
|
previewFileName.value = fileName
|
||||||
|
|
||||||
|
if (!canPreviewReportModelFile(fileName) && !canPreviewReportModelFile(resolveReportModelFileName(row))) {
|
||||||
|
previewErrorMessage.value = '当前仅支持 .docx 模板页面内预览,请下载 .doc 文件后查看'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewFileBlob.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
previewErrorMessage.value = getReportModelErrorMessage(error, '模板预览加载失败')
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAuditDialog = async (row: ReportModel.ReportModelRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法提交审核')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认提交模板“${row.name || '-'}”进入审核中状态吗?`, '提交审核', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:提交审核只允许编制态记录调用,前端在按钮态和提交前都保持同一约束。
|
||||||
|
await submitReportModelAuditApi({ id: row.id })
|
||||||
|
ElMessage.success('模板提交审核成功')
|
||||||
|
refreshReportModels()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportModelErrorMessage(error, '模板提交审核失败'))
|
||||||
|
} finally {
|
||||||
|
auditSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveReportModel = async ({
|
||||||
|
request,
|
||||||
|
file
|
||||||
|
}: {
|
||||||
|
request: ReportModel.ReportModelSaveRequest
|
||||||
|
file: File | null
|
||||||
|
}) => {
|
||||||
|
formSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (formMode.value === 'create') {
|
||||||
|
if (!file) {
|
||||||
|
ElMessage.warning('新增模板时必须上传 Word 模板文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createReportModelApi(request, file)
|
||||||
|
ElMessage.success('模板新增成功')
|
||||||
|
} else {
|
||||||
|
await updateReportModelApi(request, file)
|
||||||
|
ElMessage.success('模板编辑成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
formDialogVisible.value = false
|
||||||
|
refreshReportModels()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportModelErrorMessage(error, '模板保存失败'))
|
||||||
|
} finally {
|
||||||
|
formSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteReportModels = async (ids: string[], successMessage: string) => {
|
||||||
|
if (!ids.length) {
|
||||||
|
ElMessage.warning('请选择要删除的模板')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('删除后模板将不可见,是否继续?', '删除模板', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteReportModelsApi(ids)
|
||||||
|
ElMessage.success(successMessage)
|
||||||
|
refreshReportModels()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportModelErrorMessage(error, '模板删除失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row: ReportModel.ReportModelRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteReportModels([row.id], '模板删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBatchDelete = async (ids: string[]) => {
|
||||||
|
await deleteReportModels(ids, '模板批量删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = async (row: ReportModel.ReportModelRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前模板缺少 ID,无法下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await useDownloadWithServerFileName(() => downloadReportModelApi(row.id!), row.name || 'report-model', undefined, false, '.docx')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-model-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
frontend/src/views/aireport/reportModel/utils/reportModel.ts
Normal file
125
frontend/src/views/aireport/reportModel/utils/reportModel.ts
Normal file
@@ -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<string, unknown>
|
||||||
|
|
||||||
|
const REPORT_MODEL_STATE_MAP: Record<string, { text: string; tagType: TagType }> = {
|
||||||
|
'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 = <T>(response: ResultData<T> | T): T => {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeReportModelPage = <T>(
|
||||||
|
response: ResultData<ResPage<T>> | ResPage<T> | null | undefined
|
||||||
|
): ResPage<T> => {
|
||||||
|
const payload = unwrapApiPayload<ResPage<T> | null | undefined>(response as ResultData<ResPage<T>>)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visibleProxy" title="检测设备详情" width="720px" append-to-body destroy-on-close>
|
||||||
|
<div v-loading="loading" class="test-device-detail-dialog">
|
||||||
|
<el-descriptions class="detail-descriptions" :column="2" border>
|
||||||
|
<el-descriptions-item label="设备型号">{{ resolveTestDeviceText(detail?.typeName) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备编号">{{ resolveTestDeviceText(detail?.no) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备状态">
|
||||||
|
<el-tag :type="getTestDeviceStateTagType(detail?.state)" effect="light">
|
||||||
|
{{ getTestDeviceStateText(detail?.state) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="校准有效期">
|
||||||
|
{{ resolveTestDeviceText(detail?.validityPeriod) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="校准报告" :span="2">
|
||||||
|
{{ resolveTestDeviceText(detail?.validityReportName || detail?.validityReport) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">
|
||||||
|
{{ resolveTestDeviceText(detail?.createByName || detail?.createBy) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ resolveTestDeviceText(detail?.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新人">
|
||||||
|
{{ resolveTestDeviceText(detail?.updateByName || detail?.updateBy) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ resolveTestDeviceText(detail?.updateTime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { TestDevice } from '@/api/aireport/testDevice/interface'
|
||||||
|
import { getTestDeviceStateTagType, getTestDeviceStateText, resolveTestDeviceText } from '../utils/testDevice'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TestDeviceDetailDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
detail: TestDevice.TestDeviceRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.test-device-detail-dialog {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-device-detail-dialog :deep(.detail-descriptions .el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-device-detail-dialog :deep(.detail-descriptions .el-descriptions__label.el-descriptions__cell.is-bordered-label) {
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="mode === 'create' ? '新增检测设备' : '编辑检测设备'"
|
||||||
|
width="640px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="108px">
|
||||||
|
<el-form-item label="设备型号" prop="type">
|
||||||
|
<el-select v-model="formModel.type" filterable clearable placeholder="请选择设备型号">
|
||||||
|
<el-option
|
||||||
|
v-for="option in deviceTypeSelectOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备编号" prop="no">
|
||||||
|
<el-input v-model="formModel.no" maxlength="64" clearable placeholder="请输入设备编号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="校准有效期" prop="validityPeriod">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formModel.validityPeriod"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择校准有效期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="校准报告" :required="mode === 'create'">
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input :model-value="selectedFileName" readonly placeholder="请选择 PDF 校准报告" />
|
||||||
|
<el-button type="primary" plain :icon="FolderOpened" :disabled="saving" @click="openFilePicker">
|
||||||
|
选择文件
|
||||||
|
</el-button>
|
||||||
|
<input ref="fileInputRef" class="hidden-file-input" type="file" accept=".pdf" @change="handleFileChange" />
|
||||||
|
</div>
|
||||||
|
<div class="file-select-tip">
|
||||||
|
{{
|
||||||
|
mode === 'create'
|
||||||
|
? `新增检测设备时必须上传 PDF 校准报告,文件大小不能超过 ${TEST_DEVICE_REPORT_MAX_SIZE_TEXT}`
|
||||||
|
: `编辑时不上传则保留原报告,新上传文件需为 PDF 且不能超过 ${TEST_DEVICE_REPORT_MAX_SIZE_TEXT}`
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备状态" prop="state">
|
||||||
|
<el-select v-model="formModel.state" placeholder="请选择设备状态">
|
||||||
|
<el-option
|
||||||
|
v-for="option in TEST_DEVICE_STATE_OPTIONS"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import type { TestDevice } from '@/api/aireport/testDevice/interface'
|
||||||
|
import {
|
||||||
|
TEST_DEVICE_REPORT_MAX_SIZE_TEXT,
|
||||||
|
TEST_DEVICE_STATE_OPTIONS,
|
||||||
|
validateTestDeviceReportFile
|
||||||
|
} from '../utils/testDevice'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TestDeviceFormDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
mode: 'create' | 'edit'
|
||||||
|
record: TestDevice.TestDeviceRecord | null
|
||||||
|
saving: boolean
|
||||||
|
deviceTypeOptions: Array<{ label: string; value: string }>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: { request: TestDevice.TestDeviceSaveRequest; reportFile: File | null }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const formModel = reactive({
|
||||||
|
id: '',
|
||||||
|
type: '',
|
||||||
|
no: '',
|
||||||
|
validityPeriod: '',
|
||||||
|
state: '01'
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
type: [{ required: true, message: '请选择设备型号', trigger: 'change' }],
|
||||||
|
no: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||||
|
validityPeriod: [{ required: true, message: '请选择校准有效期', trigger: 'change' }],
|
||||||
|
state: [{ required: true, message: '请选择设备状态', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
const selectedFileName = computed(() => selectedFile.value?.name || props.record?.validityReportName || '')
|
||||||
|
const deviceTypeSelectOptions = computed(() => {
|
||||||
|
if (!formModel.type || props.deviceTypeOptions.some(option => option.value === formModel.type)) {
|
||||||
|
return props.deviceTypeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...props.deviceTypeOptions,
|
||||||
|
{
|
||||||
|
label: props.record?.typeName || formModel.type,
|
||||||
|
value: formModel.type
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
selectedFile.value = null
|
||||||
|
formModel.id = ''
|
||||||
|
formModel.type = ''
|
||||||
|
formModel.no = ''
|
||||||
|
formModel.validityPeriod = ''
|
||||||
|
formModel.state = '01'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillForm = () => {
|
||||||
|
formModel.id = props.record?.id || ''
|
||||||
|
formModel.type = props.record?.type || ''
|
||||||
|
formModel.no = props.record?.no || ''
|
||||||
|
formModel.validityPeriod = props.record?.validityPeriod || ''
|
||||||
|
formModel.state = props.record?.state || '01'
|
||||||
|
selectedFile.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.mode, props.record?.id],
|
||||||
|
([visible]) => {
|
||||||
|
if (visible) fillForm()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const openFilePicker = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const reportFile = input.files?.[0] || null
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
if (!reportFile) return
|
||||||
|
|
||||||
|
const validationMessage = validateTestDeviceReportFile(reportFile)
|
||||||
|
if (validationMessage) {
|
||||||
|
ElMessage.warning(validationMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFile.value = reportFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
if (props.mode === 'create' && !selectedFile.value) {
|
||||||
|
ElMessage.warning('新增检测设备时必须上传 PDF 校准报告')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode === 'edit' && !formModel.id) {
|
||||||
|
ElMessage.warning('当前检测设备缺少 ID,无法保存编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
request: {
|
||||||
|
id: formModel.id || undefined,
|
||||||
|
type: formModel.type.trim(),
|
||||||
|
no: formModel.no.trim(),
|
||||||
|
validityPeriod: formModel.validityPeriod,
|
||||||
|
state: formModel.state
|
||||||
|
},
|
||||||
|
reportFile: selectedFile.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.file-select-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visibleProxy" title="导入检测设备" width="680px" append-to-body destroy-on-close @closed="resetImport">
|
||||||
|
<div class="test-device-import-dialog">
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input :model-value="selectedFileName" readonly placeholder="请选择检测设备 Excel 文件" />
|
||||||
|
<el-button type="primary" plain :icon="FolderOpened" :disabled="importing" @click="openFilePicker">
|
||||||
|
选择Excel
|
||||||
|
</el-button>
|
||||||
|
<input ref="fileInputRef" class="hidden-file-input" type="file" accept=".xlsx,.xls" @change="handleFileChange" />
|
||||||
|
</div>
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input :model-value="selectedZipName" readonly placeholder="请选择校准报告 ZIP" />
|
||||||
|
<el-button type="primary" plain :icon="FolderOpened" :disabled="importing" @click="openZipPicker">
|
||||||
|
选择ZIP
|
||||||
|
</el-button>
|
||||||
|
<input ref="zipInputRef" class="hidden-file-input" type="file" accept=".zip" @change="handleZipChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="result"
|
||||||
|
class="import-result"
|
||||||
|
:title="`导入完成:成功 ${result.successCount || 0} 条,失败 ${result.failCount || 0} 条`"
|
||||||
|
:type="result.failCount ? 'warning' : 'success'"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table v-if="result?.failReasons?.length" class="fail-reason-table" :data="failReasonRows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column prop="reason" label="失败原因" min-width="260" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="importing" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="importing" @click="submitImport">开始导入</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type { TestDevice } from '@/api/aireport/testDevice/interface'
|
||||||
|
import { validateTestDeviceImportExcelFile, validateTestDeviceImportZipFile } from '../utils/testDevice'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TestDeviceImportDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
importing: boolean
|
||||||
|
result: TestDevice.TestDeviceImportResult | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: { file: File; reportZip: File }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const zipInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const reportZip = ref<File | null>(null)
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
const selectedFileName = computed(() => selectedFile.value?.name || '')
|
||||||
|
const selectedZipName = computed(() => reportZip.value?.name || '')
|
||||||
|
const failReasonRows = computed(() => (props.result?.failReasons || []).map(reason => ({ reason })))
|
||||||
|
|
||||||
|
const openFilePicker = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openZipPicker = () => {
|
||||||
|
zipInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0] || null
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const validationMessage = validateTestDeviceImportExcelFile(file)
|
||||||
|
if (validationMessage) {
|
||||||
|
ElMessage.warning(validationMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZipChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0] || null
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const validationMessage = validateTestDeviceImportZipFile(file)
|
||||||
|
if (validationMessage) {
|
||||||
|
ElMessage.warning(validationMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reportZip.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetImport = () => {
|
||||||
|
selectedFile.value = null
|
||||||
|
reportZip.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitImport = () => {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
ElMessage.warning('请选择检测设备 Excel 文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!reportZip.value) {
|
||||||
|
ElMessage.warning('请选择校准报告 ZIP')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
file: selectedFile.value,
|
||||||
|
reportZip: reportZip.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.test-device-import-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="previewTitle"
|
||||||
|
width="960px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="clearPreview"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="test-device-report-preview-dialog">
|
||||||
|
<div v-if="errorMessage" class="preview-error-wrapper">
|
||||||
|
<el-alert :title="errorMessage" type="warning" :closable="false" show-icon />
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
v-else-if="previewUrl"
|
||||||
|
:src="previewUrl"
|
||||||
|
class="preview-frame"
|
||||||
|
frameborder="0"
|
||||||
|
title="校准报告预览"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-empty-wrapper">
|
||||||
|
<el-empty description="暂无可预览的校准报告" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TestDeviceReportPreviewDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
fileBlob: Blob | null
|
||||||
|
fileName: string
|
||||||
|
errorMessage: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const previewUrl = ref('')
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const previewTitle = computed(() => (props.fileName ? `校准报告预览 - ${props.fileName}` : '校准报告预览'))
|
||||||
|
|
||||||
|
const revokePreviewUrl = () => {
|
||||||
|
if (!previewUrl.value) return
|
||||||
|
|
||||||
|
URL.revokeObjectURL(previewUrl.value)
|
||||||
|
previewUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPreview = () => {
|
||||||
|
revokePreviewUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.fileBlob],
|
||||||
|
() => {
|
||||||
|
revokePreviewUrl()
|
||||||
|
|
||||||
|
if (!props.visible || !props.fileBlob) return
|
||||||
|
|
||||||
|
// 关键业务节点:PDF 预览必须为当前 blob 重新创建对象地址,并在弹窗关闭或文件切换时立即释放,避免旧报告串页或内存泄漏。
|
||||||
|
previewUrl.value = URL.createObjectURL(props.fileBlob)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.test-device-report-preview-dialog {
|
||||||
|
min-height: 520px;
|
||||||
|
height: 70vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error-wrapper,
|
||||||
|
.preview-empty-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 520px;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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 apiDir = path.resolve(srcDir, 'api/aireport/testDevice')
|
||||||
|
|
||||||
|
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(path.join(apiDir, 'index.ts'), [
|
||||||
|
"const TEST_DEVICE_BASE_URL = '/api/test-device'",
|
||||||
|
'listTestDevicesApi',
|
||||||
|
'createTestDeviceApi',
|
||||||
|
'updateTestDeviceApi',
|
||||||
|
'deleteTestDevicesApi',
|
||||||
|
'getTestDeviceDetailApi',
|
||||||
|
'downloadTestDeviceImportTemplateApi',
|
||||||
|
'importTestDevicesApi',
|
||||||
|
'exportTestDevicesApi',
|
||||||
|
'downloadTestDeviceReportApi',
|
||||||
|
'previewTestDeviceReportApi',
|
||||||
|
"return http.get(`${TEST_DEVICE_BASE_URL}/${id}/report`, undefined, { responseType: 'blob' })",
|
||||||
|
"formData.append('request'",
|
||||||
|
"formData.append('reportFile'",
|
||||||
|
"formData.append('reportZip'"
|
||||||
|
])
|
||||||
|
|
||||||
|
const apiIndexContent = read(path.join(apiDir, 'index.ts'))
|
||||||
|
if (apiIndexContent.includes('/api/test-report/')) {
|
||||||
|
throw new Error('api/aireport/testDevice/index.ts should not reference /api/test-report preview endpoint')
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(apiDir, 'interface/index.ts'), [
|
||||||
|
'export namespace TestDevice',
|
||||||
|
'TestDeviceRecord',
|
||||||
|
'TestDeviceListParams',
|
||||||
|
'TestDeviceSaveRequest',
|
||||||
|
'TestDeviceImportResult',
|
||||||
|
"export type TestDeviceState = '01' | '02' | (string & {})"
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'utils/testDevice.ts'), [
|
||||||
|
'normalizeTestDeviceListParams',
|
||||||
|
'normalizeTestDevicePage',
|
||||||
|
'getTestDeviceStateText',
|
||||||
|
'getTestDeviceStateTagType',
|
||||||
|
'unwrapTestDevicePayload'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/TestDeviceFormDialog.vue'), [
|
||||||
|
"name: 'TestDeviceFormDialog'",
|
||||||
|
'reportFile',
|
||||||
|
'validityPeriod',
|
||||||
|
'新增检测设备时必须上传 PDF 校准报告'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/TestDeviceImportDialog.vue'), [
|
||||||
|
"name: 'TestDeviceImportDialog'",
|
||||||
|
'reportZip',
|
||||||
|
'failReasons',
|
||||||
|
'请选择校准报告 ZIP'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/TestDeviceDetailDialog.vue'), [
|
||||||
|
"name: 'TestDeviceDetailDialog'",
|
||||||
|
'设备型号',
|
||||||
|
'校准报告',
|
||||||
|
"detail?.updateByName || detail?.updateBy",
|
||||||
|
':span="2"',
|
||||||
|
'table-layout: fixed',
|
||||||
|
'width: 112px'
|
||||||
|
])
|
||||||
|
|
||||||
|
const detailDialogContent = read(path.join(pageDir, 'components/TestDeviceDetailDialog.vue'))
|
||||||
|
if (detailDialogContent.includes('设备型号ID')) {
|
||||||
|
throw new Error('views/aireport/testDevice/components/TestDeviceDetailDialog.vue should not display 设备型号ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'index.vue'), [
|
||||||
|
"name: 'TestDevicePage'",
|
||||||
|
'<ProTable',
|
||||||
|
'TestDeviceFormDialog',
|
||||||
|
'TestDeviceDetailDialog',
|
||||||
|
'TestDeviceImportDialog',
|
||||||
|
'TestDeviceReportPreviewDialog',
|
||||||
|
'handleDownloadReport',
|
||||||
|
'openPreviewDialog',
|
||||||
|
'previewTestDeviceReportApi',
|
||||||
|
'handleDownloadTemplate',
|
||||||
|
'handleImportTestDevices',
|
||||||
|
'type="primary" :icon="Plus"',
|
||||||
|
'type="danger"',
|
||||||
|
'type="primary" plain'
|
||||||
|
])
|
||||||
|
|
||||||
|
const pageSource = read(path.join(pageDir, 'index.vue'))
|
||||||
|
if (!/prop:\s*'timeRange'[\s\S]*label:\s*'更新时间'[\s\S]*startPlaceholder:\s*'开始更新时间'[\s\S]*endPlaceholder:\s*'结束更新时间'/.test(pageSource)) {
|
||||||
|
throw new Error('views/aireport/testDevice/index.vue should use 更新时间 as the search time range label')
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/TestDeviceReportPreviewDialog.vue'), [
|
||||||
|
"name: 'TestDeviceReportPreviewDialog'",
|
||||||
|
'fileBlob',
|
||||||
|
'fileName',
|
||||||
|
'errorMessage',
|
||||||
|
'URL.createObjectURL'
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('testDevice page contract passed')
|
||||||
449
frontend/src/views/aireport/testDevice/index.vue
Normal file
449
frontend/src/views/aireport/testDevice/index.vue
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box test-device-page">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #tableHeader="scope">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleBatchDelete(scope.selectedListIds)"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Download" @click="handleExport">导出</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Document" @click="handleDownloadTemplate">下载模板</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Upload" @click="openImportDialog">导入</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button link type="primary" :icon="View" @click="openDetailDialog(row)">详情</el-button>
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button link type="primary" :icon="Download" :disabled="!row.id" @click="handleDownloadReport(row)">
|
||||||
|
报告
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
|
||||||
|
<TestDeviceFormDialog
|
||||||
|
v-model:visible="formDialogVisible"
|
||||||
|
:mode="formMode"
|
||||||
|
:record="currentRecord"
|
||||||
|
:saving="formSaving"
|
||||||
|
:device-type-options="deviceTypeOptions"
|
||||||
|
@submit="handleSaveTestDevice"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestDeviceDetailDialog
|
||||||
|
v-model:visible="detailDialogVisible"
|
||||||
|
:loading="detailLoading"
|
||||||
|
:detail="detailRecord"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestDeviceImportDialog
|
||||||
|
v-model:visible="importDialogVisible"
|
||||||
|
:importing="importing"
|
||||||
|
:result="importResult"
|
||||||
|
@submit="handleImportTestDevices"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestDeviceReportPreviewDialog
|
||||||
|
v-model:visible="previewDialogVisible"
|
||||||
|
:loading="previewLoading"
|
||||||
|
:file-name="previewFileName"
|
||||||
|
:file-blob="previewFileBlob"
|
||||||
|
:error-message="previewErrorMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { Delete, Document, Download, Edit, Plus, Upload, View } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import type { ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import {
|
||||||
|
createTestDeviceApi,
|
||||||
|
deleteTestDevicesApi,
|
||||||
|
downloadTestDeviceImportTemplateApi,
|
||||||
|
downloadTestDeviceReportApi,
|
||||||
|
exportTestDevicesApi,
|
||||||
|
getTestDeviceDetailApi,
|
||||||
|
importTestDevicesApi,
|
||||||
|
listTestDevicesApi,
|
||||||
|
previewTestDeviceReportApi,
|
||||||
|
updateTestDeviceApi
|
||||||
|
} from '@/api/aireport/testDevice'
|
||||||
|
import type { TestDevice } from '@/api/aireport/testDevice/interface'
|
||||||
|
import { listDeviceTypesApi } from '@/api/tools/mmsmapping'
|
||||||
|
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
|
||||||
|
import TestDeviceDetailDialog from './components/TestDeviceDetailDialog.vue'
|
||||||
|
import TestDeviceFormDialog from './components/TestDeviceFormDialog.vue'
|
||||||
|
import TestDeviceImportDialog from './components/TestDeviceImportDialog.vue'
|
||||||
|
import TestDeviceReportPreviewDialog from './components/TestDeviceReportPreviewDialog.vue'
|
||||||
|
import {
|
||||||
|
TEST_DEVICE_STATE_OPTIONS,
|
||||||
|
getTestDeviceErrorMessage,
|
||||||
|
getTestDeviceImportResultMessage,
|
||||||
|
getTestDeviceStateTagType,
|
||||||
|
getTestDeviceStateText,
|
||||||
|
normalizeTestDeviceListParams,
|
||||||
|
normalizeTestDevicePage,
|
||||||
|
resolveTestDeviceText,
|
||||||
|
unwrapTestDevicePayload
|
||||||
|
} from './utils/testDevice'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TestDevicePage'
|
||||||
|
})
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const previewDialogVisible = ref(false)
|
||||||
|
const formSaving = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const importing = ref(false)
|
||||||
|
const previewLoading = ref(false)
|
||||||
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
|
const currentRecord = ref<TestDevice.TestDeviceRecord | null>(null)
|
||||||
|
const detailRecord = ref<TestDevice.TestDeviceRecord | null>(null)
|
||||||
|
const importResult = ref<TestDevice.TestDeviceImportResult | null>(null)
|
||||||
|
const deviceTypeOptions = ref<Array<{ label: string; value: string }>>([])
|
||||||
|
const previewFileBlob = ref<Blob | null>(null)
|
||||||
|
const previewFileName = ref('')
|
||||||
|
const previewErrorMessage = ref('')
|
||||||
|
|
||||||
|
const currentSearchParams = computed(() =>
|
||||||
|
normalizeTestDeviceListParams((proTable.value?.searchParam || {}) as TestDevice.TestDeviceListParams)
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<TestDevice.TestDeviceRecord>[]>([
|
||||||
|
{ type: 'selection', fixed: 'left', width: 70 },
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'keyword',
|
||||||
|
label: '关键字',
|
||||||
|
minWidth: 180,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'input',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请输入设备型号或设备编号'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'timeRange',
|
||||||
|
label: '更新时间',
|
||||||
|
minWidth: 220,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'date-picker',
|
||||||
|
order: 2,
|
||||||
|
key: 'timeRange',
|
||||||
|
props: {
|
||||||
|
type: 'datetimerange',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
startPlaceholder: '开始更新时间',
|
||||||
|
endPlaceholder: '结束更新时间'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prop: 'typeName', label: '设备型号', minWidth: 180, fixed: 'left', render: ({ row }) => resolveTestDeviceText(row.typeName) },
|
||||||
|
{ prop: 'no', label: '设备编号', minWidth: 170, showOverflowTooltip: true, render: ({ row }) => resolveTestDeviceText(row.no) },
|
||||||
|
{ prop: 'validityPeriod', label: '校准有效期', width: 130, render: ({ row }) => resolveTestDeviceText(row.validityPeriod) },
|
||||||
|
{
|
||||||
|
prop: 'validityReportName',
|
||||||
|
label: '校准报告',
|
||||||
|
minWidth: 180,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => {
|
||||||
|
const reportName = row.validityReportName || row.validityReport
|
||||||
|
|
||||||
|
return row.id && reportName ? (
|
||||||
|
<el-button link type="primary" onClick={() => openPreviewDialog(row)}>
|
||||||
|
{reportName}
|
||||||
|
</el-button>
|
||||||
|
) : (
|
||||||
|
resolveTestDeviceText(reportName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'state',
|
||||||
|
label: '设备状态',
|
||||||
|
width: 110,
|
||||||
|
enum: TEST_DEVICE_STATE_OPTIONS,
|
||||||
|
isFilterEnum: false,
|
||||||
|
render: scope => (
|
||||||
|
<el-tag type={getTestDeviceStateTagType(scope.row.state)} effect="light">
|
||||||
|
{getTestDeviceStateText(scope.row.state)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'createByName', label: '创建人', width: 120, render: ({ row }) => resolveTestDeviceText(row.createByName || row.createBy) },
|
||||||
|
{ prop: 'createTime', label: '创建时间', minWidth: 170, render: ({ row }) => resolveTestDeviceText(row.createTime) },
|
||||||
|
{ prop: 'updateByName', label: '更新人', width: 120, render: ({ row }) => resolveTestDeviceText(row.updateByName || row.updateBy) },
|
||||||
|
{ prop: 'updateTime', label: '更新时间', minWidth: 170, render: ({ row }) => resolveTestDeviceText(row.updateTime) },
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 300 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getTableList = async (params: TestDevice.TestDeviceListParams = {}) => {
|
||||||
|
const response = await listTestDevicesApi(normalizeTestDeviceListParams(params))
|
||||||
|
const pageData = normalizeTestDevicePage<TestDevice.TestDeviceRecord>(
|
||||||
|
response as unknown as ResPage<TestDevice.TestDeviceRecord>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTestDevices = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备列表查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPreviewState = () => {
|
||||||
|
previewLoading.value = false
|
||||||
|
previewFileBlob.value = null
|
||||||
|
previewFileName.value = ''
|
||||||
|
previewErrorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDeviceTypeOptions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listDeviceTypesApi()
|
||||||
|
const records = unwrapTestDevicePayload<Array<{ id?: string; name?: string }>>(response) || []
|
||||||
|
|
||||||
|
deviceTypeOptions.value = records
|
||||||
|
.filter(record => record.id && record.name)
|
||||||
|
.map(record => ({
|
||||||
|
label: record.name!,
|
||||||
|
value: record.id!
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '设备型号列表查询失败'))
|
||||||
|
deviceTypeOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
formMode.value = 'create'
|
||||||
|
currentRecord.value = null
|
||||||
|
loadDeviceTypeOptions()
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditDialog = (row: TestDevice.TestDeviceRecord) => {
|
||||||
|
formMode.value = 'edit'
|
||||||
|
currentRecord.value = row
|
||||||
|
loadDeviceTypeOptions()
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetailDialog = async (row: TestDevice.TestDeviceRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前检测设备缺少 ID,无法查看详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRecord.value = null
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 详情以 GET /{id} 为准,避免列表缺失报告名称或创建人等回显字段。
|
||||||
|
const response = await getTestDeviceDetailApi(row.id)
|
||||||
|
detailRecord.value = unwrapTestDevicePayload<TestDevice.TestDeviceRecord>(response)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备详情查询失败'))
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImportDialog = () => {
|
||||||
|
importResult.value = null
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPreviewDialog = async (row: TestDevice.TestDeviceRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前检测设备缺少 ID,无法预览校准报告')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPreviewState()
|
||||||
|
previewDialogVisible.value = true
|
||||||
|
previewLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:test-device 当前仅提供 /{id}/report,报告名预览与“报告”下载统一复用同一报告流。
|
||||||
|
const response = await previewTestDeviceReportApi(row.id)
|
||||||
|
previewFileBlob.value = response.data as Blob
|
||||||
|
previewFileName.value = row.validityReportName || row.validityReport || `${row.no || 'test-device-report'}.pdf`
|
||||||
|
} catch (error) {
|
||||||
|
previewErrorMessage.value = getTestDeviceErrorMessage(error, '检测设备校准报告预览加载失败')
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveTestDevice = async ({
|
||||||
|
request,
|
||||||
|
reportFile
|
||||||
|
}: {
|
||||||
|
request: TestDevice.TestDeviceSaveRequest
|
||||||
|
reportFile: File | null
|
||||||
|
}) => {
|
||||||
|
formSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (formMode.value === 'create') {
|
||||||
|
if (!reportFile) {
|
||||||
|
ElMessage.warning('新增检测设备时必须上传 PDF 校准报告')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTestDeviceApi(request, reportFile)
|
||||||
|
ElMessage.success('检测设备新增成功')
|
||||||
|
} else {
|
||||||
|
await updateTestDeviceApi(request, reportFile)
|
||||||
|
ElMessage.success('检测设备编辑成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
formDialogVisible.value = false
|
||||||
|
refreshTestDevices()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备保存失败'))
|
||||||
|
} finally {
|
||||||
|
formSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTestDevices = async (ids: string[], successMessage: string) => {
|
||||||
|
if (!ids.length) {
|
||||||
|
ElMessage.warning('请选择要删除的检测设备')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('删除后检测设备将不可见,是否继续?', '删除检测设备', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTestDevicesApi(ids)
|
||||||
|
ElMessage.success(successMessage)
|
||||||
|
refreshTestDevices()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备删除失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row: TestDevice.TestDeviceRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前检测设备缺少 ID,无法删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteTestDevices([row.id], '检测设备删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBatchDelete = async (ids: string[]) => {
|
||||||
|
await deleteTestDevices(ids, '检测设备批量删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadTemplate = async () => {
|
||||||
|
try {
|
||||||
|
await useDownloadWithServerFileName(
|
||||||
|
() => downloadTestDeviceImportTemplateApi(),
|
||||||
|
'检测设备导入模板',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'.xlsx'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备导入模板下载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
await useDownloadWithServerFileName(exportTestDevicesApi, '检测设备列表', currentSearchParams.value, false, '.xlsx')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备导出失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadReport = async (row: TestDevice.TestDeviceRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前检测设备缺少 ID,无法下载报告')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useDownloadWithServerFileName(
|
||||||
|
() => downloadTestDeviceReportApi(row.id!),
|
||||||
|
row.validityReportName || row.no || '检测设备校准报告',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'.pdf'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备校准报告下载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportTestDevices = async ({ file, reportZip }: { file: File; reportZip: File }) => {
|
||||||
|
importing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 导入接口按整批校验返回成功/失败明细,结果保留在弹窗内便于核对。
|
||||||
|
const response = await importTestDevicesApi(file, reportZip)
|
||||||
|
importResult.value = unwrapTestDevicePayload<TestDevice.TestDeviceImportResult>(response)
|
||||||
|
const importFeedback = getTestDeviceImportResultMessage(importResult.value)
|
||||||
|
ElMessage({
|
||||||
|
type: importFeedback.type,
|
||||||
|
message: importFeedback.message
|
||||||
|
})
|
||||||
|
refreshTestDevices()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getTestDeviceErrorMessage(error, '检测设备导入失败'))
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.test-device-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
138
frontend/src/views/aireport/testDevice/utils/testDevice.ts
Normal file
138
frontend/src/views/aireport/testDevice/utils/testDevice.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { TagProps } from 'element-plus'
|
||||||
|
import type { ResultData, ResPage } from '@/api/interface'
|
||||||
|
import type { TestDevice } from '@/api/aireport/testDevice/interface'
|
||||||
|
|
||||||
|
type TagType = TagProps['type']
|
||||||
|
const TEST_DEVICE_REPORT_MAX_SIZE = 10 * 1024 * 1024
|
||||||
|
const TEST_DEVICE_REPORT_EXTENSIONS = ['pdf']
|
||||||
|
const TEST_DEVICE_IMPORT_EXCEL_EXTENSIONS = ['xls', 'xlsx']
|
||||||
|
const TEST_DEVICE_IMPORT_ZIP_EXTENSIONS = ['zip']
|
||||||
|
|
||||||
|
const TEST_DEVICE_STATE_MAP: Record<string, { text: string; tagType: TagType }> = {
|
||||||
|
'01': { text: '在运', tagType: 'success' },
|
||||||
|
'02': { text: '退运', tagType: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEST_DEVICE_STATE_OPTIONS = [
|
||||||
|
{ label: '在运', value: '01', tagType: 'success' },
|
||||||
|
{ label: '退运', value: '02', tagType: 'info' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TEST_DEVICE_REPORT_MAX_SIZE_TEXT = '10MB'
|
||||||
|
|
||||||
|
export const getTestDeviceStateText = (state?: string) => TEST_DEVICE_STATE_MAP[state || '']?.text || '未知状态'
|
||||||
|
|
||||||
|
export const getTestDeviceStateTagType = (state?: string): TagType => TEST_DEVICE_STATE_MAP[state || '']?.tagType || 'info'
|
||||||
|
|
||||||
|
const getFileExtension = (fileName?: string | null) => {
|
||||||
|
if (!fileName) return ''
|
||||||
|
|
||||||
|
const segments = fileName.split('.')
|
||||||
|
return segments.length > 1 ? segments.pop()!.toLowerCase() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFileExtensionAllowed = (fileName: string, extensions: string[]) => extensions.includes(getFileExtension(fileName))
|
||||||
|
|
||||||
|
export const validateTestDeviceReportFile = (file: File) => {
|
||||||
|
if (!isFileExtensionAllowed(file.name, TEST_DEVICE_REPORT_EXTENSIONS)) {
|
||||||
|
return '校准报告仅支持PDF文件'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > TEST_DEVICE_REPORT_MAX_SIZE) {
|
||||||
|
return `校准报告不能超过${TEST_DEVICE_REPORT_MAX_SIZE_TEXT}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateTestDeviceImportExcelFile = (file: File) => {
|
||||||
|
if (!isFileExtensionAllowed(file.name, TEST_DEVICE_IMPORT_EXCEL_EXTENSIONS)) {
|
||||||
|
return '请选择 Excel 文件(.xls 或 .xlsx)'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateTestDeviceImportZipFile = (file: File) => {
|
||||||
|
if (!isFileExtensionAllowed(file.name, TEST_DEVICE_IMPORT_ZIP_EXTENSIONS)) {
|
||||||
|
return '请选择 ZIP 压缩包'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTestDeviceImportResultMessage = (result?: TestDevice.TestDeviceImportResult | null) => {
|
||||||
|
const successCount = result?.successCount || 0
|
||||||
|
const failCount = result?.failCount || 0
|
||||||
|
|
||||||
|
if (failCount > 0 && successCount > 0) {
|
||||||
|
return {
|
||||||
|
type: 'warning' as const,
|
||||||
|
message: `检测设备导入完成,成功 ${successCount} 条,失败 ${failCount} 条`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount > 0) {
|
||||||
|
return {
|
||||||
|
type: 'warning' as const,
|
||||||
|
message: `检测设备导入失败,共 ${failCount} 条,请根据失败原因修正后重试`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success' as const,
|
||||||
|
message: `检测设备导入完成,成功 ${successCount} 条`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveTestDeviceText = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) return '-'
|
||||||
|
|
||||||
|
const text = String(value).trim()
|
||||||
|
return text || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeTestDeviceListParams = (
|
||||||
|
params: TestDevice.TestDeviceListParams = {}
|
||||||
|
): TestDevice.TestDeviceListParams => {
|
||||||
|
const timeRange = Array.isArray(params.timeRange) ? params.timeRange : []
|
||||||
|
const [searchBeginTime = '', searchEndTime = ''] = timeRange
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword: String(params.keyword || '').trim() || undefined,
|
||||||
|
searchBeginTime: searchBeginTime || params.searchBeginTime || undefined,
|
||||||
|
searchEndTime: searchEndTime || params.searchEndTime || undefined,
|
||||||
|
pageNum: params.pageNum || 1,
|
||||||
|
pageSize: params.pageSize || 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unwrapTestDevicePayload = <T>(response: ResultData<T> | T): T => {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeTestDevicePage = <T>(
|
||||||
|
response: ResultData<ResPage<T>> | ResPage<T> | null | undefined
|
||||||
|
): ResPage<T> => {
|
||||||
|
const payload = unwrapTestDevicePayload<ResPage<T> | null | undefined>(response as ResultData<ResPage<T>>)
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: payload?.records || [],
|
||||||
|
current: payload?.current || 1,
|
||||||
|
size: payload?.size || 10,
|
||||||
|
total: payload?.total || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTestDeviceErrorMessage = (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
title="提交审核"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="96px">
|
||||||
|
<el-form-item label="审核意见" prop="checkResult">
|
||||||
|
<el-input
|
||||||
|
v-model="formModel.checkResult"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
maxlength="128"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入审核意见,可为空"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import type { ReportTask } from '@/api/aireport/testReport/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportTaskAuditDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
saving: boolean
|
||||||
|
record: ReportTask.ReportTaskRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: ReportTask.ReportTaskAuditRequest): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const formModel = reactive({
|
||||||
|
id: '',
|
||||||
|
checkResult: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules: FormRules = {}
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
formModel.id = ''
|
||||||
|
formModel.checkResult = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillForm = () => {
|
||||||
|
formModel.id = props.record?.id || ''
|
||||||
|
formModel.checkResult = props.record?.checkResult || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.record?.id],
|
||||||
|
([visible]) => {
|
||||||
|
if (visible) fillForm()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
id: formModel.id,
|
||||||
|
checkResult: formModel.checkResult.trim() || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visibleProxy" title="报告任务详情" width="760px" append-to-body destroy-on-close>
|
||||||
|
<div v-loading="loading" class="report-task-detail-dialog">
|
||||||
|
<el-descriptions class="detail-descriptions" :column="2" border>
|
||||||
|
<el-descriptions-item label="报告编号">{{ resolveReportTaskText(detail?.no) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="委托单位">{{ resolveReportTaskText(detail?.clientUnitName) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="报告模板">{{ resolveReportTaskText(detail?.reportModelName) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="检测公司">
|
||||||
|
{{ resolveReportTaskCreateUnitText(detail?.createUnit, companyLabelMap) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="合同编号">{{ resolveReportTaskText(detail?.contractNumber) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ resolveReportTaskText(detail?.phonenumber) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="测试依据" :span="2">
|
||||||
|
{{ resolveReportTaskStandardText(detail?.standard, standardLabelMap) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="送资数据" :span="2">{{ resolveReportTaskText(detail?.data) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核人">{{ resolveReportTaskText(detail?.checkerName) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核时间">{{ resolveReportTaskText(detail?.checkTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核意见" :span="2">{{ resolveReportTaskText(detail?.checkResult) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建人">
|
||||||
|
{{ resolveReportTaskText(detail?.createByName || detail?.createBy) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ resolveReportTaskText(detail?.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新人">
|
||||||
|
{{ resolveReportTaskText(detail?.updateByName || detail?.updateBy) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ resolveReportTaskText(detail?.updateTime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="visibleProxy = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ReportTask } from '@/api/aireport/testReport/interface'
|
||||||
|
import {
|
||||||
|
resolveReportTaskCreateUnitText,
|
||||||
|
resolveReportTaskStandardText,
|
||||||
|
resolveReportTaskText
|
||||||
|
} from '../utils/testReport'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportTaskDetailDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loading: boolean
|
||||||
|
detail: ReportTask.ReportTaskRecord | null
|
||||||
|
companyLabelMap: Record<string, string>
|
||||||
|
standardLabelMap: Record<string, string>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const companyLabelMap = computed(() => props.companyLabelMap)
|
||||||
|
const standardLabelMap = computed(() => props.standardLabelMap)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-task-detail-dialog {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-detail-dialog :deep(.detail-descriptions .el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-detail-dialog :deep(.detail-descriptions .el-descriptions__label.el-descriptions__cell.is-bordered-label) {
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visibleProxy"
|
||||||
|
:title="mode === 'create' ? '新增报告任务' : '编辑报告任务'"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="108px" class="report-task-form">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="报告编号" prop="no">
|
||||||
|
<el-input v-model="formModel.no" maxlength="32" clearable placeholder="请输入报告编号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="委托单位" prop="clientUnitId">
|
||||||
|
<el-select v-model="formModel.clientUnitId" filterable clearable placeholder="请选择委托单位">
|
||||||
|
<el-option
|
||||||
|
v-for="option in clientUnitSelectOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="报告模板" prop="reportModelId">
|
||||||
|
<el-select v-model="formModel.reportModelId" filterable clearable placeholder="请选择报告模板">
|
||||||
|
<el-option
|
||||||
|
v-for="option in reportModelSelectOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="检测公司" prop="createUnit">
|
||||||
|
<el-select v-model="formModel.createUnit" filterable clearable placeholder="请选择检测公司">
|
||||||
|
<el-option
|
||||||
|
v-for="option in companySelectOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="合同编号" prop="contractNumber">
|
||||||
|
<el-input v-model="formModel.contractNumber" maxlength="32" clearable placeholder="请输入合同编号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="测试依据" prop="standardIds">
|
||||||
|
<el-select
|
||||||
|
v-model="formModel.standardIds"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
collapse-tags
|
||||||
|
placeholder="请选择测试依据"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in standardSelectOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
<div class="standard-option">
|
||||||
|
<el-checkbox
|
||||||
|
class="standard-option-checkbox"
|
||||||
|
:model-value="formModel.standardIds.includes(option.value)"
|
||||||
|
/>
|
||||||
|
<span class="standard-option-label">{{ option.label }}</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="联系电话" prop="phonenumber">
|
||||||
|
<el-input v-model="formModel.phonenumber" maxlength="32" clearable placeholder="请输入联系电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="送资数据" prop="data">
|
||||||
|
<el-input
|
||||||
|
v-model="formModel.data"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
maxlength="256"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入送资数据"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import type { ReportTask } from '@/api/aireport/testReport/interface'
|
||||||
|
import { parseReportTaskStandardIds, stringifyReportTaskStandardIds } from '../utils/testReport'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportTaskFormDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
mode: 'create' | 'edit'
|
||||||
|
record: ReportTask.ReportTaskRecord | null
|
||||||
|
saving: boolean
|
||||||
|
clientUnitOptions: Array<{ label: string; value: string }>
|
||||||
|
reportModelOptions: Array<{ label: string; value: string }>
|
||||||
|
companyOptions: Array<{ label: string; value: string }>
|
||||||
|
standardOptions: Array<{ label: string; value: string }>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:visible', value: boolean): void
|
||||||
|
(event: 'submit', value: ReportTask.ReportTaskSaveRequest): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const formModel = reactive({
|
||||||
|
id: '',
|
||||||
|
no: '',
|
||||||
|
clientUnitId: '',
|
||||||
|
reportModelId: '',
|
||||||
|
createUnit: '',
|
||||||
|
contractNumber: '',
|
||||||
|
standardIds: [] as string[],
|
||||||
|
data: '',
|
||||||
|
phonenumber: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
no: [{ required: true, message: '请输入报告编号', trigger: 'blur' }],
|
||||||
|
clientUnitId: [{ required: true, message: '请选择委托单位', trigger: 'change' }],
|
||||||
|
reportModelId: [{ required: true, message: '请选择报告模板', trigger: 'change' }],
|
||||||
|
createUnit: [{ required: true, message: '请选择检测公司', trigger: 'change' }],
|
||||||
|
contractNumber: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
|
||||||
|
standardIds: [{ required: true, message: '请选择测试依据', trigger: 'change' }],
|
||||||
|
data: [{ required: true, message: '请输入送资数据', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ensureCurrentOption = (
|
||||||
|
options: Array<{ label: string; value: string }>,
|
||||||
|
value: string,
|
||||||
|
fallbackLabel: string
|
||||||
|
) => {
|
||||||
|
if (!value || options.some(option => option.value === value)) return options
|
||||||
|
|
||||||
|
return [...options, { label: fallbackLabel || value, value }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientUnitSelectOptions = computed(() =>
|
||||||
|
ensureCurrentOption(props.clientUnitOptions, formModel.clientUnitId, props.record?.clientUnitName || '')
|
||||||
|
)
|
||||||
|
const reportModelSelectOptions = computed(() =>
|
||||||
|
ensureCurrentOption(props.reportModelOptions, formModel.reportModelId, props.record?.reportModelName || '')
|
||||||
|
)
|
||||||
|
const companySelectOptions = computed(() =>
|
||||||
|
ensureCurrentOption(props.companyOptions, formModel.createUnit, props.record?.createUnit || '')
|
||||||
|
)
|
||||||
|
const standardSelectOptions = computed(() => {
|
||||||
|
const optionMap = new Map(props.standardOptions.map(option => [option.value, option]))
|
||||||
|
const extraOptions = formModel.standardIds
|
||||||
|
.filter(item => !optionMap.has(item))
|
||||||
|
.map(item => ({ label: item, value: item }))
|
||||||
|
|
||||||
|
return [...props.standardOptions, ...extraOptions]
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
formModel.id = ''
|
||||||
|
formModel.no = ''
|
||||||
|
formModel.clientUnitId = ''
|
||||||
|
formModel.reportModelId = ''
|
||||||
|
formModel.createUnit = ''
|
||||||
|
formModel.contractNumber = ''
|
||||||
|
formModel.standardIds = []
|
||||||
|
formModel.data = ''
|
||||||
|
formModel.phonenumber = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillForm = () => {
|
||||||
|
formModel.id = props.record?.id || ''
|
||||||
|
formModel.no = props.record?.no || ''
|
||||||
|
formModel.clientUnitId = props.record?.clientUnitId || ''
|
||||||
|
formModel.reportModelId = props.record?.reportModelId || ''
|
||||||
|
formModel.createUnit = props.record?.createUnit || ''
|
||||||
|
formModel.contractNumber = props.record?.contractNumber || ''
|
||||||
|
formModel.standardIds = parseReportTaskStandardIds(props.record?.standard)
|
||||||
|
formModel.data = props.record?.data || ''
|
||||||
|
formModel.phonenumber = props.record?.phonenumber || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.mode, props.record?.id],
|
||||||
|
([visible]) => {
|
||||||
|
if (visible) fillForm()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
id: formModel.id || undefined,
|
||||||
|
no: formModel.no.trim(),
|
||||||
|
clientUnitId: formModel.clientUnitId,
|
||||||
|
reportModelId: formModel.reportModelId,
|
||||||
|
createUnit: formModel.createUnit,
|
||||||
|
contractNumber: formModel.contractNumber.trim(),
|
||||||
|
standard: stringifyReportTaskStandardIds(formModel.standardIds),
|
||||||
|
data: formModel.data.trim(),
|
||||||
|
phonenumber: formModel.phonenumber.trim() || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.report-task-form .el-form-item) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-textarea),
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-option-checkbox {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-option-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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 apiDir = path.resolve(srcDir, 'api/aireport/testReport')
|
||||||
|
|
||||||
|
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(path.join(srcDir, 'constants/dictCodes.ts'), ['TEST_REPORT_COMPANY', 'TEST_REPORT_STANDARD'])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(apiDir, 'index.ts'), [
|
||||||
|
"const REPORT_TASK_BASE_URL = '/api/test-report'",
|
||||||
|
'listReportTasksApi',
|
||||||
|
'createReportTaskApi',
|
||||||
|
'updateReportTaskApi',
|
||||||
|
'deleteReportTasksApi',
|
||||||
|
'getReportTaskDetailApi',
|
||||||
|
'exportReportTasksApi',
|
||||||
|
'submitReportTaskAuditApi'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(apiDir, 'interface/index.ts'), [
|
||||||
|
'export namespace ReportTask',
|
||||||
|
'ReportTaskRecord',
|
||||||
|
'ReportTaskListParams',
|
||||||
|
'ReportTaskSaveRequest',
|
||||||
|
'ReportTaskAuditRequest',
|
||||||
|
'standard?: string[] | string | null',
|
||||||
|
'standard: string[]'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'utils/testReport.ts'), [
|
||||||
|
'normalizeReportTaskListParams',
|
||||||
|
'normalizeReportTaskPage',
|
||||||
|
'parseReportTaskStandardIds',
|
||||||
|
'stringifyReportTaskStandardIds',
|
||||||
|
'resolveReportTaskStandardText',
|
||||||
|
'buildDictSelectOptions',
|
||||||
|
'value?: string[] | string | null',
|
||||||
|
"return text ? [text] : []"
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/ReportTaskFormDialog.vue'), [
|
||||||
|
"name: 'ReportTaskFormDialog'",
|
||||||
|
'clientUnitId',
|
||||||
|
'reportModelId',
|
||||||
|
'createUnit',
|
||||||
|
'standardIds',
|
||||||
|
'stringifyReportTaskStandardIds',
|
||||||
|
'el-checkbox',
|
||||||
|
'standard-option-checkbox'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/ReportTaskDetailDialog.vue'), [
|
||||||
|
"name: 'ReportTaskDetailDialog'",
|
||||||
|
'报告任务详情',
|
||||||
|
'resolveReportTaskCreateUnitText',
|
||||||
|
'resolveReportTaskStandardText',
|
||||||
|
'table-layout: fixed'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'components/ReportTaskAuditDialog.vue'), [
|
||||||
|
"name: 'ReportTaskAuditDialog'",
|
||||||
|
'checkResult',
|
||||||
|
'提交审核'
|
||||||
|
])
|
||||||
|
|
||||||
|
assertFileIncludes(path.join(pageDir, 'index.vue'), [
|
||||||
|
"name: 'ReportTaskPage'",
|
||||||
|
'<ProTable',
|
||||||
|
'ReportTaskFormDialog',
|
||||||
|
'ReportTaskDetailDialog',
|
||||||
|
'ReportTaskAuditDialog',
|
||||||
|
'listReportTasksApi',
|
||||||
|
'submitReportTaskAuditApi',
|
||||||
|
'exportReportTasksApi',
|
||||||
|
'DICT_CODES.TEST_REPORT_COMPANY',
|
||||||
|
'DICT_CODES.TEST_REPORT_STANDARD',
|
||||||
|
'type="primary" :icon="Plus"',
|
||||||
|
'type="danger"',
|
||||||
|
'type="primary" plain'
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('testReport page contract passed')
|
||||||
418
frontend/src/views/aireport/testReport/index.vue
Normal file
418
frontend/src/views/aireport/testReport/index.vue
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box report-task-page">
|
||||||
|
<ProTable
|
||||||
|
ref="proTable"
|
||||||
|
:columns="columns"
|
||||||
|
:request-api="getTableList"
|
||||||
|
:request-error="handleTableRequestError"
|
||||||
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
|
:tool-button="['refresh', 'setting']"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #tableHeader="scope">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleBatchDelete(scope.selectedListIds)"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Download" @click="handleExport">导出</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button link type="primary" :icon="View" @click="openDetailDialog(row)">详情</el-button>
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button link type="primary" :icon="Upload" @click="openAuditDialog(row)">提交审核</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
|
||||||
|
<ReportTaskFormDialog
|
||||||
|
v-model:visible="formDialogVisible"
|
||||||
|
:mode="formMode"
|
||||||
|
:record="currentRecord"
|
||||||
|
:saving="formSaving"
|
||||||
|
:client-unit-options="clientUnitOptions"
|
||||||
|
:report-model-options="reportModelOptions"
|
||||||
|
:company-options="companyOptions"
|
||||||
|
:standard-options="standardOptions"
|
||||||
|
@submit="handleSaveReportTask"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReportTaskDetailDialog
|
||||||
|
v-model:visible="detailDialogVisible"
|
||||||
|
:loading="detailLoading"
|
||||||
|
:detail="detailRecord"
|
||||||
|
:company-label-map="companyLabelMap"
|
||||||
|
:standard-label-map="standardLabelMap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReportTaskAuditDialog
|
||||||
|
v-model:visible="auditDialogVisible"
|
||||||
|
:saving="auditSaving"
|
||||||
|
:record="currentAuditRecord"
|
||||||
|
@submit="handleSubmitAudit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { Delete, Download, Edit, Plus, Upload, View } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import type { Dict, ResPage } from '@/api/interface'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import {
|
||||||
|
deleteReportTasksApi,
|
||||||
|
exportReportTasksApi,
|
||||||
|
getReportTaskDetailApi,
|
||||||
|
listReportTasksApi,
|
||||||
|
submitReportTaskAuditApi,
|
||||||
|
createReportTaskApi,
|
||||||
|
updateReportTaskApi
|
||||||
|
} from '@/api/aireport/testReport'
|
||||||
|
import type { ReportTask } from '@/api/aireport/testReport/interface'
|
||||||
|
import { listClientUnitsApi } from '@/api/aireport/clientUnit'
|
||||||
|
import { listReportModelsApi } from '@/api/aireport/reportModel'
|
||||||
|
import { getDictList } from '@/api/user/login'
|
||||||
|
import { DICT_CODES } from '@/constants/dictCodes'
|
||||||
|
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
|
||||||
|
import { useDictStore } from '@/stores/modules/dict'
|
||||||
|
import ReportTaskAuditDialog from './components/ReportTaskAuditDialog.vue'
|
||||||
|
import ReportTaskDetailDialog from './components/ReportTaskDetailDialog.vue'
|
||||||
|
import ReportTaskFormDialog from './components/ReportTaskFormDialog.vue'
|
||||||
|
import {
|
||||||
|
buildDictSelectOptions,
|
||||||
|
buildOptionLabelMap,
|
||||||
|
getReportTaskErrorMessage,
|
||||||
|
normalizeReportTaskListParams,
|
||||||
|
normalizeReportTaskPage,
|
||||||
|
resolveReportTaskCreateUnitText,
|
||||||
|
resolveReportTaskStandardText,
|
||||||
|
resolveReportTaskText,
|
||||||
|
unwrapReportTaskPayload
|
||||||
|
} from './utils/testReport'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ReportTaskPage'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dictStore = useDictStore()
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const auditDialogVisible = ref(false)
|
||||||
|
const formSaving = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const auditSaving = ref(false)
|
||||||
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
|
const currentRecord = ref<ReportTask.ReportTaskRecord | null>(null)
|
||||||
|
const detailRecord = ref<ReportTask.ReportTaskRecord | null>(null)
|
||||||
|
const currentAuditRecord = ref<ReportTask.ReportTaskRecord | null>(null)
|
||||||
|
const clientUnitOptions = ref<Array<{ label: string; value: string }>>([])
|
||||||
|
const reportModelOptions = ref<Array<{ label: string; value: string }>>([])
|
||||||
|
|
||||||
|
const companyOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_COMPANY)))
|
||||||
|
const standardOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_STANDARD)))
|
||||||
|
const companyLabelMap = computed(() => buildOptionLabelMap(companyOptions.value))
|
||||||
|
const standardLabelMap = computed(() => buildOptionLabelMap(standardOptions.value))
|
||||||
|
const requiredDictCodes = [DICT_CODES.TEST_REPORT_COMPANY, DICT_CODES.TEST_REPORT_STANDARD]
|
||||||
|
const currentSearchParams = computed(() =>
|
||||||
|
normalizeReportTaskListParams((proTable.value?.searchParam || {}) as ReportTask.ReportTaskListParams)
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ReportTask.ReportTaskRecord>[]>([
|
||||||
|
{ type: 'selection', fixed: 'left', width: 70 },
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'keyword',
|
||||||
|
label: '关键字',
|
||||||
|
minWidth: 180,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'input',
|
||||||
|
order: 1,
|
||||||
|
props: {
|
||||||
|
placeholder: '请输入报告编号、合同编号、委托单位或模板'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'timeRange',
|
||||||
|
label: '创建时间',
|
||||||
|
minWidth: 220,
|
||||||
|
isShow: false,
|
||||||
|
search: {
|
||||||
|
el: 'date-picker',
|
||||||
|
order: 2,
|
||||||
|
key: 'timeRange',
|
||||||
|
props: {
|
||||||
|
type: 'datetimerange',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
startPlaceholder: '开始创建时间',
|
||||||
|
endPlaceholder: '结束创建时间'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prop: 'no', label: '报告编号', minWidth: 180, fixed: 'left', render: ({ row }) => resolveReportTaskText(row.no) },
|
||||||
|
{ prop: 'clientUnitName', label: '委托单位', minWidth: 160, render: ({ row }) => resolveReportTaskText(row.clientUnitName) },
|
||||||
|
{ prop: 'reportModelName', label: '报告模板', minWidth: 160, render: ({ row }) => resolveReportTaskText(row.reportModelName) },
|
||||||
|
{
|
||||||
|
prop: 'createUnit',
|
||||||
|
label: '检测公司',
|
||||||
|
minWidth: 160,
|
||||||
|
render: ({ row }) => resolveReportTaskCreateUnitText(row.createUnit, companyLabelMap.value)
|
||||||
|
},
|
||||||
|
{ prop: 'contractNumber', label: '合同编号', minWidth: 150, render: ({ row }) => resolveReportTaskText(row.contractNumber) },
|
||||||
|
{
|
||||||
|
prop: 'standard',
|
||||||
|
label: '测试依据',
|
||||||
|
minWidth: 220,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
render: ({ row }) => resolveReportTaskStandardText(row.standard, standardLabelMap.value)
|
||||||
|
},
|
||||||
|
{ prop: 'phonenumber', label: '联系电话', minWidth: 140, render: ({ row }) => resolveReportTaskText(row.phonenumber) },
|
||||||
|
{ prop: 'checkerName', label: '审核人', minWidth: 120, render: ({ row }) => resolveReportTaskText(row.checkerName) },
|
||||||
|
{ prop: 'checkTime', label: '审核时间', minWidth: 120, render: ({ row }) => resolveReportTaskText(row.checkTime) },
|
||||||
|
{ prop: 'checkResult', label: '审核意见', minWidth: 200, showOverflowTooltip: true, render: ({ row }) => resolveReportTaskText(row.checkResult) },
|
||||||
|
{ prop: 'createByName', label: '创建人', minWidth: 120, render: ({ row }) => resolveReportTaskText(row.createByName || row.createBy) },
|
||||||
|
{ prop: 'createTime', label: '创建时间', minWidth: 170, render: ({ row }) => resolveReportTaskText(row.createTime) },
|
||||||
|
{ prop: 'updateByName', label: '更新人', minWidth: 120, isShow: false, render: ({ row }) => resolveReportTaskText(row.updateByName || row.updateBy) },
|
||||||
|
{ prop: 'updateTime', label: '更新时间', minWidth: 170, isShow: false, render: ({ row }) => resolveReportTaskText(row.updateTime) },
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 280 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const ensureReportTaskDictOptionsReady = async () => {
|
||||||
|
const missingCodes = requiredDictCodes.filter(code => !dictStore.getDictData(code).length)
|
||||||
|
if (!missingCodes.length) return
|
||||||
|
|
||||||
|
const response = await getDictList()
|
||||||
|
const dictData = (response.data as unknown as Dict[]) || []
|
||||||
|
if (!dictData.length) return
|
||||||
|
|
||||||
|
const nextDictMap = new Map((dictStore.dictData || []).map(item => [item.code, item]))
|
||||||
|
dictData.forEach(item => {
|
||||||
|
if (item?.code) {
|
||||||
|
nextDictMap.set(item.code, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关键业务节点:检测公司和测试依据展示依赖字典缓存,需要在页面缺失时补载后再做 ID 到名称映射。
|
||||||
|
dictStore.setDictData(Array.from(nextDictMap.values()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadClientUnitOptions = async () => {
|
||||||
|
const response = await listClientUnitsApi({ pageNum: 1, pageSize: 200 })
|
||||||
|
const pageData = normalizeReportTaskPage<{ id?: string; name?: string }>(
|
||||||
|
response as unknown as ResPage<{ id?: string; name?: string }>
|
||||||
|
)
|
||||||
|
|
||||||
|
clientUnitOptions.value = (pageData.records || [])
|
||||||
|
.filter(item => item.id && item.name)
|
||||||
|
.map(item => ({
|
||||||
|
label: item.name!,
|
||||||
|
value: item.id!
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadReportModelOptions = async () => {
|
||||||
|
const response = await listReportModelsApi({ pageNum: 1, pageSize: 200 })
|
||||||
|
const pageData = normalizeReportTaskPage<{ id?: string; name?: string }>(
|
||||||
|
response as unknown as ResPage<{ id?: string; name?: string }>
|
||||||
|
)
|
||||||
|
|
||||||
|
reportModelOptions.value = (pageData.records || [])
|
||||||
|
.filter(item => item.id && item.name)
|
||||||
|
.map(item => ({
|
||||||
|
label: item.name!,
|
||||||
|
value: item.id!
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureFormOptionsReady = async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all([ensureReportTaskDictOptionsReady(), loadClientUnitOptions(), loadReportModelOptions()])
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务关联选项加载失败'))
|
||||||
|
clientUnitOptions.value = []
|
||||||
|
reportModelOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableList = async (params: ReportTask.ReportTaskListParams = {}) => {
|
||||||
|
const response = await listReportTasksApi(normalizeReportTaskListParams(params))
|
||||||
|
const pageData = normalizeReportTaskPage<ReportTask.ReportTaskRecord>(
|
||||||
|
response as unknown as ResPage<ReportTask.ReportTaskRecord>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: pageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshReportTasks = () => {
|
||||||
|
proTable.value?.clearSelection()
|
||||||
|
proTable.value?.getTableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableRequestError = (error: unknown) => {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务列表查询失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = async () => {
|
||||||
|
formMode.value = 'create'
|
||||||
|
currentRecord.value = null
|
||||||
|
await ensureFormOptionsReady()
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditDialog = async (row: ReportTask.ReportTaskRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前报告任务缺少 ID,无法编辑')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureFormOptionsReady()
|
||||||
|
formMode.value = 'edit'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:编辑表单提交时依赖 clientUnitId、reportModelId、createUnit、standard 等原始值,不能只用列表展示字段回填。
|
||||||
|
const response = await getReportTaskDetailApi(row.id)
|
||||||
|
currentRecord.value = unwrapReportTaskPayload<ReportTask.ReportTaskRecord>(response)
|
||||||
|
formDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务详情查询失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetailDialog = async (row: ReportTask.ReportTaskRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前报告任务缺少 ID,无法查看详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRecord.value = null
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getReportTaskDetailApi(row.id)
|
||||||
|
detailRecord.value = unwrapReportTaskPayload<ReportTask.ReportTaskRecord>(response)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务详情查询失败'))
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAuditDialog = (row: ReportTask.ReportTaskRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前报告任务缺少 ID,无法提交审核')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAuditRecord.value = row
|
||||||
|
auditDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveReportTask = async (params: ReportTask.ReportTaskSaveRequest) => {
|
||||||
|
formSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (formMode.value === 'create') {
|
||||||
|
await createReportTaskApi(params)
|
||||||
|
ElMessage.success('报告任务新增成功')
|
||||||
|
} else {
|
||||||
|
await updateReportTaskApi(params)
|
||||||
|
ElMessage.success('报告任务编辑成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
formDialogVisible.value = false
|
||||||
|
refreshReportTasks()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务保存失败'))
|
||||||
|
} finally {
|
||||||
|
formSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAudit = async (params: ReportTask.ReportTaskAuditRequest) => {
|
||||||
|
auditSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submitReportTaskAuditApi(params)
|
||||||
|
ElMessage.success('报告任务提交审核成功')
|
||||||
|
auditDialogVisible.value = false
|
||||||
|
refreshReportTasks()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务提交审核失败'))
|
||||||
|
} finally {
|
||||||
|
auditSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteReportTasks = async (ids: string[], successMessage: string) => {
|
||||||
|
if (!ids.length) {
|
||||||
|
ElMessage.warning('请选择要删除的报告任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('删除后报告任务将不可见,是否继续?', '删除报告任务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteReportTasksApi(ids)
|
||||||
|
ElMessage.success(successMessage)
|
||||||
|
refreshReportTasks()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务删除失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row: ReportTask.ReportTaskRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前报告任务缺少 ID,无法删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteReportTasks([row.id], '报告任务删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBatchDelete = async (ids: string[]) => {
|
||||||
|
await deleteReportTasks(ids, '报告任务批量删除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
await useDownloadWithServerFileName(exportReportTasksApi, '普测报告列表', currentSearchParams.value, false, '.xlsx')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '报告任务导出失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await ensureReportTaskDictOptionsReady()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getReportTaskErrorMessage(error, '鎶ュ憡浠诲姟瀛楀吀鍔犺浇澶辫触'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.report-task-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
frontend/src/views/aireport/testReport/utils/testReport.ts
Normal file
111
frontend/src/views/aireport/testReport/utils/testReport.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type { Dict, ResultData, ResPage } from '@/api/interface'
|
||||||
|
import type { ReportTask } from '@/api/aireport/testReport/interface'
|
||||||
|
|
||||||
|
export const resolveReportTaskText = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) return '-'
|
||||||
|
|
||||||
|
const text = String(value).trim()
|
||||||
|
return text || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeReportTaskListParams = (
|
||||||
|
params: ReportTask.ReportTaskListParams = {}
|
||||||
|
): ReportTask.ReportTaskListParams => {
|
||||||
|
const timeRange = Array.isArray(params.timeRange) ? params.timeRange : []
|
||||||
|
const [searchBeginTime = '', searchEndTime = ''] = timeRange
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword: String(params.keyword || '').trim() || undefined,
|
||||||
|
searchBeginTime: searchBeginTime || params.searchBeginTime || undefined,
|
||||||
|
searchEndTime: searchEndTime || params.searchEndTime || undefined,
|
||||||
|
pageNum: params.pageNum || 1,
|
||||||
|
pageSize: params.pageSize || 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unwrapReportTaskPayload = <T>(response: ResultData<T> | T): T => {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeReportTaskPage = <T>(
|
||||||
|
response: ResultData<ResPage<T>> | ResPage<T> | null | undefined
|
||||||
|
): ResPage<T> => {
|
||||||
|
const payload = unwrapReportTaskPayload<ResPage<T> | null | undefined>(response as ResultData<ResPage<T>>)
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: payload?.records || [],
|
||||||
|
current: payload?.current || 1,
|
||||||
|
size: payload?.size || 10,
|
||||||
|
total: payload?.total || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getReportTaskErrorMessage = (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
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildDictSelectOptions = (dictData: Dict[] = []) =>
|
||||||
|
dictData
|
||||||
|
.filter(item => item.id && item.name)
|
||||||
|
.map(item => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const buildOptionLabelMap = (options: Array<{ label: string; value: string }>) =>
|
||||||
|
options.reduce<Record<string, string>>((result, option) => {
|
||||||
|
result[option.value] = option.label
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
export const parseReportTaskStandardIds = (value?: string[] | string | null) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(item => String(item).trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(text)
|
||||||
|
if (Array.isArray(parsedValue)) {
|
||||||
|
return parsedValue.map(item => String(item).trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return text ? [text] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stringifyReportTaskStandardIds = (value: string[]) =>
|
||||||
|
value.map(item => String(item).trim()).filter(Boolean)
|
||||||
|
|
||||||
|
export const resolveReportTaskCreateUnitText = (
|
||||||
|
value: string | null | undefined,
|
||||||
|
companyLabelMap: Record<string, string>
|
||||||
|
) => {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return '-'
|
||||||
|
|
||||||
|
return companyLabelMap[text] || text
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveReportTaskStandardText = (
|
||||||
|
value: string[] | string | null | undefined,
|
||||||
|
standardLabelMap: Record<string, string>
|
||||||
|
) => {
|
||||||
|
const standardIds = parseReportTaskStandardIds(value)
|
||||||
|
if (!standardIds.length) return resolveReportTaskText(value)
|
||||||
|
|
||||||
|
return standardIds.map(item => standardLabelMap[item] || item).join('、') || '-'
|
||||||
|
}
|
||||||
@@ -4,21 +4,23 @@
|
|||||||
|
|
||||||
- 模块路径:`steady/check-square`
|
- 模块路径:`steady/check-square`
|
||||||
- 接口基础路径:`/steady/checksquare`
|
- 接口基础路径:`/steady/checksquare`
|
||||||
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的业务数据字段 `data`。
|
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的 `data`
|
||||||
- 时间格式:`yyyy-MM-dd HH:mm:ss`
|
- 时间格式:`yyyy-MM-dd HH:mm:ss`
|
||||||
|
- Content-Type:`application/json`
|
||||||
|
|
||||||
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询、失败任务重启和任务删除能力。当前标准支持单监测点和多监测点任务:单监测点可继续传 `lineId`,多监测点推荐传 `lineIds`。
|
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询、失败任务重启和任务删除能力。
|
||||||
|
|
||||||
## 2. 通用约定
|
当前标准统一使用 `lineIds` 表示任务监测点列表。单监测点也传单元素数组,例如:
|
||||||
|
|
||||||
### 2.1 请求头
|
```json
|
||||||
|
{
|
||||||
|
"lineIds": ["LINE_001"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
| 名称 | 示例 | 说明 |
|
## 2. 通用枚举
|
||||||
| --- | --- | --- |
|
|
||||||
| `Content-Type` | `application/json` | `POST` 请求使用 JSON 请求体 |
|
|
||||||
| `Authorization` | 登录态 Token | 如当前环境开启认证,需携带现有登录接口返回的认证信息 |
|
|
||||||
|
|
||||||
### 2.2 任务状态
|
### 2.1 任务状态
|
||||||
|
|
||||||
| 值 | 说明 |
|
| 值 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
| `SUCCESS` | 执行成功 |
|
| `SUCCESS` | 执行成功 |
|
||||||
| `FAIL` | 执行失败 |
|
| `FAIL` | 执行失败 |
|
||||||
|
|
||||||
### 2.3 明细类型
|
### 2.2 明细类型
|
||||||
|
|
||||||
| 值 | 说明 |
|
| 值 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
| `VALUE_ORDER` | 指标值大小关系异常明细 |
|
| `VALUE_ORDER` | 指标值大小关系异常明细 |
|
||||||
| `HARMONIC_PARITY` | 谐波奇偶关系异常明细 |
|
| `HARMONIC_PARITY` | 谐波奇偶关系异常明细 |
|
||||||
|
|
||||||
### 2.4 统计类型
|
### 2.3 统计类型
|
||||||
|
|
||||||
| 值 | 说明 |
|
| 值 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -43,19 +45,17 @@
|
|||||||
| `MIN` | 最小值 |
|
| `MIN` | 最小值 |
|
||||||
| `CP95` | CP95 值 |
|
| `CP95` | CP95 值 |
|
||||||
|
|
||||||
## 3. 调试顺序建议
|
## 3. 调试顺序
|
||||||
|
|
||||||
1. 调用 `POST /steady/checksquare/create`:按监测点列表、时间范围和指标列表创建或复用任务。
|
1. 调用 `POST /steady/checksquare/create`,按监测点列表、时间范围和指标列表创建或复用任务。
|
||||||
2. 读取 `/create` 返回的 `data.taskId`:该返回值是任务列表中的行信息。
|
2. 读取 `/create` 返回的 `data.taskId`。
|
||||||
3. 如果 `taskStatus=RUNNING`,轮询 `POST /steady/checksquare/query`,或稍后调用 `GET /steady/checksquare/detail` 查看任务详情。
|
3. 如果 `taskStatus=RUNNING`,轮询 `POST /steady/checksquare/query`,或稍后调用 `GET /steady/checksquare/detail` 查看任务详情。
|
||||||
4. 调用 `GET /steady/checksquare/detail`:用 `taskId` 查询任务下的检测项。
|
4. 调用 `GET /steady/checksquare/detail`,用 `taskId` 查询任务下的检测项。
|
||||||
5. 读取 `/detail` 返回的 `items[].itemId`:按检测项继续查询连续性区间或异常明细。
|
5. 读取 `/detail` 返回的 `items[].itemId`。
|
||||||
6. 调用 `GET /steady/checksquare/item-detail`:按 `itemId + detailType` 查询具体明细。
|
6. 调用 `GET /steady/checksquare/item-detail`,按 `itemId + detailType` 查询具体明细。
|
||||||
7. 任务失败后如需重新执行,调用 `POST /steady/checksquare/restart`。
|
7. 任务失败后如需重新执行,调用 `POST /steady/checksquare/restart`。
|
||||||
8. 需要清理任务时调用 `POST /steady/checksquare/delete`。
|
8. 需要清理任务时调用 `POST /steady/checksquare/delete`。
|
||||||
|
|
||||||
注意:旧的获取或创建兼容接口已移除。创建或复用任务的逻辑已经合并到 `/create` 中。
|
|
||||||
|
|
||||||
## 4. 创建或复用任务
|
## 4. 创建或复用任务
|
||||||
|
|
||||||
### 4.1 接口
|
### 4.1 接口
|
||||||
@@ -64,28 +64,27 @@
|
|||||||
|
|
||||||
### 4.2 行为说明
|
### 4.2 行为说明
|
||||||
|
|
||||||
接口开始执行前,会先按 `lineIds + timeStart + timeEnd` 查询是否存在未删除任务:
|
接口执行前会先按 `lineIds + timeStart + timeEnd` 查询是否存在未删除任务:
|
||||||
|
|
||||||
- 已存在:直接返回该任务的任务列表行信息。
|
- 已存在:直接返回该任务的任务列表行信息,不生成重复任务。
|
||||||
- 不存在:创建 `RUNNING` 任务并立即返回任务列表行信息,后台继续执行校验;任务完成后状态更新为 `SUCCESS`,失败时更新为 `FAIL`。
|
- 不存在:创建 `RUNNING` 任务并立即返回任务列表行信息,后台继续执行校验。
|
||||||
|
|
||||||
`lineIds` 会去重并清理空字符串。若未传 `lineIds`,后端会兼容读取 `lineId` 并转换为单元素监测点列表。返回对象为 `SteadyChecksquareTaskVO`。
|
`lineIds` 会去重并清理空字符串。任务执行成功后状态更新为 `SUCCESS`,失败时更新为 `FAIL`。
|
||||||
|
|
||||||
### 4.3 请求字段
|
### 4.3 请求字段
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `lineId` | `String` | 条件必填 | 单监测点 ID;当 `lineIds` 为空时使用 |
|
| `lineIds` | `Array<String>` | 是 | 监测点 ID 列表;单监测点也传单元素数组 |
|
||||||
| `lineIds` | `Array<String>` | 条件必填 | 监测点 ID 列表;多监测点推荐使用该字段 |
|
|
||||||
| `indicatorCodes` | `Array<String>` | 是 | 指标编码列表 |
|
| `indicatorCodes` | `Array<String>` | 是 | 指标编码列表 |
|
||||||
| `timeStart` | `String` | 是 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
| `timeStart` | `String` | 是 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
| `timeEnd` | `String` | 是 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
| `timeEnd` | `String` | 是 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
|
||||||
`lineId` 与 `lineIds` 至少传一个;`indicatorCodes` 是数组,不要传成单个字符串。
|
`indicatorCodes` 是数组,不要传成单个字符串。
|
||||||
|
|
||||||
### 4.4 请求示例
|
### 4.4 请求示例
|
||||||
|
|
||||||
单监测点兼容写法:
|
单监测点:
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /steady/checksquare/create
|
POST /steady/checksquare/create
|
||||||
@@ -94,14 +93,14 @@ Content-Type: application/json
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"lineId": "LINE_001",
|
"lineIds": ["LINE_001"],
|
||||||
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
|
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
|
||||||
"timeStart": "2026-06-18 00:00:00",
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
"timeEnd": "2026-06-18 01:00:00"
|
"timeEnd": "2026-06-18 01:00:00"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
多监测点标准写法:
|
多监测点:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -118,7 +117,6 @@ Content-Type: application/json
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `taskId` | `String` | 任务 ID |
|
| `taskId` | `String` | 任务 ID |
|
||||||
| `taskNo` | `String` | 任务编号 |
|
| `taskNo` | `String` | 任务编号 |
|
||||||
| `lineId` | `String` | 任务首个监测点 ID;兼容旧页面字段 |
|
|
||||||
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||||
| `lineName` | `String` | 监测点名称;多监测点时为多个名称拼接结果 |
|
| `lineName` | `String` | 监测点名称;多监测点时为多个名称拼接结果 |
|
||||||
| `timeStart` | `String` | 开始时间 |
|
| `timeStart` | `String` | 开始时间 |
|
||||||
@@ -139,7 +137,6 @@ Content-Type: application/json
|
|||||||
"data": {
|
"data": {
|
||||||
"taskId": "1812345678901234567",
|
"taskId": "1812345678901234567",
|
||||||
"taskNo": "CS202606180001",
|
"taskNo": "CS202606180001",
|
||||||
"lineId": "LINE_001",
|
|
||||||
"lineIds": ["LINE_001", "LINE_002"],
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
"lineName": "1号监测点,2号监测点",
|
"lineName": "1号监测点,2号监测点",
|
||||||
"timeStart": "2026-06-18 00:00:00",
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
@@ -216,7 +213,6 @@ Content-Type: application/json
|
|||||||
{
|
{
|
||||||
"taskId": "1812345678901234567",
|
"taskId": "1812345678901234567",
|
||||||
"taskNo": "CS202606180001",
|
"taskNo": "CS202606180001",
|
||||||
"lineId": "LINE_001",
|
|
||||||
"lineIds": ["LINE_001", "LINE_002"],
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
"lineName": "1号监测点,2号监测点",
|
"lineName": "1号监测点,2号监测点",
|
||||||
"timeStart": "2026-06-18 00:00:00",
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
@@ -261,7 +257,6 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `taskId` | `String` | 任务 ID |
|
| `taskId` | `String` | 任务 ID |
|
||||||
| `taskNo` | `String` | 任务编号 |
|
| `taskNo` | `String` | 任务编号 |
|
||||||
| `lineId` | `String` | 任务首个监测点 ID;兼容旧页面字段 |
|
|
||||||
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||||
| `lineName` | `String` | 监测点名称 |
|
| `lineName` | `String` | 监测点名称 |
|
||||||
| `timeStart` | `String` | 开始时间 |
|
| `timeStart` | `String` | 开始时间 |
|
||||||
@@ -318,7 +313,6 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
|||||||
"data": {
|
"data": {
|
||||||
"taskId": "1812345678901234567",
|
"taskId": "1812345678901234567",
|
||||||
"taskNo": "CS202606180001",
|
"taskNo": "CS202606180001",
|
||||||
"lineId": "LINE_001",
|
|
||||||
"lineIds": ["LINE_001", "LINE_002"],
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
"lineName": "1号监测点,2号监测点",
|
"lineName": "1号监测点,2号监测点",
|
||||||
"timeStart": "2026-06-18 00:00:00",
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
@@ -327,7 +321,7 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"itemId": "1812345678901234568",
|
"itemId": "1812345678901234568",
|
||||||
"itemKey": "LINE_001:VOLTAGE_A",
|
"itemKey": "LINE_001|VOLTAGE_A",
|
||||||
"lineId": "LINE_001",
|
"lineId": "LINE_001",
|
||||||
"lineName": "1号监测点",
|
"lineName": "1号监测点",
|
||||||
"indicatorCode": "VOLTAGE_A",
|
"indicatorCode": "VOLTAGE_A",
|
||||||
@@ -378,8 +372,8 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
|||||||
| `itemId` | `String` | 是 | 检测项 ID |
|
| `itemId` | `String` | 是 | 检测项 ID |
|
||||||
| `detailType` | `String` | 是 | 明细类型:`SEGMENT`、`VALUE_ORDER`、`HARMONIC_PARITY` |
|
| `detailType` | `String` | 是 | 明细类型:`SEGMENT`、`VALUE_ORDER`、`HARMONIC_PARITY` |
|
||||||
| `statType` | `String` | 否 | 统计类型;查询统计类型连续性或谐波奇偶明细时使用 |
|
| `statType` | `String` | 否 | 统计类型;查询统计类型连续性或谐波奇偶明细时使用 |
|
||||||
| `pageNum` | `Integer` | 否 | 页码;和 `pageSize` 同时为正数时启用分页 |
|
| `pageNum` | `Integer` | 否 | 页码;和 `pageSize` 同时为正整数时启用分页 |
|
||||||
| `pageSize` | `Integer` | 否 | 每页条数;和 `pageNum` 同时为正数时启用分页 |
|
| `pageSize` | `Integer` | 否 | 每页条数;和 `pageNum` 同时为正整数时启用分页 |
|
||||||
|
|
||||||
### 7.3 请求示例
|
### 7.3 请求示例
|
||||||
|
|
||||||
@@ -530,7 +524,7 @@ GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=HARMON
|
|||||||
- 仅允许重启 `taskStatus=FAIL` 的任务。
|
- 仅允许重启 `taskStatus=FAIL` 的任务。
|
||||||
- 重启复用原任务记录,`taskId` 不变。
|
- 重启复用原任务记录,`taskId` 不变。
|
||||||
- 重启前会清理该任务旧的检测项、统计摘要和明细数据,避免重复结果键。
|
- 重启前会清理该任务旧的检测项、统计摘要和明细数据,避免重复结果键。
|
||||||
- 接口返回时任务状态已更新为 `RUNNING`;后台执行结束后更新为 `SUCCESS` 或 `FAIL`。
|
- 接口返回时任务状态已更新为 `RUNNING`。
|
||||||
|
|
||||||
### 8.3 请求参数
|
### 8.3 请求参数
|
||||||
|
|
||||||
@@ -544,9 +538,7 @@ GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=HARMON
|
|||||||
POST /steady/checksquare/restart?taskId=1812345678901234567
|
POST /steady/checksquare/restart?taskId=1812345678901234567
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8.5 响应说明
|
### 8.5 响应示例
|
||||||
|
|
||||||
响应 `data` 与 `/create` 的 `SteadyChecksquareTaskVO` 字段一致。
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -555,7 +547,6 @@ POST /steady/checksquare/restart?taskId=1812345678901234567
|
|||||||
"data": {
|
"data": {
|
||||||
"taskId": "1812345678901234567",
|
"taskId": "1812345678901234567",
|
||||||
"taskNo": "CS202606180001",
|
"taskNo": "CS202606180001",
|
||||||
"lineId": "LINE_001",
|
|
||||||
"lineIds": ["LINE_001", "LINE_002"],
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
"lineName": "1号监测点,2号监测点",
|
"lineName": "1号监测点,2号监测点",
|
||||||
"timeStart": "2026-06-18 00:00:00",
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
@@ -607,15 +598,15 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. 常见调试注意事项
|
## 10. 调试注意事项
|
||||||
|
|
||||||
- `/create` 返回的是任务列表行信息,不是详情页完整数据。
|
- `/create` 只接收 `lineIds`,不再接收任务级 `lineId`。
|
||||||
- `/create` 如果命中已存在任务,会直接返回该任务,不会生成重复任务。
|
- 单监测点也传 `lineIds` 单元素数组。
|
||||||
- 新标准推荐传 `lineIds`;`lineId` 仅作为单监测点兼容字段保留。
|
- `/create` 如果命中已存在任务,会直接返回该任务,不生成重复任务。
|
||||||
- `/query` 的 `lineId` 会匹配任务内的 `lineIds`,可用于筛选多监测点任务。
|
- `/query` 的 `lineId` 是筛选条件,会匹配任务内的 `lineIds`。
|
||||||
|
- 任务级返回字段使用 `lineIds`;检测项级 `items[].lineId` 表示该检测项实际所属监测点。
|
||||||
- `/detail` 需要使用 `/create`、`/restart` 或 `/query` 返回的 `taskId`。
|
- `/detail` 需要使用 `/create`、`/restart` 或 `/query` 返回的 `taskId`。
|
||||||
- `/detail` 的任务级 `lineId` 是首个监测点,检测项级 `items[].lineId` 才是该检测项实际所属监测点。
|
|
||||||
- `/item-detail` 需要使用 `/detail` 返回的 `items[].itemId`。
|
- `/item-detail` 需要使用 `/detail` 返回的 `items[].itemId`。
|
||||||
- `/item-detail` 只有 `pageNum` 和 `pageSize` 同时为正数时才分页;否则返回全部匹配明细,分页字段为空。
|
- `/item-detail` 只有 `pageNum` 和 `pageSize` 同时为正整数时才分页;否则返回全部匹配明细,分页字段为空。
|
||||||
- `/restart` 只允许重启失败任务,成功或执行中的任务调用会返回业务失败。
|
- `/restart` 只允许重启失败任务,成功或执行中的任务调用会返回业务失败。
|
||||||
- `dataIntegrity` 是 0 到 1 的小数,展示百分比优先使用 `dataIntegrityText`。
|
- `dataIntegrity` 是 0 到 1 的小数,展示百分比优先使用 `dataIntegrityText`。
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ const checks = [
|
|||||||
const typeBlock =
|
const typeBlock =
|
||||||
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
||||||
return (
|
return (
|
||||||
/lineId\?: string/.test(typeBlock) &&
|
!/lineId\?: string/.test(typeBlock) &&
|
||||||
/lineIds\?: string\[\]/.test(typeBlock) &&
|
/lineIds: string\[\]/.test(typeBlock) &&
|
||||||
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
||||||
/timeStart: string/.test(typeBlock) &&
|
/timeStart: string/.test(typeBlock) &&
|
||||||
/timeEnd: string/.test(typeBlock) &&
|
/timeEnd: string/.test(typeBlock) &&
|
||||||
@@ -206,7 +206,7 @@ const checks = [
|
|||||||
return (
|
return (
|
||||||
/buildSteadyChecksquareCreatePayload\s*=\s*\(\s*lineIds:\s*string\[\]/.test(payload) &&
|
/buildSteadyChecksquareCreatePayload\s*=\s*\(\s*lineIds:\s*string\[\]/.test(payload) &&
|
||||||
/lineIds,/.test(payload) &&
|
/lineIds,/.test(payload) &&
|
||||||
/lineId:\s*lineIds\[0\]/.test(payload) &&
|
!/lineId:\s*lineIds\[0\]/.test(payload) &&
|
||||||
!/lineIds\.length > 1/.test(payload) &&
|
!/lineIds\.length > 1/.test(payload) &&
|
||||||
/buildSteadyChecksquareCreatePayload\(lineIds\.value,\s*selectedIndicators\.value,\s*formState\.value\)/.test(
|
/buildSteadyChecksquareCreatePayload\(lineIds\.value,\s*selectedIndicators\.value,\s*formState\.value\)/.test(
|
||||||
page
|
page
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export const buildSteadyChecksquareCreatePayload = (
|
|||||||
formState: ChecksquareFormState
|
formState: ChecksquareFormState
|
||||||
): SteadyDataView.SteadyChecksquareCreateParams => {
|
): SteadyDataView.SteadyChecksquareCreateParams => {
|
||||||
return {
|
return {
|
||||||
lineId: lineIds[0],
|
|
||||||
lineIds,
|
lineIds,
|
||||||
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
||||||
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const openDiskMonitor = async () => {
|
const openDiskMonitor = async () => {
|
||||||
// 系统监控页作为模块入口,保持磁盘监控返回链路闭环。
|
// 系统监控页作为模块入口,保持磁盘监控返回链路闭环。
|
||||||
await router.push('/systemMonitor/diskMonitor')
|
await router.push('/system-monitor/diskMonitor')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
||||||
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
||||||
@update-mapping-json="handleUpdateMappingJsonEvent"
|
@update-mapping-json="handleUpdateMappingJsonEvent"
|
||||||
|
@update-xml-mapping="handleUpdateXmlMappingEvent"
|
||||||
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
|
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
|
||||||
@sequence-config-complete="handleSequenceConfigComplete"
|
@sequence-config-complete="handleSequenceConfigComplete"
|
||||||
@save-icd-check-result="handleSaveIcdCheckResult"
|
@save-icd-check-result="handleSaveIcdCheckResult"
|
||||||
@@ -201,6 +202,7 @@ const {
|
|||||||
handleConfirmIcdConsistencyProblems,
|
handleConfirmIcdConsistencyProblems,
|
||||||
handleRemoveIcdConsistencyProblem,
|
handleRemoveIcdConsistencyProblem,
|
||||||
handleUpdateMappingJson,
|
handleUpdateMappingJson,
|
||||||
|
handleUpdateXmlMapping,
|
||||||
handleSequenceConfigComplete,
|
handleSequenceConfigComplete,
|
||||||
handleSaveIcdCheckResult,
|
handleSaveIcdCheckResult,
|
||||||
handleConfirmIndexSelection
|
handleConfirmIndexSelection
|
||||||
@@ -249,6 +251,13 @@ const handleUpdateMappingJsonEvent = (...args: unknown[]) => {
|
|||||||
handleUpdateMappingJson(mappingJson)
|
handleUpdateMappingJson(mappingJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpdateXmlMappingEvent = (...args: unknown[]) => {
|
||||||
|
const [xmlMapping] = args
|
||||||
|
|
||||||
|
if (typeof xmlMapping !== 'string') return
|
||||||
|
handleUpdateXmlMapping(xmlMapping)
|
||||||
|
}
|
||||||
|
|
||||||
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
|
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
|
||||||
|
|
||||||
interface IcdCheckFlowStep {
|
interface IcdCheckFlowStep {
|
||||||
|
|||||||
@@ -46,23 +46,18 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角度">
|
<div class="icd-path-form-dialog__switch-row">
|
||||||
<el-input-number
|
<el-form-item label="支持相角">
|
||||||
v-model="formModel.angle"
|
<el-switch v-model="formModel.angle" :active-value="1" :inactive-value="0" />
|
||||||
:min="-360"
|
|
||||||
:max="360"
|
|
||||||
controls-position="right"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="相位索引">
|
<el-form-item label="使用相别指标">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="formModel.usePhaseIndex"
|
v-model="formModel.usePhaseIndex"
|
||||||
:active-value="1"
|
:active-value="1"
|
||||||
:inactive-value="0"
|
:inactive-value="0"
|
||||||
active-text="启用"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,9 +123,11 @@ const icdTypeOptions = [
|
|||||||
{ label: '上游解析传递的标准 ICD', value: 3 },
|
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||||
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||||
]
|
]
|
||||||
|
const isStandardIcdType = (type?: number) => type === 1 || type === 3
|
||||||
|
const isReferenceIcdRequired = computed(() => !isStandardIcdType(formModel.type))
|
||||||
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
||||||
name: [{ required: true, message: '请输入 ICD 名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入 ICD 名称', trigger: 'blur' }],
|
||||||
referenceIcdId: [{ required: true, message: '请选择标准 ICD 引用', trigger: 'change' }]
|
referenceIcdId: [{ required: isReferenceIcdRequired.value, message: '请选择标准 ICD 引用', trigger: 'change' }]
|
||||||
}))
|
}))
|
||||||
const dialogTitle = computed(() => (props.mode === 'create' ? '新增ICD记录' : '编辑ICD记录'))
|
const dialogTitle = computed(() => (props.mode === 'create' ? '新增ICD记录' : '编辑ICD记录'))
|
||||||
|
|
||||||
@@ -179,12 +176,23 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formModel.type,
|
||||||
|
type => {
|
||||||
|
// 标准 ICD 的引用就是当前记录本身,切换类型时清掉旧的外部引用。
|
||||||
|
if (isStandardIcdType(type)) {
|
||||||
|
formModel.referenceIcdId = ''
|
||||||
|
formRef.value?.clearValidate('referenceIcdId')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
||||||
name: formModel.name.trim(),
|
name: formModel.name.trim(),
|
||||||
angle: formModel.angle,
|
angle: formModel.angle,
|
||||||
usePhaseIndex: formModel.usePhaseIndex,
|
usePhaseIndex: formModel.usePhaseIndex,
|
||||||
type: formModel.type,
|
type: formModel.type,
|
||||||
referenceIcdId: formModel.referenceIcdId
|
referenceIcdId: isStandardIcdType(formModel.type) ? '' : formModel.referenceIcdId
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -262,6 +270,12 @@ const handleSave = async () => {
|
|||||||
padding: 4px 0 0;
|
padding: 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icd-path-form-dialog__switch-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
column-gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.icd-check-dialog .el-dialog__body) {
|
:deep(.icd-check-dialog .el-dialog__body) {
|
||||||
padding: 16px 20px 4px;
|
padding: 16px 20px 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,51 @@
|
|||||||
<div class="json-tree-meta">{{ metaText }}</div>
|
<div class="json-tree-meta">{{ metaText }}</div>
|
||||||
<div class="json-tree-actions">
|
<div class="json-tree-actions">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
<el-button type="primary" plain size="small" :disabled="!rootNode" @click="expandAll">全部展开</el-button>
|
<template v-if="editable">
|
||||||
<el-button plain size="small" :disabled="!rootNode" @click="collapseAll">全部收起</el-button>
|
<el-button v-if="!isEditing" type="primary" plain size="small" :icon="Edit" @click="startEdit">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary" plain size="small" :icon="Check" @click="applyEdit">完成</el-button>
|
||||||
|
<el-button plain size="small" :icon="Close" @click="cancelEdit">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<el-tooltip content="全部展开" placement="top">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Expand"
|
||||||
|
:disabled="!rootNode"
|
||||||
|
aria-label="全部展开"
|
||||||
|
@click="expandAll"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="全部收起" placement="top">
|
||||||
|
<el-button
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Fold"
|
||||||
|
:disabled="!rootNode"
|
||||||
|
aria-label="全部收起"
|
||||||
|
@click="collapseAll"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
<slot name="trailing-actions" />
|
<slot name="trailing-actions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="rootNode" class="json-tree-body">
|
<div v-if="isEditing" class="json-editor-wrap">
|
||||||
|
<el-input
|
||||||
|
v-model="editableSource"
|
||||||
|
type="textarea"
|
||||||
|
resize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
class="json-editor-input"
|
||||||
|
/>
|
||||||
|
<div v-if="editError" class="json-editor-error">{{ editError }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="rootNode" class="json-tree-body">
|
||||||
<JsonTreeNode :node="rootNode" :depth="0" :is-last="true" :expanded-keys="expandedKeys" @toggle="toggleNode" />
|
<JsonTreeNode :node="rootNode" :depth="0" :is-last="true" :expanded-keys="expandedKeys" @toggle="toggleNode" />
|
||||||
</div>
|
</div>
|
||||||
<pre v-else class="mapping-json-text">{{ source }}</pre>
|
<pre v-else class="mapping-json-text">{{ source }}</pre>
|
||||||
@@ -18,6 +56,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Check, Close, Edit, Expand, Fold } from '@element-plus/icons-vue'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import JsonTreeNode, { type JsonTreeNodeModel, type JsonValueType } from './JsonMappingTreeNode.vue'
|
import JsonTreeNode, { type JsonTreeNodeModel, type JsonValueType } from './JsonMappingTreeNode.vue'
|
||||||
|
|
||||||
@@ -26,12 +65,22 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
source: string
|
source: string
|
||||||
metaText?: string
|
metaText?: string
|
||||||
|
editable?: boolean
|
||||||
|
}>(), {
|
||||||
|
editable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:source', value: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const expandedKeys = ref<Set<string>>(new Set())
|
const expandedKeys = ref<Set<string>>(new Set())
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const editableSource = ref('')
|
||||||
|
const editError = ref('')
|
||||||
|
|
||||||
const parsedJson = computed<{ valid: true; value: JsonValue } | { valid: false }>(() => {
|
const parsedJson = computed<{ valid: true; value: JsonValue } | { valid: false }>(() => {
|
||||||
try {
|
try {
|
||||||
@@ -60,6 +109,16 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.source,
|
||||||
|
source => {
|
||||||
|
if (isEditing.value) return
|
||||||
|
editableSource.value = source
|
||||||
|
editError.value = ''
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
function buildJsonNode(keyName: string | undefined, source: JsonValue, path: string): JsonTreeNodeModel {
|
function buildJsonNode(keyName: string | undefined, source: JsonValue, path: string): JsonTreeNodeModel {
|
||||||
if (Array.isArray(source)) {
|
if (Array.isArray(source)) {
|
||||||
return {
|
return {
|
||||||
@@ -137,6 +196,30 @@ function expandAll() {
|
|||||||
function collapseAll() {
|
function collapseAll() {
|
||||||
expandedKeys.value = new Set()
|
expandedKeys.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
editableSource.value = props.source
|
||||||
|
editError.value = ''
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editableSource.value = props.source
|
||||||
|
editError.value = ''
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEdit() {
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(editableSource.value)
|
||||||
|
|
||||||
|
emit('update:source', JSON.stringify(parsedValue, null, 4))
|
||||||
|
editError.value = ''
|
||||||
|
isEditing.value = false
|
||||||
|
} catch (error) {
|
||||||
|
editError.value = error instanceof Error ? error.message : 'JSON 格式不正确'
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -178,7 +261,8 @@ function collapseAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.json-tree-body,
|
.json-tree-body,
|
||||||
.mapping-json-text {
|
.mapping-json-text,
|
||||||
|
.json-editor-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -193,6 +277,38 @@ function collapseAll() {
|
|||||||
color: #172033;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.json-editor-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-input {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-input :deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-error {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid #f3d19e;
|
||||||
|
color: #b45309;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
.mapping-json-text {
|
.mapping-json-text {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(currentDir, '../../../..')
|
||||||
|
const configPanelFile = path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingConfigPanel.vue')
|
||||||
|
const resultPanelFile = path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingResultPanel.vue')
|
||||||
|
|
||||||
|
const configPanelSource = fs.readFileSync(configPanelFile, 'utf8')
|
||||||
|
const resultPanelSource = fs.readFileSync(resultPanelFile, 'utf8')
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
[
|
||||||
|
'config panel exposes fullscreen action for index selection content',
|
||||||
|
() =>
|
||||||
|
/:icon="FullScreen"/.test(configPanelSource) &&
|
||||||
|
/@click="indexSelectionFullscreenVisible = true"/.test(configPanelSource) &&
|
||||||
|
/title="索引配置全屏查看"/.test(configPanelSource) &&
|
||||||
|
/class="index-selection-fullscreen-input"/.test(configPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'result panel exposes fullscreen action for JSON mapping content',
|
||||||
|
() =>
|
||||||
|
/@click="jsonMappingFullscreenVisible = true"/.test(resultPanelSource) &&
|
||||||
|
/title="JSON映射全屏查看"/.test(resultPanelSource) &&
|
||||||
|
/class="mapping-fullscreen-dialog mapping-fullscreen-dialog--json"/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'result panel exposes fullscreen action for XML mapping content',
|
||||||
|
() =>
|
||||||
|
/@click="xmlMappingFullscreenVisible = true"/.test(resultPanelSource) &&
|
||||||
|
/title="XML映射全屏查看"/.test(resultPanelSource) &&
|
||||||
|
/class="mapping-fullscreen-dialog mapping-fullscreen-dialog--xml"/.test(resultPanelSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('mmsMapping fullscreen actions contract failed:')
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.error(`- ${failure}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('mmsMapping fullscreen actions contract passed')
|
||||||
@@ -104,7 +104,14 @@ const checks = [
|
|||||||
['ICD JSON consistency API uses backend comparison endpoint', () => /checkIcdJsonConsistencyApi/.test(apiSource) && /\/api\/mms-mapping\/check-icd-json-consistency/.test(apiSource) && /interface\s+IcdJsonConsistencyCheckRequest/.test(typeSource) && /interface\s+IcdJsonConsistencyCheckResponse/.test(typeSource)],
|
['ICD JSON consistency API uses backend comparison endpoint', () => /checkIcdJsonConsistencyApi/.test(apiSource) && /\/api\/mms-mapping\/check-icd-json-consistency/.test(apiSource) && /interface\s+IcdJsonConsistencyCheckRequest/.test(typeSource) && /interface\s+IcdJsonConsistencyCheckResponse/.test(typeSource)],
|
||||||
['ICD path types are defined', () => /interface\s+IcdPathRecord/.test(typeSource) && /interface\s+CreateIcdPathRequest/.test(typeSource) && /interface\s+UpdateIcdPathRequest/.test(typeSource)],
|
['ICD path types are defined', () => /interface\s+IcdPathRecord/.test(typeSource) && /interface\s+CreateIcdPathRequest/.test(typeSource) && /interface\s+UpdateIcdPathRequest/.test(typeSource)],
|
||||||
['ICD path reference option type is defined', () => /interface\s+IcdPathReferenceOption[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string/.test(typeSource)],
|
['ICD path reference option type is defined', () => /interface\s+IcdPathReferenceOption[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string/.test(typeSource)],
|
||||||
['ICD path save requests include standard reference ID', () => /CreateIcdPathRequest[\s\S]*referenceIcdId\?:\s*string/.test(typeSource) && /buildSavePayload[\s\S]*referenceIcdId:\s*formModel\.referenceIcdId/.test(formDialogSource)],
|
[
|
||||||
|
'ICD path save requests include standard reference ID',
|
||||||
|
() =>
|
||||||
|
/CreateIcdPathRequest[\s\S]*referenceIcdId\?:\s*string/.test(typeSource) &&
|
||||||
|
/buildSavePayload[\s\S]*referenceIcdId:\s*isStandardIcdType\(formModel\.type\)\s*\?\s*''\s*:\s*formModel\.referenceIcdId/.test(
|
||||||
|
formDialogSource
|
||||||
|
)
|
||||||
|
],
|
||||||
['ICD path mapping detail response type is defined', () => /interface\s+IcdPathMappingDetailResponse[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string[\s\S]*jsonStr\?:\s*string[\s\S]*xmlStr\?:\s*string[\s\S]*icdText\?:\s*string/.test(typeSource)],
|
['ICD path mapping detail response type is defined', () => /interface\s+IcdPathMappingDetailResponse[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string[\s\S]*jsonStr\?:\s*string[\s\S]*xmlStr\?:\s*string[\s\S]*icdText\?:\s*string/.test(typeSource)],
|
||||||
[
|
[
|
||||||
'ICD path type options cover manual and upstream standard states',
|
'ICD path type options cover manual and upstream standard states',
|
||||||
@@ -119,7 +126,30 @@ const checks = [
|
|||||||
['ICD path type renders readable text in table', () => /getIcdTypeText\(scope\.row\.type\)/.test(pageSource)],
|
['ICD path type renders readable text in table', () => /getIcdTypeText\(scope\.row\.type\)/.test(pageSource)],
|
||||||
['ICD path type search uses select options', () => /prop:\s*'type'[\s\S]*enum:\s*icdTypeOptions[\s\S]*search:\s*\{[\s\S]*el:\s*'select'/.test(pageSource)],
|
['ICD path type search uses select options', () => /prop:\s*'type'[\s\S]*enum:\s*icdTypeOptions[\s\S]*search:\s*\{[\s\S]*el:\s*'select'/.test(pageSource)],
|
||||||
['ICD path form uses select for type', () => /<el-select\s+v-model="formModel\.type"[\s\S]*<el-option[\s\S]*v-for="option in icdTypeOptions"/.test(formDialogSource)],
|
['ICD path form uses select for type', () => /<el-select\s+v-model="formModel\.type"[\s\S]*<el-option[\s\S]*v-for="option in icdTypeOptions"/.test(formDialogSource)],
|
||||||
['ICD path form requires standard reference select after ICD type', () => /<el-form-item label="ICD类型"[\s\S]*<\/el-form-item>\s*<el-form-item label="标准ICD引用" prop="referenceIcdId">[\s\S]*<el-select\s+v-model="formModel\.referenceIcdId"[\s\S]*v-for="option in referenceOptions"[\s\S]*:label="option\.name"[\s\S]*:value="option\.id"/.test(formDialogSource) && /referenceIcdId:\s*\[\{\s*required:\s*true,\s*message:\s*'请选择标准 ICD 引用'/.test(formDialogSource)],
|
[
|
||||||
|
'ICD path form shows support angle and phase metric switches on one row',
|
||||||
|
() =>
|
||||||
|
/class="icd-path-form-dialog__switch-row"[\s\S]*<el-form-item label="支持相角"[\s\S]*v-model="formModel\.angle"[\s\S]*<el-form-item label="使用相别指标"[\s\S]*v-model="formModel\.usePhaseIndex"/.test(
|
||||||
|
formDialogSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD path form requires standard reference only for non-standard ICD types',
|
||||||
|
() =>
|
||||||
|
/<el-form-item label="ICD类型"[\s\S]*<\/el-form-item>\s*<el-form-item label="标准ICD引用" prop="referenceIcdId">[\s\S]*<el-select\s+v-model="formModel\.referenceIcdId"[\s\S]*v-for="option in referenceOptions"[\s\S]*:label="option\.name"[\s\S]*:value="option\.id"/.test(
|
||||||
|
formDialogSource
|
||||||
|
) &&
|
||||||
|
/isStandardIcdType\s*=\s*\(type\?:\s*number\)\s*=>\s*type\s*===\s*1\s*\|\|\s*type\s*===\s*3/.test(
|
||||||
|
formDialogSource
|
||||||
|
) &&
|
||||||
|
/isReferenceIcdRequired\s*=\s*computed\(\(\)\s*=>\s*!isStandardIcdType\(formModel\.type\)\)/.test(
|
||||||
|
formDialogSource
|
||||||
|
) &&
|
||||||
|
/referenceIcdId:\s*\[\s*\{\s*required:\s*isReferenceIcdRequired\.value/.test(formDialogSource) &&
|
||||||
|
/watch\(\s*\(\)\s*=>\s*formModel\.type[\s\S]*if\s*\(isStandardIcdType\(type\)\)\s*\{[\s\S]*formModel\.referenceIcdId\s*=\s*''/.test(
|
||||||
|
formDialogSource
|
||||||
|
)
|
||||||
|
],
|
||||||
['ICD path form loads and backfills standard reference ID', () => /listIcdPathReferencesApi/.test(formDialogSource) && /loadReferenceOptions/.test(formDialogSource) && /formModel\.referenceIcdId\s*=\s*record\.referenceIcdId\s*\|\|\s*''/.test(formDialogSource)],
|
['ICD path form loads and backfills standard reference ID', () => /listIcdPathReferencesApi/.test(formDialogSource) && /loadReferenceOptions/.test(formDialogSource) && /formModel\.referenceIcdId\s*=\s*record\.referenceIcdId\s*\|\|\s*''/.test(formDialogSource)],
|
||||||
['ICD path create and edit form uses single column layout', () => !/<el-col\s+:span="12"/.test(formDialogSource)],
|
['ICD path create and edit form uses single column layout', () => !/<el-col\s+:span="12"/.test(formDialogSource)],
|
||||||
['new ICD path defaults to upstream parsed non-standard type', () => /type:\s*4/.test(formDialogSource) && /formModel\.type\s*=\s*4/.test(formDialogSource)],
|
['new ICD path defaults to upstream parsed non-standard type', () => /type:\s*4/.test(formDialogSource) && /formModel\.type\s*=\s*4/.test(formDialogSource)],
|
||||||
@@ -313,6 +343,29 @@ const checks = [
|
|||||||
/icdDocument:\s*parsedIcdDocument\.value\s*\|\|\s*responsePayload\.value\?\.icdDocument/.test(flowSource)
|
/icdDocument:\s*parsedIcdDocument\.value\s*\|\|\s*responsePayload\.value\?\.icdDocument/.test(flowSource)
|
||||||
],
|
],
|
||||||
['mapping result panel has no mojibake text', () => !/[鏄搴褰鍙纭鏍閰獙][\s\S]*[犲垪撳栨疆]/.test(resultPanelSource)],
|
['mapping result panel has no mojibake text', () => !/[鏄搴褰鍙纭鏍閰獙][\s\S]*[犲垪撳栨疆]/.test(resultPanelSource)],
|
||||||
|
[
|
||||||
|
'mapping result panel supports editable JSON and XML mapping content',
|
||||||
|
() =>
|
||||||
|
/update-mapping-json/.test(resultPanelSource) &&
|
||||||
|
/update-xml-mapping/.test(resultPanelSource) &&
|
||||||
|
/editable/.test(resultPanelSource) &&
|
||||||
|
!/<pre class="xml-file-content">/.test(resultPanelSource) &&
|
||||||
|
/<el-input[\s\S]*type="textarea"[\s\S]*xmlMappingDraft/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'JSON mapping expand and collapse actions are icon-only tooltips',
|
||||||
|
() => {
|
||||||
|
const jsonTreeFile = path.resolve(rootDir, 'views/tools/mmsMapping/components/JsonMappingTree.vue')
|
||||||
|
const jsonTreeSource = exists(jsonTreeFile) ? read(jsonTreeFile) : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
/<el-tooltip[\s\S]*content="全部展开"[\s\S]*<el-button[\s\S]*:icon="Expand"/.test(jsonTreeSource) &&
|
||||||
|
/<el-tooltip[\s\S]*content="全部收起"[\s\S]*<el-button[\s\S]*:icon="Fold"/.test(jsonTreeSource) &&
|
||||||
|
!/>全部展开<\/el-button>/.test(jsonTreeSource) &&
|
||||||
|
!/>全部收起<\/el-button>/.test(jsonTreeSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
['flow supports injected ICD check save handler', () => /saveIcdCheckResult\?:/.test(flowSource) && /options\.saveIcdCheckResult/.test(flowSource)],
|
['flow supports injected ICD check save handler', () => /saveIcdCheckResult\?:/.test(flowSource) && /options\.saveIcdCheckResult/.test(flowSource)],
|
||||||
['form dialog follows ICD check dialog visual shell', () => /class="icd-check-dialog"/.test(formDialogSource) && /class="icd-check-dialog__body"/.test(formDialogSource)],
|
['form dialog follows ICD check dialog visual shell', () => /class="icd-check-dialog"/.test(formDialogSource) && /class="icd-check-dialog__body"/.test(formDialogSource)],
|
||||||
['check dialog reuses shared MMS mapping flow panels', () => /<MappingRequestPanel/.test(checkDialogSource) && /<MappingConfigPanel/.test(checkDialogSource) && /<MappingResultPanel/.test(checkDialogSource) && /<MappingConfirmDialog/.test(checkDialogSource)]
|
['check dialog reuses shared MMS mapping flow panels', () => /<MappingRequestPanel/.test(checkDialogSource) && /<MappingConfigPanel/.test(checkDialogSource) && /<MappingResultPanel/.test(checkDialogSource) && /<MappingConfirmDialog/.test(checkDialogSource)]
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ const checks = [
|
|||||||
'ICD type column is widened to avoid text clipping',
|
'ICD type column is widened to avoid text clipping',
|
||||||
() => /prop: 'type'[\s\S]*?label: 'ICD类型'[\s\S]*?width: 220/.test(columnsSource)
|
() => /prop: 'type'[\s\S]*?label: 'ICD类型'[\s\S]*?width: 220/.test(columnsSource)
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'Angle and phase metric columns use requested labels',
|
||||||
|
() =>
|
||||||
|
/prop: 'angle'[\s\S]*?label: '支持相角'[\s\S]*?width: 90/.test(columnsSource) &&
|
||||||
|
/prop: 'usePhaseIndex'[\s\S]*?label: '使用相别指标'[\s\S]*?width: 130/.test(columnsSource)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'Mapping detail column uses width 150',
|
'Mapping detail column uses width 150',
|
||||||
() => /prop: 'mappingDetail'[\s\S]*?label: '映射文件详情'[\s\S]*?width: 150/.test(columnsSource)
|
() => /prop: 'mappingDetail'[\s\S]*?label: '映射文件详情'[\s\S]*?width: 150/.test(columnsSource)
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ const source = fs.readFileSync(panelFile, 'utf8')
|
|||||||
|
|
||||||
const checks = [
|
const checks = [
|
||||||
['JSON problem button uses dedicated JSON mapping issue text', () => /problemButtonText/.test(source)],
|
['JSON problem button uses dedicated JSON mapping issue text', () => /problemButtonText/.test(source)],
|
||||||
|
['JSON result label uses updated JSON match text', () => source.includes('JSON匹配结果')],
|
||||||
|
['JSON export button uses shortened download text', () => source.includes('下载JSON')],
|
||||||
|
['Old JSON mapping issue list text is removed', () => !source.includes('JSON映射问题列表')],
|
||||||
|
['XML match result label uses updated XML match text', () => source.includes('XML匹配结果')],
|
||||||
|
['XML export button uses shortened download text', () => source.includes('下载XML')],
|
||||||
|
['Old XML mapping match result list text is removed', () => !source.includes('XML映射匹配结果列表')],
|
||||||
['Old generic problem dialog title is removed', () => !source.includes('title="问题列表"')],
|
['Old generic problem dialog title is removed', () => !source.includes('title="问题列表"')],
|
||||||
['XML match result action opens dedicated XML match dialog', () => /@click="matchResultDialogVisible = true"/.test(source)],
|
['XML match result action opens dedicated XML match dialog', () => /@click="matchResultDialogVisible = true"/.test(source)],
|
||||||
['Old XML match result display text is removed', () => !source.includes('匹配结果展示')],
|
['Old XML match result display text is removed', () => !source.includes('匹配结果展示')],
|
||||||
|
|||||||
@@ -496,11 +496,11 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ prop: 'angle', label: '角度', width: 90 },
|
{ prop: 'angle', label: '支持相角', width: 90 },
|
||||||
{
|
{
|
||||||
prop: 'usePhaseIndex',
|
prop: 'usePhaseIndex',
|
||||||
label: '相位索引',
|
label: '使用相别指标',
|
||||||
width: 110,
|
width: 130,
|
||||||
render: scope => getUsePhaseIndexText(scope.row.usePhaseIndex)
|
render: scope => getUsePhaseIndexText(scope.row.usePhaseIndex)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -817,10 +817,33 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
reviewedIcdConsistencyProblemList.value = []
|
reviewedIcdConsistencyProblemList.value = []
|
||||||
hasConfirmedIcdConsistencyProblems.value = false
|
hasConfirmedIcdConsistencyProblems.value = false
|
||||||
shouldOpenIcdConsistencyProblems.value = 0
|
shouldOpenIcdConsistencyProblems.value = 0
|
||||||
hasSequenceConfigured.value = false
|
|
||||||
activeResultTab.value = 'json'
|
activeResultTab.value = 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpdateXmlMapping = (xml: string) => {
|
||||||
|
if (!xmlResponsePayload.value) return
|
||||||
|
|
||||||
|
const xmlFile = xmlResponsePayload.value.xmlFile
|
||||||
|
|
||||||
|
// 关键业务节点:XML 结果允许人工修正,保存和下载必须使用编辑后的内容。
|
||||||
|
xmlResponsePayload.value = {
|
||||||
|
...xmlResponsePayload.value,
|
||||||
|
xmlFile: xmlFile
|
||||||
|
? {
|
||||||
|
...xmlFile,
|
||||||
|
content: xml
|
||||||
|
}
|
||||||
|
: xmlResponsePayload.value.xmlFile,
|
||||||
|
mappingXml: xml,
|
||||||
|
xmlContent: xml,
|
||||||
|
xmlText: xml
|
||||||
|
}
|
||||||
|
lastIcdConsistencyCheckResult.value = null
|
||||||
|
reviewedIcdConsistencyProblemList.value = []
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = false
|
||||||
|
shouldOpenIcdConsistencyProblems.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
const handleSequenceConfigComplete = () => {
|
const handleSequenceConfigComplete = () => {
|
||||||
hasSequenceConfigured.value = true
|
hasSequenceConfigured.value = true
|
||||||
}
|
}
|
||||||
@@ -894,6 +917,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
handleConfirmIcdConsistencyProblems,
|
handleConfirmIcdConsistencyProblems,
|
||||||
handleRemoveIcdConsistencyProblem,
|
handleRemoveIcdConsistencyProblem,
|
||||||
handleUpdateMappingJson,
|
handleUpdateMappingJson,
|
||||||
|
handleUpdateXmlMapping,
|
||||||
handleSequenceConfigComplete,
|
handleSequenceConfigComplete,
|
||||||
handleSaveIcdCheckResult,
|
handleSaveIcdCheckResult,
|
||||||
handleConfirmIndexSelection
|
handleConfirmIndexSelection
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
>
|
>
|
||||||
JSON映射
|
JSON映射
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="primary" plain :icon="FullScreen" @click="indexSelectionFullscreenVisible = true">
|
||||||
|
全屏查看
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,11 +51,34 @@
|
|||||||
|
|
||||||
<el-empty v-if="!hasDefaultJson" :description="emptyDescription" />
|
<el-empty v-if="!hasDefaultJson" :description="emptyDescription" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="indexSelectionFullscreenVisible"
|
||||||
|
class="mapping-fullscreen-dialog mapping-fullscreen-dialog--index"
|
||||||
|
title="索引配置全屏查看"
|
||||||
|
fullscreen
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div class="mapping-fullscreen-body">
|
||||||
|
<el-alert v-if="jsonError" :title="jsonError" type="error" :closable="false" class="json-alert" />
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
class="index-selection-fullscreen-input"
|
||||||
|
:model-value="indexSelectionJson"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
resize="none"
|
||||||
|
placeholder="索引配置完成后,这里会自动回填索引配置,仍可继续直接编辑。"
|
||||||
|
@update:model-value="value => emit('update:indexSelectionJson', String(value || ''))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Connection, EditPen } from '@element-plus/icons-vue'
|
import { Connection, EditPen, FullScreen } from '@element-plus/icons-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MappingConfigPanel'
|
name: 'MappingConfigPanel'
|
||||||
@@ -80,6 +106,8 @@ const emit = defineEmits<{
|
|||||||
(event: 'confirm-config'): void
|
(event: 'confirm-config'): void
|
||||||
(event: 'generate'): void
|
(event: 'generate'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const indexSelectionFullscreenVisible = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -202,6 +230,37 @@ const emit = defineEmits<{
|
|||||||
margin: 16px 0 0;
|
margin: 16px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.mapping-fullscreen-dialog .el-dialog__body) {
|
||||||
|
height: calc(100vh - 54px);
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-fullscreen-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-selection-fullscreen-input {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-selection-fullscreen-input :deep(.el-textarea) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-selection-fullscreen-input :deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mapping-panel {
|
.mapping-panel {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
:content="
|
:content="
|
||||||
icdConsistencyStatus === 'success'
|
icdConsistencyStatus === 'success'
|
||||||
? 'ICD校验通过'
|
? 'ICD校验通过'
|
||||||
: `ICD校验不通过,点击查看JSON映射问题列表(${icdConsistencyProblemCount}条)`
|
: `ICD校验不通过,点击查看JSON匹配结果(${icdConsistencyProblemCount}条)`
|
||||||
"
|
"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
@@ -101,6 +101,8 @@
|
|||||||
v-if="mappingJsonPreview"
|
v-if="mappingJsonPreview"
|
||||||
:source="mappingJsonPreview"
|
:source="mappingJsonPreview"
|
||||||
:meta-text="mappingMetaText"
|
:meta-text="mappingMetaText"
|
||||||
|
editable
|
||||||
|
@update:source="emit('update-mapping-json', $event)"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -124,6 +126,16 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template #trailing-actions>
|
<template #trailing-actions>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="FullScreen"
|
||||||
|
:disabled="!mappingJsonPreview"
|
||||||
|
@click="jsonMappingFullscreenVisible = true"
|
||||||
|
>
|
||||||
|
全屏查看
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
@@ -132,7 +144,7 @@
|
|||||||
:disabled="!canExportJsonMapping"
|
:disabled="!canExportJsonMapping"
|
||||||
@click="emit('export-mapping', 'json')"
|
@click="emit('export-mapping', 'json')"
|
||||||
>
|
>
|
||||||
下载JSON映射
|
下载JSON
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</JsonMappingTree>
|
</JsonMappingTree>
|
||||||
@@ -145,6 +157,16 @@
|
|||||||
<div class="xml-preview-toolbar">
|
<div class="xml-preview-toolbar">
|
||||||
<div class="xml-preview-meta">{{ xmlMetaText }}</div>
|
<div class="xml-preview-meta">{{ xmlMetaText }}</div>
|
||||||
<div class="xml-preview-actions">
|
<div class="xml-preview-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="FullScreen"
|
||||||
|
:disabled="!xmlMappingPreview"
|
||||||
|
@click="xmlMappingFullscreenVisible = true"
|
||||||
|
>
|
||||||
|
全屏查看
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
@@ -153,7 +175,7 @@
|
|||||||
:disabled="!methodDescribe"
|
:disabled="!methodDescribe"
|
||||||
@click="matchResultDialogVisible = true"
|
@click="matchResultDialogVisible = true"
|
||||||
>
|
>
|
||||||
XML映射匹配结果列表
|
XML匹配结果
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -163,11 +185,18 @@
|
|||||||
:disabled="!canExportXmlMapping"
|
:disabled="!canExportXmlMapping"
|
||||||
@click="emit('export-mapping', 'xml')"
|
@click="emit('export-mapping', 'xml')"
|
||||||
>
|
>
|
||||||
下载XML映射
|
下载XML
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="xml-file-content">{{ xmlMappingPreview }}</pre>
|
<el-input
|
||||||
|
v-model="xmlMappingDraft"
|
||||||
|
type="textarea"
|
||||||
|
resize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
class="xml-file-content"
|
||||||
|
@input="handleXmlMappingInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else :description="xmlEmptyText" />
|
<el-empty v-else :description="xmlEmptyText" />
|
||||||
</div>
|
</div>
|
||||||
@@ -178,7 +207,7 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="problemDialogVisible"
|
v-model="problemDialogVisible"
|
||||||
title="JSON映射问题列表"
|
title="JSON匹配结果"
|
||||||
width="720px"
|
width="720px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
top="8vh"
|
top="8vh"
|
||||||
@@ -229,7 +258,7 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="matchResultDialogVisible"
|
v-model="matchResultDialogVisible"
|
||||||
title="XML映射匹配结果列表"
|
title="XML匹配结果"
|
||||||
width="640px"
|
width="640px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
top="12vh"
|
top="12vh"
|
||||||
@@ -239,6 +268,108 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="jsonMappingFullscreenVisible"
|
||||||
|
class="mapping-fullscreen-dialog mapping-fullscreen-dialog--json"
|
||||||
|
title="JSON映射全屏查看"
|
||||||
|
fullscreen
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div class="mapping-fullscreen-body">
|
||||||
|
<JsonMappingTree
|
||||||
|
v-if="mappingJsonPreview"
|
||||||
|
:source="mappingJsonPreview"
|
||||||
|
:meta-text="mappingMetaText"
|
||||||
|
editable
|
||||||
|
@update:source="emit('update-mapping-json', $event)"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Warning"
|
||||||
|
@click="problemDialogVisible = true"
|
||||||
|
>
|
||||||
|
{{ problemButtonText }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="icdConsistencyStatus"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="WarningFilled"
|
||||||
|
@click="openIcdConsistencyProblems"
|
||||||
|
>
|
||||||
|
{{ icdConsistencyProblemButtonText }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #trailing-actions>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Download"
|
||||||
|
:disabled="!canExportJsonMapping"
|
||||||
|
@click="emit('export-mapping', 'json')"
|
||||||
|
>
|
||||||
|
下载JSON
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</JsonMappingTree>
|
||||||
|
<el-empty v-else description="当前返回未包含 mappingJson" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="xmlMappingFullscreenVisible"
|
||||||
|
class="mapping-fullscreen-dialog mapping-fullscreen-dialog--xml"
|
||||||
|
title="XML映射全屏查看"
|
||||||
|
fullscreen
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div class="mapping-fullscreen-body">
|
||||||
|
<div v-if="xmlMappingPreview" class="xml-preview-viewer">
|
||||||
|
<div class="xml-preview-toolbar">
|
||||||
|
<div class="xml-preview-meta">{{ xmlMetaText }}</div>
|
||||||
|
<div class="xml-preview-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Document"
|
||||||
|
:disabled="!methodDescribe"
|
||||||
|
@click="matchResultDialogVisible = true"
|
||||||
|
>
|
||||||
|
XML匹配结果
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:icon="Download"
|
||||||
|
:disabled="!canExportXmlMapping"
|
||||||
|
@click="emit('export-mapping', 'xml')"
|
||||||
|
>
|
||||||
|
下载XML
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="xmlMappingDraft"
|
||||||
|
type="textarea"
|
||||||
|
resize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
class="xml-file-content"
|
||||||
|
@input="handleXmlMappingInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else :description="xmlEmptyText" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="xmlDataTypeDialogVisible"
|
v-model="xmlDataTypeDialogVisible"
|
||||||
title="XML映射类型"
|
title="XML映射类型"
|
||||||
@@ -345,6 +476,7 @@ import {
|
|||||||
Document,
|
Document,
|
||||||
Download,
|
Download,
|
||||||
Finished,
|
Finished,
|
||||||
|
FullScreen,
|
||||||
Search,
|
Search,
|
||||||
Setting,
|
Setting,
|
||||||
Warning,
|
Warning,
|
||||||
@@ -436,6 +568,7 @@ const emit = defineEmits<{
|
|||||||
(event: 'export-mapping', value: ExportMappingType): void
|
(event: 'export-mapping', value: ExportMappingType): void
|
||||||
(event: 'generate-xml-mapping', value: number): void
|
(event: 'generate-xml-mapping', value: number): void
|
||||||
(event: 'update-mapping-json', value: string): void
|
(event: 'update-mapping-json', value: string): void
|
||||||
|
(event: 'update-xml-mapping', value: string): void
|
||||||
(event: 'update:sequenceDialogVisible', value: boolean): void
|
(event: 'update:sequenceDialogVisible', value: boolean): void
|
||||||
(event: 'sequence-config-complete'): void
|
(event: 'sequence-config-complete'): void
|
||||||
(event: 'icd-check'): void
|
(event: 'icd-check'): void
|
||||||
@@ -449,7 +582,7 @@ const activeTabProxy = computed({
|
|||||||
set: value => emit('update:activeResultTab', value)
|
set: value => emit('update:activeResultTab', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const JSON_MAPPING_PROBLEM_LABEL = 'JSON\u6620\u5c04\u95ee\u9898\u5217\u8868'
|
const JSON_MAPPING_PROBLEM_LABEL = 'JSON\u5339\u914d\u7ed3\u679c'
|
||||||
const ICD_CONSISTENCY_PROBLEM_LABEL = 'ICD\u6821\u9a8c\u95ee\u9898\u5217\u8868'
|
const ICD_CONSISTENCY_PROBLEM_LABEL = 'ICD\u6821\u9a8c\u95ee\u9898\u5217\u8868'
|
||||||
const problemButtonText = computed(() =>
|
const problemButtonText = computed(() =>
|
||||||
props.problemList.length ? `${JSON_MAPPING_PROBLEM_LABEL} (${props.problemList.length})` : JSON_MAPPING_PROBLEM_LABEL
|
props.problemList.length ? `${JSON_MAPPING_PROBLEM_LABEL} (${props.problemList.length})` : JSON_MAPPING_PROBLEM_LABEL
|
||||||
@@ -463,10 +596,13 @@ const icdConsistencyProblemCount = computed(() => props.icdConsistencyProblemLis
|
|||||||
const problemDialogVisible = ref(false)
|
const problemDialogVisible = ref(false)
|
||||||
const icdConsistencyProblemDialogVisible = ref(false)
|
const icdConsistencyProblemDialogVisible = ref(false)
|
||||||
const matchResultDialogVisible = ref(false)
|
const matchResultDialogVisible = ref(false)
|
||||||
|
const jsonMappingFullscreenVisible = ref(false)
|
||||||
|
const xmlMappingFullscreenVisible = ref(false)
|
||||||
const xmlDataTypeDialogVisible = ref(false)
|
const xmlDataTypeDialogVisible = ref(false)
|
||||||
const xmlDataType = ref<number>(2)
|
const xmlDataType = ref<number>(2)
|
||||||
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
||||||
const sequenceSearchKeyword = ref('')
|
const sequenceSearchKeyword = ref('')
|
||||||
|
const xmlMappingDraft = ref('')
|
||||||
const sequenceDialogVisible = computed({
|
const sequenceDialogVisible = computed({
|
||||||
get: () => props.sequenceDialogVisible,
|
get: () => props.sequenceDialogVisible,
|
||||||
set: value => emit('update:sequenceDialogVisible', value)
|
set: value => emit('update:sequenceDialogVisible', value)
|
||||||
@@ -671,6 +807,18 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.xmlMappingPreview,
|
||||||
|
value => {
|
||||||
|
xmlMappingDraft.value = value
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleXmlMappingInput = (value: string | number) => {
|
||||||
|
emit('update-xml-mapping', String(value))
|
||||||
|
}
|
||||||
|
|
||||||
const confirmSequenceConfig = () => {
|
const confirmSequenceConfig = () => {
|
||||||
try {
|
try {
|
||||||
const nextJson = JSON.parse(props.mappingJsonPreview) as unknown
|
const nextJson = JSON.parse(props.mappingJsonPreview) as unknown
|
||||||
@@ -918,14 +1066,22 @@ const confirmSequenceConfig = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.xml-file-content {
|
.xml-file-content {
|
||||||
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xml-file-content :deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #dbe3f0;
|
border: 1px solid #dbe3f0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
box-shadow: none;
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
@@ -1181,6 +1337,18 @@ const confirmSequenceConfig = () => {
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.mapping-fullscreen-dialog .el-dialog__body) {
|
||||||
|
height: calc(100vh - 54px);
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-fullscreen-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mapping-panel {
|
.mapping-panel {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user