refactor(steady-checksquare): 更新稳态校验功能的数据结构和接口规范
- 移除 lineId 字段,统一使用 lineIds 数组字段 - 更新创建任务请求体构建逻辑,不再包含 lineId 属性 - 修改任务详情和检测项明细的接口契约验证 - 更新稳态校验 API 文档说明,移除对 lineId 的支持 - 重构稳态校验表单项验证规则 - 移除磁盘监控作业详情抽屉组件 - 移除磁盘监控作业表格组件 - 更新路由配置以支持新的系统监控路径 - 添加测试报告相关字典代码常量 - 更新 ICD 路径表单和结果面板功能
This commit is contained in:
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 {
|
||||
lineId?: string
|
||||
lineIds?: string[]
|
||||
lineIds: string[]
|
||||
indicatorCodes: string[]
|
||||
timeStart: string
|
||||
timeEnd: string
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const DICT_CODES = {
|
||||
USER_STATE: 'state',
|
||||
EVENT_TYPE: 'event_type',
|
||||
TEST_REPORT_COMPANY: 'test_report_company',
|
||||
TEST_REPORT_STANDARD: 'test_report_standard',
|
||||
LEDGER_DEVICE_TYPE: 'ledger_device_type',
|
||||
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type',
|
||||
LEDGER_TERMINAL_MODEL: 'Dev_Type',
|
||||
|
||||
@@ -17,8 +17,8 @@ const routeContracts = [
|
||||
['toolAddData', 'src/views/tools/addData/index.vue'],
|
||||
['toolAddLedger', 'src/views/tools/addLedger/index.vue'],
|
||||
['eventList', 'src/views/event/eventList/index.vue'],
|
||||
['systemMonitor', 'src/views/systemMonitor/index.vue'],
|
||||
['diskMonitor', 'src/views/systemMonitor/diskMonitor/index.vue']
|
||||
['system-monitor', 'src/views/system-monitor/index.vue'],
|
||||
['diskMonitor', 'src/views/system-monitor/diskMonitor/index.vue']
|
||||
]
|
||||
|
||||
const extractRouteBlock = routeName => {
|
||||
|
||||
@@ -9,6 +9,12 @@ const staticRouterSource = read('routers/modules/staticRouter.ts')
|
||||
const authStoreSource = read('stores/modules/auth.ts')
|
||||
|
||||
const expectedPaths = [
|
||||
'/system-monitor/dbms',
|
||||
'/system-monitor/dbms/index',
|
||||
'/system-monitor/databaseMonitor',
|
||||
'/system-monitor/databaseMonitor/index',
|
||||
'/system-monitor/database-monitor',
|
||||
'/system-monitor/database-monitor/index',
|
||||
'/systemMonitor/dbms',
|
||||
'/systemMonitor/dbms/index',
|
||||
'/systemMonitor/databaseMonitor',
|
||||
|
||||
@@ -27,6 +27,12 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
||||
'/steady/check-square': '/steady/checksquare',
|
||||
'/steady/check-square/index': '/steady/checksquare/index',
|
||||
// 数据库监控菜单统一落到 system-ops/dbms 页面,兼容后端菜单常见 component 写法。
|
||||
'/system-monitor/dbms': '/system-ops/dbms',
|
||||
'/system-monitor/dbms/index': '/system-ops/dbms/index',
|
||||
'/system-monitor/databaseMonitor': '/system-ops/dbms',
|
||||
'/system-monitor/databaseMonitor/index': '/system-ops/dbms/index',
|
||||
'/system-monitor/database-monitor': '/system-ops/dbms',
|
||||
'/system-monitor/database-monitor/index': '/system-ops/dbms/index',
|
||||
'/systemMonitor/dbms': '/system-ops/dbms',
|
||||
'/systemMonitor/dbms/index': '/system-ops/dbms/index',
|
||||
'/systemMonitor/databaseMonitor': '/system-ops/dbms',
|
||||
@@ -34,7 +40,20 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
||||
'/systemMonitor/database-monitor': '/system-ops/dbms',
|
||||
'/systemMonitor/database-monitor/index': '/system-ops/dbms/index',
|
||||
'/system-ops/database-monitor': '/system-ops/dbms',
|
||||
'/system-ops/database-monitor/index': '/system-ops/dbms/index'
|
||||
'/system-ops/database-monitor/index': '/system-ops/dbms/index',
|
||||
// 报告菜单后端使用 aiReport,前端目录沿用既有 aireport 命名。
|
||||
'/aiReport/reportTask': '/aireport/testReport',
|
||||
'/aiReport/reportTask/index': '/aireport/testReport/index',
|
||||
'/aiReport/testReport': '/aireport/testReport',
|
||||
'/aiReport/testReport/index': '/aireport/testReport/index',
|
||||
'/aiReport/reportModel': '/aireport/reportModel',
|
||||
'/aiReport/reportModel/index': '/aireport/reportModel/index',
|
||||
'/aiReport/clientUnit': '/aireport/clientUnit',
|
||||
'/aiReport/clientUnit/index': '/aireport/clientUnit/index',
|
||||
'/aiReport/testDevice': '/aireport/testDevice',
|
||||
'/aiReport/testDevice/index': '/aireport/testDevice/index',
|
||||
'/aiReport/reportApproval': '/aireport/reportApproval',
|
||||
'/aiReport/reportApproval/index': '/aireport/reportApproval/index'
|
||||
}
|
||||
const STATIC_ROUTE_NAMES = new Set([
|
||||
'layout',
|
||||
@@ -51,6 +70,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
||||
'steadyDataView',
|
||||
'steadyTrend',
|
||||
'checksquare',
|
||||
'system-monitor',
|
||||
'systemMonitor',
|
||||
'diskMonitor',
|
||||
'systemOpsDbms',
|
||||
|
||||
@@ -195,24 +195,26 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/systemMonitor',
|
||||
name: 'systemMonitor',
|
||||
component: () => import('@/views/systemMonitor/index.vue'),
|
||||
path: '/system-monitor',
|
||||
name: 'system-monitor',
|
||||
alias: ['/systemMonitor'],
|
||||
component: () => import('@/views/system-monitor/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'SystemMonitorPage',
|
||||
title: '系统监控'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/systemMonitor/diskMonitor',
|
||||
path: '/system-monitor/diskMonitor',
|
||||
name: 'diskMonitor',
|
||||
component: () => import('@/views/systemMonitor/diskMonitor/index.vue'),
|
||||
alias: ['/systemMonitor/diskMonitor'],
|
||||
component: () => import('@/views/system-monitor/diskMonitor/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'DiskMonitorPage',
|
||||
// 磁盘监控页复用系统监控主标签
|
||||
activeMenu: '/systemMonitor',
|
||||
activeMenu: '/system-monitor',
|
||||
hideTab: true,
|
||||
parentPath: '/systemMonitor',
|
||||
parentPath: '/system-monitor',
|
||||
title: '磁盘监控'
|
||||
}
|
||||
},
|
||||
@@ -220,6 +222,12 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
path: '/system-ops/dbms',
|
||||
name: 'systemOpsDbms',
|
||||
alias: [
|
||||
'/system-monitor/dbms',
|
||||
'/system-monitor/dbms/index',
|
||||
'/system-monitor/databaseMonitor',
|
||||
'/system-monitor/databaseMonitor/index',
|
||||
'/system-monitor/database-monitor',
|
||||
'/system-monitor/database-monitor/index',
|
||||
'/systemMonitor/dbms',
|
||||
'/systemMonitor/dbms/index',
|
||||
'/systemMonitor/databaseMonitor',
|
||||
|
||||
@@ -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/checksquare`
|
||||
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的业务数据字段 `data`。
|
||||
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的 `data`
|
||||
- 时间格式:`yyyy-MM-dd HH:mm:ss`
|
||||
- Content-Type:`application/json`
|
||||
|
||||
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询、失败任务重启和任务删除能力。当前标准支持单监测点和多监测点任务:单监测点可继续传 `lineId`,多监测点推荐传 `lineIds`。
|
||||
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询、失败任务重启和任务删除能力。
|
||||
|
||||
## 2. 通用约定
|
||||
当前标准统一使用 `lineIds` 表示任务监测点列表。单监测点也传单元素数组,例如:
|
||||
|
||||
### 2.1 请求头
|
||||
```json
|
||||
{
|
||||
"lineIds": ["LINE_001"]
|
||||
}
|
||||
```
|
||||
|
||||
| 名称 | 示例 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Content-Type` | `application/json` | `POST` 请求使用 JSON 请求体 |
|
||||
| `Authorization` | 登录态 Token | 如当前环境开启认证,需携带现有登录接口返回的认证信息 |
|
||||
## 2. 通用枚举
|
||||
|
||||
### 2.2 任务状态
|
||||
### 2.1 任务状态
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -26,7 +28,7 @@
|
||||
| `SUCCESS` | 执行成功 |
|
||||
| `FAIL` | 执行失败 |
|
||||
|
||||
### 2.3 明细类型
|
||||
### 2.2 明细类型
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -34,7 +36,7 @@
|
||||
| `VALUE_ORDER` | 指标值大小关系异常明细 |
|
||||
| `HARMONIC_PARITY` | 谐波奇偶关系异常明细 |
|
||||
|
||||
### 2.4 统计类型
|
||||
### 2.3 统计类型
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -43,19 +45,17 @@
|
||||
| `MIN` | 最小值 |
|
||||
| `CP95` | CP95 值 |
|
||||
|
||||
## 3. 调试顺序建议
|
||||
## 3. 调试顺序
|
||||
|
||||
1. 调用 `POST /steady/checksquare/create`:按监测点列表、时间范围和指标列表创建或复用任务。
|
||||
2. 读取 `/create` 返回的 `data.taskId`:该返回值是任务列表中的行信息。
|
||||
1. 调用 `POST /steady/checksquare/create`,按监测点列表、时间范围和指标列表创建或复用任务。
|
||||
2. 读取 `/create` 返回的 `data.taskId`。
|
||||
3. 如果 `taskStatus=RUNNING`,轮询 `POST /steady/checksquare/query`,或稍后调用 `GET /steady/checksquare/detail` 查看任务详情。
|
||||
4. 调用 `GET /steady/checksquare/detail`:用 `taskId` 查询任务下的检测项。
|
||||
5. 读取 `/detail` 返回的 `items[].itemId`:按检测项继续查询连续性区间或异常明细。
|
||||
6. 调用 `GET /steady/checksquare/item-detail`:按 `itemId + detailType` 查询具体明细。
|
||||
4. 调用 `GET /steady/checksquare/detail`,用 `taskId` 查询任务下的检测项。
|
||||
5. 读取 `/detail` 返回的 `items[].itemId`。
|
||||
6. 调用 `GET /steady/checksquare/item-detail`,按 `itemId + detailType` 查询具体明细。
|
||||
7. 任务失败后如需重新执行,调用 `POST /steady/checksquare/restart`。
|
||||
8. 需要清理任务时调用 `POST /steady/checksquare/delete`。
|
||||
|
||||
注意:旧的获取或创建兼容接口已移除。创建或复用任务的逻辑已经合并到 `/create` 中。
|
||||
|
||||
## 4. 创建或复用任务
|
||||
|
||||
### 4.1 接口
|
||||
@@ -64,28 +64,27 @@
|
||||
|
||||
### 4.2 行为说明
|
||||
|
||||
接口开始执行前,会先按 `lineIds + timeStart + timeEnd` 查询是否存在未删除任务:
|
||||
接口执行前会先按 `lineIds + timeStart + timeEnd` 查询是否存在未删除任务:
|
||||
|
||||
- 已存在:直接返回该任务的任务列表行信息。
|
||||
- 不存在:创建 `RUNNING` 任务并立即返回任务列表行信息,后台继续执行校验;任务完成后状态更新为 `SUCCESS`,失败时更新为 `FAIL`。
|
||||
- 已存在:直接返回该任务的任务列表行信息,不生成重复任务。
|
||||
- 不存在:创建 `RUNNING` 任务并立即返回任务列表行信息,后台继续执行校验。
|
||||
|
||||
`lineIds` 会去重并清理空字符串。若未传 `lineIds`,后端会兼容读取 `lineId` 并转换为单元素监测点列表。返回对象为 `SteadyChecksquareTaskVO`。
|
||||
`lineIds` 会去重并清理空字符串。任务执行成功后状态更新为 `SUCCESS`,失败时更新为 `FAIL`。
|
||||
|
||||
### 4.3 请求字段
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `lineId` | `String` | 条件必填 | 单监测点 ID;当 `lineIds` 为空时使用 |
|
||||
| `lineIds` | `Array<String>` | 条件必填 | 监测点 ID 列表;多监测点推荐使用该字段 |
|
||||
| `lineIds` | `Array<String>` | 是 | 监测点 ID 列表;单监测点也传单元素数组 |
|
||||
| `indicatorCodes` | `Array<String>` | 是 | 指标编码列表 |
|
||||
| `timeStart` | `String` | 是 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `timeEnd` | `String` | 是 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
`lineId` 与 `lineIds` 至少传一个;`indicatorCodes` 是数组,不要传成单个字符串。
|
||||
`indicatorCodes` 是数组,不要传成单个字符串。
|
||||
|
||||
### 4.4 请求示例
|
||||
|
||||
单监测点兼容写法:
|
||||
单监测点:
|
||||
|
||||
```http
|
||||
POST /steady/checksquare/create
|
||||
@@ -94,14 +93,14 @@ Content-Type: application/json
|
||||
|
||||
```json
|
||||
{
|
||||
"lineId": "LINE_001",
|
||||
"lineIds": ["LINE_001"],
|
||||
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
|
||||
"timeStart": "2026-06-18 00:00:00",
|
||||
"timeEnd": "2026-06-18 01:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
多监测点标准写法:
|
||||
多监测点:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -118,7 +117,6 @@ Content-Type: application/json
|
||||
| --- | --- | --- |
|
||||
| `taskId` | `String` | 任务 ID |
|
||||
| `taskNo` | `String` | 任务编号 |
|
||||
| `lineId` | `String` | 任务首个监测点 ID;兼容旧页面字段 |
|
||||
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||
| `lineName` | `String` | 监测点名称;多监测点时为多个名称拼接结果 |
|
||||
| `timeStart` | `String` | 开始时间 |
|
||||
@@ -139,7 +137,6 @@ Content-Type: application/json
|
||||
"data": {
|
||||
"taskId": "1812345678901234567",
|
||||
"taskNo": "CS202606180001",
|
||||
"lineId": "LINE_001",
|
||||
"lineIds": ["LINE_001", "LINE_002"],
|
||||
"lineName": "1号监测点,2号监测点",
|
||||
"timeStart": "2026-06-18 00:00:00",
|
||||
@@ -216,7 +213,6 @@ Content-Type: application/json
|
||||
{
|
||||
"taskId": "1812345678901234567",
|
||||
"taskNo": "CS202606180001",
|
||||
"lineId": "LINE_001",
|
||||
"lineIds": ["LINE_001", "LINE_002"],
|
||||
"lineName": "1号监测点,2号监测点",
|
||||
"timeStart": "2026-06-18 00:00:00",
|
||||
@@ -261,7 +257,6 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
||||
| --- | --- | --- |
|
||||
| `taskId` | `String` | 任务 ID |
|
||||
| `taskNo` | `String` | 任务编号 |
|
||||
| `lineId` | `String` | 任务首个监测点 ID;兼容旧页面字段 |
|
||||
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||
| `lineName` | `String` | 监测点名称 |
|
||||
| `timeStart` | `String` | 开始时间 |
|
||||
@@ -318,7 +313,6 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
||||
"data": {
|
||||
"taskId": "1812345678901234567",
|
||||
"taskNo": "CS202606180001",
|
||||
"lineId": "LINE_001",
|
||||
"lineIds": ["LINE_001", "LINE_002"],
|
||||
"lineName": "1号监测点,2号监测点",
|
||||
"timeStart": "2026-06-18 00:00:00",
|
||||
@@ -327,7 +321,7 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
||||
"items": [
|
||||
{
|
||||
"itemId": "1812345678901234568",
|
||||
"itemKey": "LINE_001:VOLTAGE_A",
|
||||
"itemKey": "LINE_001|VOLTAGE_A",
|
||||
"lineId": "LINE_001",
|
||||
"lineName": "1号监测点",
|
||||
"indicatorCode": "VOLTAGE_A",
|
||||
@@ -378,8 +372,8 @@ GET /steady/checksquare/detail?taskId=1812345678901234567
|
||||
| `itemId` | `String` | 是 | 检测项 ID |
|
||||
| `detailType` | `String` | 是 | 明细类型:`SEGMENT`、`VALUE_ORDER`、`HARMONIC_PARITY` |
|
||||
| `statType` | `String` | 否 | 统计类型;查询统计类型连续性或谐波奇偶明细时使用 |
|
||||
| `pageNum` | `Integer` | 否 | 页码;和 `pageSize` 同时为正数时启用分页 |
|
||||
| `pageSize` | `Integer` | 否 | 每页条数;和 `pageNum` 同时为正数时启用分页 |
|
||||
| `pageNum` | `Integer` | 否 | 页码;和 `pageSize` 同时为正整数时启用分页 |
|
||||
| `pageSize` | `Integer` | 否 | 每页条数;和 `pageNum` 同时为正整数时启用分页 |
|
||||
|
||||
### 7.3 请求示例
|
||||
|
||||
@@ -530,7 +524,7 @@ GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=HARMON
|
||||
- 仅允许重启 `taskStatus=FAIL` 的任务。
|
||||
- 重启复用原任务记录,`taskId` 不变。
|
||||
- 重启前会清理该任务旧的检测项、统计摘要和明细数据,避免重复结果键。
|
||||
- 接口返回时任务状态已更新为 `RUNNING`;后台执行结束后更新为 `SUCCESS` 或 `FAIL`。
|
||||
- 接口返回时任务状态已更新为 `RUNNING`。
|
||||
|
||||
### 8.3 请求参数
|
||||
|
||||
@@ -544,9 +538,7 @@ GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=HARMON
|
||||
POST /steady/checksquare/restart?taskId=1812345678901234567
|
||||
```
|
||||
|
||||
### 8.5 响应说明
|
||||
|
||||
响应 `data` 与 `/create` 的 `SteadyChecksquareTaskVO` 字段一致。
|
||||
### 8.5 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -555,7 +547,6 @@ POST /steady/checksquare/restart?taskId=1812345678901234567
|
||||
"data": {
|
||||
"taskId": "1812345678901234567",
|
||||
"taskNo": "CS202606180001",
|
||||
"lineId": "LINE_001",
|
||||
"lineIds": ["LINE_001", "LINE_002"],
|
||||
"lineName": "1号监测点,2号监测点",
|
||||
"timeStart": "2026-06-18 00:00:00",
|
||||
@@ -607,15 +598,15 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 常见调试注意事项
|
||||
## 10. 调试注意事项
|
||||
|
||||
- `/create` 返回的是任务列表行信息,不是详情页完整数据。
|
||||
- `/create` 如果命中已存在任务,会直接返回该任务,不会生成重复任务。
|
||||
- 新标准推荐传 `lineIds`;`lineId` 仅作为单监测点兼容字段保留。
|
||||
- `/query` 的 `lineId` 会匹配任务内的 `lineIds`,可用于筛选多监测点任务。
|
||||
- `/create` 只接收 `lineIds`,不再接收任务级 `lineId`。
|
||||
- 单监测点也传 `lineIds` 单元素数组。
|
||||
- `/create` 如果命中已存在任务,会直接返回该任务,不生成重复任务。
|
||||
- `/query` 的 `lineId` 是筛选条件,会匹配任务内的 `lineIds`。
|
||||
- 任务级返回字段使用 `lineIds`;检测项级 `items[].lineId` 表示该检测项实际所属监测点。
|
||||
- `/detail` 需要使用 `/create`、`/restart` 或 `/query` 返回的 `taskId`。
|
||||
- `/detail` 的任务级 `lineId` 是首个监测点,检测项级 `items[].lineId` 才是该检测项实际所属监测点。
|
||||
- `/item-detail` 需要使用 `/detail` 返回的 `items[].itemId`。
|
||||
- `/item-detail` 只有 `pageNum` 和 `pageSize` 同时为正数时才分页;否则返回全部匹配明细,分页字段为空。
|
||||
- `/item-detail` 只有 `pageNum` 和 `pageSize` 同时为正整数时才分页;否则返回全部匹配明细,分页字段为空。
|
||||
- `/restart` 只允许重启失败任务,成功或执行中的任务调用会返回业务失败。
|
||||
- `dataIntegrity` 是 0 到 1 的小数,展示百分比优先使用 `dataIntegrityText`。
|
||||
|
||||
@@ -78,8 +78,8 @@ const checks = [
|
||||
const typeBlock =
|
||||
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
||||
return (
|
||||
/lineId\?: string/.test(typeBlock) &&
|
||||
/lineIds\?: string\[\]/.test(typeBlock) &&
|
||||
!/lineId\?: string/.test(typeBlock) &&
|
||||
/lineIds: string\[\]/.test(typeBlock) &&
|
||||
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
||||
/timeStart: string/.test(typeBlock) &&
|
||||
/timeEnd: string/.test(typeBlock) &&
|
||||
@@ -206,7 +206,7 @@ const checks = [
|
||||
return (
|
||||
/buildSteadyChecksquareCreatePayload\s*=\s*\(\s*lineIds:\s*string\[\]/.test(payload) &&
|
||||
/lineIds,/.test(payload) &&
|
||||
/lineId:\s*lineIds\[0\]/.test(payload) &&
|
||||
!/lineId:\s*lineIds\[0\]/.test(payload) &&
|
||||
!/lineIds\.length > 1/.test(payload) &&
|
||||
/buildSteadyChecksquareCreatePayload\(lineIds\.value,\s*selectedIndicators\.value,\s*formState\.value\)/.test(
|
||||
page
|
||||
|
||||
@@ -46,7 +46,6 @@ export const buildSteadyChecksquareCreatePayload = (
|
||||
formState: ChecksquareFormState
|
||||
): SteadyDataView.SteadyChecksquareCreateParams => {
|
||||
return {
|
||||
lineId: lineIds[0],
|
||||
lineIds,
|
||||
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
||||
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
||||
|
||||
@@ -30,7 +30,7 @@ const router = useRouter()
|
||||
|
||||
const openDiskMonitor = async () => {
|
||||
// 系统监控页作为模块入口,保持磁盘监控返回链路闭环。
|
||||
await router.push('/systemMonitor/diskMonitor')
|
||||
await router.push('/system-monitor/diskMonitor')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
||||
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
||||
@update-mapping-json="handleUpdateMappingJsonEvent"
|
||||
@update-xml-mapping="handleUpdateXmlMappingEvent"
|
||||
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
|
||||
@sequence-config-complete="handleSequenceConfigComplete"
|
||||
@save-icd-check-result="handleSaveIcdCheckResult"
|
||||
@@ -201,6 +202,7 @@ const {
|
||||
handleConfirmIcdConsistencyProblems,
|
||||
handleRemoveIcdConsistencyProblem,
|
||||
handleUpdateMappingJson,
|
||||
handleUpdateXmlMapping,
|
||||
handleSequenceConfigComplete,
|
||||
handleSaveIcdCheckResult,
|
||||
handleConfirmIndexSelection
|
||||
@@ -249,6 +251,13 @@ const handleUpdateMappingJsonEvent = (...args: unknown[]) => {
|
||||
handleUpdateMappingJson(mappingJson)
|
||||
}
|
||||
|
||||
const handleUpdateXmlMappingEvent = (...args: unknown[]) => {
|
||||
const [xmlMapping] = args
|
||||
|
||||
if (typeof xmlMapping !== 'string') return
|
||||
handleUpdateXmlMapping(xmlMapping)
|
||||
}
|
||||
|
||||
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
|
||||
|
||||
interface IcdCheckFlowStep {
|
||||
|
||||
@@ -46,23 +46,18 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角度">
|
||||
<el-input-number
|
||||
v-model="formModel.angle"
|
||||
:min="-360"
|
||||
:max="360"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="相位索引">
|
||||
<el-switch
|
||||
v-model="formModel.usePhaseIndex"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="icd-path-form-dialog__switch-row">
|
||||
<el-form-item label="支持相角">
|
||||
<el-switch v-model="formModel.angle" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="使用相别指标">
|
||||
<el-switch
|
||||
v-model="formModel.usePhaseIndex"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,9 +123,11 @@ const icdTypeOptions = [
|
||||
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||
]
|
||||
const isStandardIcdType = (type?: number) => type === 1 || type === 3
|
||||
const isReferenceIcdRequired = computed(() => !isStandardIcdType(formModel.type))
|
||||
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
||||
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记录'))
|
||||
|
||||
@@ -179,12 +176,23 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => formModel.type,
|
||||
type => {
|
||||
// 标准 ICD 的引用就是当前记录本身,切换类型时清掉旧的外部引用。
|
||||
if (isStandardIcdType(type)) {
|
||||
formModel.referenceIcdId = ''
|
||||
formRef.value?.clearValidate('referenceIcdId')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
||||
name: formModel.name.trim(),
|
||||
angle: formModel.angle,
|
||||
usePhaseIndex: formModel.usePhaseIndex,
|
||||
type: formModel.type,
|
||||
referenceIcdId: formModel.referenceIcdId
|
||||
referenceIcdId: isStandardIcdType(formModel.type) ? '' : formModel.referenceIcdId
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -262,6 +270,12 @@ const handleSave = async () => {
|
||||
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) {
|
||||
padding: 16px 20px 4px;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,51 @@
|
||||
<div class="json-tree-meta">{{ metaText }}</div>
|
||||
<div class="json-tree-actions">
|
||||
<slot name="actions" />
|
||||
<el-button type="primary" plain size="small" :disabled="!rootNode" @click="expandAll">全部展开</el-button>
|
||||
<el-button plain size="small" :disabled="!rootNode" @click="collapseAll">全部收起</el-button>
|
||||
<template v-if="editable">
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
<pre v-else class="mapping-json-text">{{ source }}</pre>
|
||||
@@ -18,6 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Edit, Expand, Fold } from '@element-plus/icons-vue'
|
||||
import { computed, ref, watch } from '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 }
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
source: string
|
||||
metaText?: string
|
||||
editable?: boolean
|
||||
}>(), {
|
||||
editable: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:source', value: string): void
|
||||
}>()
|
||||
|
||||
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 }>(() => {
|
||||
try {
|
||||
@@ -60,6 +109,16 @@ watch(
|
||||
{ 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 {
|
||||
if (Array.isArray(source)) {
|
||||
return {
|
||||
@@ -137,6 +196,30 @@ function expandAll() {
|
||||
function collapseAll() {
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -178,7 +261,8 @@ function collapseAll() {
|
||||
}
|
||||
|
||||
.json-tree-body,
|
||||
.mapping-json-text {
|
||||
.mapping-json-text,
|
||||
.json-editor-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
@@ -193,6 +277,38 @@ function collapseAll() {
|
||||
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 {
|
||||
white-space: pre-wrap;
|
||||
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 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 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 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 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 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 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)],
|
||||
@@ -313,6 +343,29 @@ const checks = [
|
||||
/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 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)],
|
||||
['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)]
|
||||
|
||||
@@ -19,6 +19,12 @@ const checks = [
|
||||
'ICD type column is widened to avoid text clipping',
|
||||
() => /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',
|
||||
() => /prop: 'mappingDetail'[\s\S]*?label: '映射文件详情'[\s\S]*?width: 150/.test(columnsSource)
|
||||
|
||||
@@ -10,6 +10,12 @@ const source = fs.readFileSync(panelFile, 'utf8')
|
||||
|
||||
const checks = [
|
||||
['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="问题列表"')],
|
||||
['XML match result action opens dedicated XML match dialog', () => /@click="matchResultDialogVisible = true"/.test(source)],
|
||||
['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',
|
||||
label: '相位索引',
|
||||
width: 110,
|
||||
label: '使用相别指标',
|
||||
width: 130,
|
||||
render: scope => getUsePhaseIndexText(scope.row.usePhaseIndex)
|
||||
},
|
||||
{
|
||||
|
||||
@@ -817,10 +817,33 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
reviewedIcdConsistencyProblemList.value = []
|
||||
hasConfirmedIcdConsistencyProblems.value = false
|
||||
shouldOpenIcdConsistencyProblems.value = 0
|
||||
hasSequenceConfigured.value = false
|
||||
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 = () => {
|
||||
hasSequenceConfigured.value = true
|
||||
}
|
||||
@@ -894,6 +917,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
handleConfirmIcdConsistencyProblems,
|
||||
handleRemoveIcdConsistencyProblem,
|
||||
handleUpdateMappingJson,
|
||||
handleUpdateXmlMapping,
|
||||
handleSequenceConfigComplete,
|
||||
handleSaveIcdCheckResult,
|
||||
handleConfirmIndexSelection
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
>
|
||||
JSON映射
|
||||
</el-button>
|
||||
<el-button type="primary" plain :icon="FullScreen" @click="indexSelectionFullscreenVisible = true">
|
||||
全屏查看
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,11 +51,34 @@
|
||||
|
||||
<el-empty v-if="!hasDefaultJson" :description="emptyDescription" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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({
|
||||
name: 'MappingConfigPanel'
|
||||
@@ -80,6 +106,8 @@ const emit = defineEmits<{
|
||||
(event: 'confirm-config'): void
|
||||
(event: 'generate'): void
|
||||
}>()
|
||||
|
||||
const indexSelectionFullscreenVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -202,6 +230,37 @@ const emit = defineEmits<{
|
||||
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) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
:content="
|
||||
icdConsistencyStatus === 'success'
|
||||
? 'ICD校验通过'
|
||||
: `ICD校验不通过,点击查看JSON映射问题列表(${icdConsistencyProblemCount}条)`
|
||||
: `ICD校验不通过,点击查看JSON匹配结果(${icdConsistencyProblemCount}条)`
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
@@ -101,6 +101,8 @@
|
||||
v-if="mappingJsonPreview"
|
||||
:source="mappingJsonPreview"
|
||||
:meta-text="mappingMetaText"
|
||||
editable
|
||||
@update:source="emit('update-mapping-json', $event)"
|
||||
>
|
||||
<template #actions>
|
||||
<el-button
|
||||
@@ -124,6 +126,16 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template #trailing-actions>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:icon="FullScreen"
|
||||
:disabled="!mappingJsonPreview"
|
||||
@click="jsonMappingFullscreenVisible = true"
|
||||
>
|
||||
全屏查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@@ -132,7 +144,7 @@
|
||||
:disabled="!canExportJsonMapping"
|
||||
@click="emit('export-mapping', 'json')"
|
||||
>
|
||||
下载JSON映射
|
||||
下载JSON
|
||||
</el-button>
|
||||
</template>
|
||||
</JsonMappingTree>
|
||||
@@ -145,6 +157,16 @@
|
||||
<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="FullScreen"
|
||||
:disabled="!xmlMappingPreview"
|
||||
@click="xmlMappingFullscreenVisible = true"
|
||||
>
|
||||
全屏查看
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@@ -153,7 +175,7 @@
|
||||
:disabled="!methodDescribe"
|
||||
@click="matchResultDialogVisible = true"
|
||||
>
|
||||
XML映射匹配结果列表
|
||||
XML匹配结果
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -163,11 +185,18 @@
|
||||
:disabled="!canExportXmlMapping"
|
||||
@click="emit('export-mapping', 'xml')"
|
||||
>
|
||||
下载XML映射
|
||||
下载XML
|
||||
</el-button>
|
||||
</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>
|
||||
<el-empty v-else :description="xmlEmptyText" />
|
||||
</div>
|
||||
@@ -178,7 +207,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="problemDialogVisible"
|
||||
title="JSON映射问题列表"
|
||||
title="JSON匹配结果"
|
||||
width="720px"
|
||||
destroy-on-close
|
||||
top="8vh"
|
||||
@@ -229,7 +258,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="matchResultDialogVisible"
|
||||
title="XML映射匹配结果列表"
|
||||
title="XML匹配结果"
|
||||
width="640px"
|
||||
destroy-on-close
|
||||
top="12vh"
|
||||
@@ -239,6 +268,108 @@
|
||||
</div>
|
||||
</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
|
||||
v-model="xmlDataTypeDialogVisible"
|
||||
title="XML映射类型"
|
||||
@@ -345,6 +476,7 @@ import {
|
||||
Document,
|
||||
Download,
|
||||
Finished,
|
||||
FullScreen,
|
||||
Search,
|
||||
Setting,
|
||||
Warning,
|
||||
@@ -436,6 +568,7 @@ const emit = defineEmits<{
|
||||
(event: 'export-mapping', value: ExportMappingType): void
|
||||
(event: 'generate-xml-mapping', value: number): void
|
||||
(event: 'update-mapping-json', value: string): void
|
||||
(event: 'update-xml-mapping', value: string): void
|
||||
(event: 'update:sequenceDialogVisible', value: boolean): void
|
||||
(event: 'sequence-config-complete'): void
|
||||
(event: 'icd-check'): void
|
||||
@@ -449,7 +582,7 @@ const activeTabProxy = computed({
|
||||
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 problemButtonText = computed(() =>
|
||||
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 icdConsistencyProblemDialogVisible = ref(false)
|
||||
const matchResultDialogVisible = ref(false)
|
||||
const jsonMappingFullscreenVisible = ref(false)
|
||||
const xmlMappingFullscreenVisible = ref(false)
|
||||
const xmlDataTypeDialogVisible = ref(false)
|
||||
const xmlDataType = ref<number>(2)
|
||||
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
||||
const sequenceSearchKeyword = ref('')
|
||||
const xmlMappingDraft = ref('')
|
||||
const sequenceDialogVisible = computed({
|
||||
get: () => props.sequenceDialogVisible,
|
||||
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 = () => {
|
||||
try {
|
||||
const nextJson = JSON.parse(props.mappingJsonPreview) as unknown
|
||||
@@ -918,14 +1066,22 @@ const confirmSequenceConfig = () => {
|
||||
}
|
||||
|
||||
.xml-file-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.xml-file-content :deep(.el-textarea__inner) {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
overflow: auto;
|
||||
box-shadow: none;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
@@ -1181,6 +1337,18 @@ const confirmSequenceConfig = () => {
|
||||
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) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user