feat(safety): 移植安全生产模块前端页面
来源:dev_pro_长辉电力 分支,14 个文件。
页面(src/views/safety/):
- org 安全组织架构、cert 特种作业证、inspection/point 巡检点位、
inspection/record 巡检记录、hazard 隐患闭环、permit 高危作业票、
material 应急物资台账、material/stock 出入库流水、
training 安全培训、drill 消防演练、accident 事故登记、ledger 标准化台账
- shared.ts:枚举选项、状态颜色、数字格式化等公共常量
API(src/api/safety/index.ts):
- 对接后端 /safety/** 全部接口,类型定义与后端 VO 一致
康意适配:
- 员工选择组件路径改为康意已有的 hrm/employee/components/select.vue
(长辉为 select-dropdown.vue,康意没有该文件;两者 modelValue/placeholder
接口一致,均为弹窗选择)
- 车间下拉 API 路径改为 #/api/mes/md/workstation/workshop/index
(康意为目录形式,长辉为 workshop.ts)
验证:
- pnpm vite build --mode production 构建通过,产出 dist
- 已部署到 /data/vue/mom/kyzn/dist(nginx 9008),
js/safety-BY74d3hE.js、index.html、/safety/org 路由均返回 200
- 菜单接口返回的 13 个 component 路径与页面文件一一对应
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | /** å®å
¨ç产 - éç¨å页忰 */ |
| | | type PageQuery = PageParam & Record<string, any>; |
| | | |
| | | /** å®å
¨ç产 API å½åç©ºé´ */ |
| | | export namespace SafetyApi { |
| | | // ==================== å®å
¨ç»ç»æ¶æ ==================== |
| | | export interface Org { |
| | | id?: number; |
| | | parentId?: number; |
| | | name: string; |
| | | /** è´è´£äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | leaderId?: number; |
| | | leaderName?: string; |
| | | leaderPhone?: string; |
| | | memberNames?: string; |
| | | duty?: string; |
| | | sort: number; |
| | | status: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | children?: Org[]; |
| | | } |
| | | |
| | | // ==================== ç¹ç§ä½ä¸è¯ ==================== |
| | | export interface Certificate { |
| | | id?: number; |
| | | /** æè¯äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | employeeId?: number; |
| | | /** æè¯äººå
³èçç³»ç»ç¨æ·ç¼å· */ |
| | | userId?: number; |
| | | employeeName?: string; |
| | | employeeNo?: string; |
| | | deptName?: string; |
| | | phone?: string; |
| | | certType: string; |
| | | certTypeName?: string; |
| | | certNo: string; |
| | | issuer?: string; |
| | | issueDate?: string; |
| | | expireDate: string; |
| | | reviewDate?: string; |
| | | warnDays?: number; |
| | | status?: string; |
| | | statusName?: string; |
| | | remainingDays?: number; |
| | | attachment?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== å·¡æ£ç¹ä½ ==================== |
| | | export interface InspectionPoint { |
| | | id?: number; |
| | | code: string; |
| | | name: string; |
| | | pointType: string; |
| | | pointTypeName?: string; |
| | | /** æå±è½¦é´ï¼å
³è mes_md_workshopï¼ */ |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | location?: string; |
| | | inspectItems?: string; |
| | | inspectCycle?: string; |
| | | inspectCycleName?: string; |
| | | /** 责任人ï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | ownerId?: number; |
| | | ownerName?: string; |
| | | status: number; |
| | | lastInspectTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== å·¡æ£è®°å½ ==================== |
| | | export interface Inspection { |
| | | id?: number; |
| | | pointId: number; |
| | | pointName?: string; |
| | | pointType?: string; |
| | | pointTypeName?: string; |
| | | inspectTime: string; |
| | | /** å·¡æ£äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | inspectorId?: number; |
| | | inspector?: string; |
| | | result?: string; |
| | | resultName?: string; |
| | | content?: string; |
| | | photoUrls?: string; |
| | | hazardCount?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== 鿣 ==================== |
| | | export interface Hazard { |
| | | id?: number; |
| | | code?: string; |
| | | title: string; |
| | | level?: string; |
| | | levelName?: string; |
| | | source?: string; |
| | | sourceName?: string; |
| | | inspectionId?: number; |
| | | pointId?: number; |
| | | pointName?: string; |
| | | location?: string; |
| | | description?: string; |
| | | photoUrls?: string; |
| | | /** 䏿¥äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | reporterId?: number; |
| | | reporter?: string; |
| | | reportTime?: string; |
| | | /** æ´æ¹è´£ä»»äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | assigneeId?: number; |
| | | assignee?: string; |
| | | assignTime?: string; |
| | | rectifyDeadline?: string; |
| | | overdue?: boolean; |
| | | rectifyDesc?: string; |
| | | rectifyPhotoUrls?: string; |
| | | rectifyTime?: string; |
| | | /** 夿¥äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | recheckerId?: number; |
| | | rechecker?: string; |
| | | recheckTime?: string; |
| | | recheckResult?: string; |
| | | recheckResultName?: string; |
| | | recheckRemark?: string; |
| | | status?: string; |
| | | statusName?: string; |
| | | closeTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== é«å±ä½ä¸ç¥¨ ==================== |
| | | export interface WorkPermit { |
| | | id?: number; |
| | | code?: string; |
| | | permitType: string; |
| | | permitTypeName?: string; |
| | | title: string; |
| | | /** ä½ä¸è½¦é´ï¼å
³è mes_md_workshopï¼ */ |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | location?: string; |
| | | workContent?: string; |
| | | /** ç³è¯·äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | applicantId?: number; |
| | | applicant?: string; |
| | | applyTime?: string; |
| | | planStartTime: string; |
| | | planEndTime: string; |
| | | actualStartTime?: string; |
| | | actualEndTime?: string; |
| | | riskLevel?: string; |
| | | riskLevelName?: string; |
| | | safetyMeasures?: string; |
| | | /** 审æ¹äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | approverId?: number; |
| | | approver?: string; |
| | | approveTime?: string; |
| | | approveComment?: string; |
| | | /** çæ¤äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | guardianId?: number; |
| | | guardian?: string; |
| | | guardianRequired?: boolean; |
| | | status?: string; |
| | | statusName?: string; |
| | | attachment?: string; |
| | | photoUrls?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface WorkPermitMonitor { |
| | | id?: number; |
| | | permitId: number; |
| | | monitorTime?: string; |
| | | /** çæ¤äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | guardianId?: number; |
| | | guardian?: string; |
| | | monitorContent?: string; |
| | | abnormal?: string; |
| | | photoUrls?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== åºæ¥ç©èµ ==================== |
| | | export interface Material { |
| | | id?: number; |
| | | code: string; |
| | | name: string; |
| | | category: string; |
| | | categoryName?: string; |
| | | spec?: string; |
| | | unit?: string; |
| | | stockQty?: number; |
| | | safeStock?: number; |
| | | belowSafeStock?: boolean; |
| | | storageLocation?: string; |
| | | expireDate?: string; |
| | | expireRemainingDays?: number; |
| | | validityMonths?: number; |
| | | /** ä¿ç®¡äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | managerId?: number; |
| | | manager?: string; |
| | | status: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface MaterialStock { |
| | | id?: number; |
| | | materialId: number; |
| | | materialName?: string; |
| | | category?: string; |
| | | categoryName?: string; |
| | | bizType: string; |
| | | bizTypeName?: string; |
| | | quantity: number; |
| | | stockBefore?: number; |
| | | stockAfter?: number; |
| | | bizDate?: string; |
| | | operator: string; |
| | | receiver?: string; |
| | | purpose?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== å¹è® / æ¼ç» / äºæ
==================== |
| | | export interface Training { |
| | | id?: number; |
| | | title: string; |
| | | trainingType?: string; |
| | | trainingTypeName?: string; |
| | | trainDate: string; |
| | | durationHours?: number; |
| | | /** 讲å¸/ç»ç»äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | trainerId?: number; |
| | | trainer?: string; |
| | | location?: string; |
| | | participants?: string; |
| | | participantCount?: number; |
| | | content?: string; |
| | | examResult?: string; |
| | | attachment?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface Drill { |
| | | id?: number; |
| | | title: string; |
| | | drillType?: string; |
| | | drillTypeName?: string; |
| | | drillDate: string; |
| | | location?: string; |
| | | /** ç»ç»äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | organizerId?: number; |
| | | organizer?: string; |
| | | participants?: string; |
| | | participantCount?: number; |
| | | planDesc?: string; |
| | | processDesc?: string; |
| | | summary?: string; |
| | | durationMinutes?: number; |
| | | attachment?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface Accident { |
| | | id?: number; |
| | | code?: string; |
| | | title: string; |
| | | accidentType: string; |
| | | accidentTypeName?: string; |
| | | level?: string; |
| | | levelName?: string; |
| | | occurTime: string; |
| | | location?: string; |
| | | /** æå±è½¦é´ï¼å
³è mes_md_workshopï¼ */ |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | casualtyDesc?: string; |
| | | economicLoss?: number; |
| | | involvedPersons?: string; |
| | | description?: string; |
| | | causeAnalysis?: string; |
| | | measures?: string; |
| | | /** ç»è®°äººï¼åå·¥ç¼å·ï¼å
³è hrm_employeeï¼ */ |
| | | reporterId?: number; |
| | | reporter?: string; |
| | | reportTime?: string; |
| | | status?: string; |
| | | statusName?: string; |
| | | attachment?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | // ==================== æ»è§ ==================== |
| | | export interface MonthlyCount { |
| | | month: string; |
| | | count: number; |
| | | } |
| | | |
| | | export interface Overview { |
| | | certTotal: number; |
| | | certExpiring: number; |
| | | certExpired: number; |
| | | hazardTotal: number; |
| | | hazardOpen: number; |
| | | hazardOverdue: number; |
| | | hazardCloseRate: number; |
| | | permitPending: number; |
| | | permitInProgress: number; |
| | | materialBelowSafeStock: number; |
| | | materialExpiring: number; |
| | | trainingThisMonth: number; |
| | | drillThisMonth: number; |
| | | accidentThisMonth: number; |
| | | hazardTrend: MonthlyCount[]; |
| | | accidentTrend: MonthlyCount[]; |
| | | } |
| | | } |
| | | |
| | | // ==================== å®å
¨ç»ç»æ¶æ ==================== |
| | | export function getOrgTree() { |
| | | return requestClient.get<SafetyApi.Org[]>('/safety/org/tree'); |
| | | } |
| | | export function getOrg(id: number) { |
| | | return requestClient.get<SafetyApi.Org>('/safety/org/get', { params: { id } }); |
| | | } |
| | | export function createOrg(data: SafetyApi.Org) { |
| | | return requestClient.post<number>('/safety/org/create', data); |
| | | } |
| | | export function updateOrg(data: SafetyApi.Org) { |
| | | return requestClient.put('/safety/org/update', data); |
| | | } |
| | | export function deleteOrg(id: number) { |
| | | return requestClient.delete('/safety/org/delete', { params: { id } }); |
| | | } |
| | | |
| | | // ==================== ç¹ç§ä½ä¸è¯ ==================== |
| | | export function getCertificatePage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Certificate>>('/safety/certificate/page', { params }); |
| | | } |
| | | export function getCertificate(id: number) { |
| | | return requestClient.get<SafetyApi.Certificate>('/safety/certificate/get', { params: { id } }); |
| | | } |
| | | export function createCertificate(data: SafetyApi.Certificate) { |
| | | return requestClient.post<number>('/safety/certificate/create', data); |
| | | } |
| | | export function updateCertificate(data: SafetyApi.Certificate) { |
| | | return requestClient.put('/safety/certificate/update', data); |
| | | } |
| | | export function deleteCertificate(id: number) { |
| | | return requestClient.delete('/safety/certificate/delete', { params: { id } }); |
| | | } |
| | | export function getExpiringCertificates(days?: number) { |
| | | return requestClient.get<SafetyApi.Certificate[]>('/safety/certificate/expiring-list', { |
| | | params: { days }, |
| | | }); |
| | | } |
| | | |
| | | // ==================== å·¡æ£ç¹ä½ ==================== |
| | | export function getInspectionPointPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.InspectionPoint>>('/safety/inspection/point/page', { params }); |
| | | } |
| | | export function getInspectionPoint(id: number) { |
| | | return requestClient.get<SafetyApi.InspectionPoint>('/safety/inspection/point/get', { params: { id } }); |
| | | } |
| | | export function getInspectionPointSimpleList() { |
| | | return requestClient.get<SafetyApi.InspectionPoint[]>('/safety/inspection/point/simple-list'); |
| | | } |
| | | export function createInspectionPoint(data: SafetyApi.InspectionPoint) { |
| | | return requestClient.post<number>('/safety/inspection/point/create', data); |
| | | } |
| | | export function updateInspectionPoint(data: SafetyApi.InspectionPoint) { |
| | | return requestClient.put('/safety/inspection/point/update', data); |
| | | } |
| | | export function deleteInspectionPoint(id: number) { |
| | | return requestClient.delete('/safety/inspection/point/delete', { params: { id } }); |
| | | } |
| | | |
| | | // ==================== å·¡æ£è®°å½ ==================== |
| | | export function getInspectionPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Inspection>>('/safety/inspection/record/page', { params }); |
| | | } |
| | | export function getInspection(id: number) { |
| | | return requestClient.get<SafetyApi.Inspection>('/safety/inspection/record/get', { params: { id } }); |
| | | } |
| | | export function createInspection(data: SafetyApi.Inspection) { |
| | | return requestClient.post<number>('/safety/inspection/record/create', data); |
| | | } |
| | | export function updateInspection(data: SafetyApi.Inspection) { |
| | | return requestClient.put('/safety/inspection/record/update', data); |
| | | } |
| | | export function deleteInspection(id: number) { |
| | | return requestClient.delete('/safety/inspection/record/delete', { params: { id } }); |
| | | } |
| | | |
| | | // ==================== 鿣 ==================== |
| | | export function getHazardPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Hazard>>('/safety/hazard/page', { params }); |
| | | } |
| | | export function getHazard(id: number) { |
| | | return requestClient.get<SafetyApi.Hazard>('/safety/hazard/get', { params: { id } }); |
| | | } |
| | | export function createHazard(data: SafetyApi.Hazard) { |
| | | return requestClient.post<number>('/safety/hazard/create', data); |
| | | } |
| | | export function updateHazard(data: SafetyApi.Hazard) { |
| | | return requestClient.put('/safety/hazard/update', data); |
| | | } |
| | | export function deleteHazard(id: number) { |
| | | return requestClient.delete('/safety/hazard/delete', { params: { id } }); |
| | | } |
| | | export function assignHazard(data: { id: number; assigneeId: number; rectifyDeadline?: string; remark?: string }) { |
| | | return requestClient.put('/safety/hazard/assign', data); |
| | | } |
| | | export function rectifyHazard(data: { id: number; rectifyDesc: string; rectifyPhotoUrls?: string }) { |
| | | return requestClient.put('/safety/hazard/rectify', data); |
| | | } |
| | | export function recheckHazard(data: { |
| | | id: number; |
| | | recheckerId: number; |
| | | recheckResult: string; |
| | | recheckRemark?: string; |
| | | }) { |
| | | return requestClient.put('/safety/hazard/recheck', data); |
| | | } |
| | | |
| | | // ==================== é«å±ä½ä¸ç¥¨ ==================== |
| | | export function getWorkPermitPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.WorkPermit>>('/safety/permit/page', { params }); |
| | | } |
| | | export function getWorkPermit(id: number) { |
| | | return requestClient.get<SafetyApi.WorkPermit>('/safety/permit/get', { params: { id } }); |
| | | } |
| | | export function createWorkPermit(data: SafetyApi.WorkPermit) { |
| | | return requestClient.post<number>('/safety/permit/create', data); |
| | | } |
| | | export function updateWorkPermit(data: SafetyApi.WorkPermit) { |
| | | return requestClient.put('/safety/permit/update', data); |
| | | } |
| | | export function deleteWorkPermit(id: number) { |
| | | return requestClient.delete('/safety/permit/delete', { params: { id } }); |
| | | } |
| | | export function approveWorkPermit(data: { |
| | | id: number; |
| | | approverId: number; |
| | | decision: string; |
| | | approveComment?: string; |
| | | }) { |
| | | return requestClient.put('/safety/permit/approve', data); |
| | | } |
| | | export function startWorkPermit(id: number) { |
| | | return requestClient.put('/safety/permit/start', null, { params: { id } }); |
| | | } |
| | | export function closeWorkPermit(id: number) { |
| | | return requestClient.put('/safety/permit/close', null, { params: { id } }); |
| | | } |
| | | export function cancelWorkPermit(id: number) { |
| | | return requestClient.put('/safety/permit/cancel', null, { params: { id } }); |
| | | } |
| | | export function getMonitorList(permitId: number) { |
| | | return requestClient.get<SafetyApi.WorkPermitMonitor[]>('/safety/permit/monitor/list', { |
| | | params: { permitId }, |
| | | }); |
| | | } |
| | | export function createMonitor(data: SafetyApi.WorkPermitMonitor) { |
| | | return requestClient.post<number>('/safety/permit/monitor/create', data); |
| | | } |
| | | export function deleteMonitor(id: number) { |
| | | return requestClient.delete('/safety/permit/monitor/delete', { params: { id } }); |
| | | } |
| | | |
| | | // ==================== åºæ¥ç©èµ ==================== |
| | | export function getMaterialPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Material>>('/safety/material/page', { params }); |
| | | } |
| | | export function getMaterial(id: number) { |
| | | return requestClient.get<SafetyApi.Material>('/safety/material/get', { params: { id } }); |
| | | } |
| | | export function createMaterial(data: SafetyApi.Material) { |
| | | return requestClient.post<number>('/safety/material/create', data); |
| | | } |
| | | export function updateMaterial(data: SafetyApi.Material) { |
| | | return requestClient.put('/safety/material/update', data); |
| | | } |
| | | export function deleteMaterial(id: number) { |
| | | return requestClient.delete('/safety/material/delete', { params: { id } }); |
| | | } |
| | | export function getBelowSafeStockMaterials() { |
| | | return requestClient.get<SafetyApi.Material[]>('/safety/material/below-safe-stock'); |
| | | } |
| | | export function getExpiringMaterials(days?: number) { |
| | | return requestClient.get<SafetyApi.Material[]>('/safety/material/expiring-list', { params: { days } }); |
| | | } |
| | | export function getMaterialStockPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.MaterialStock>>('/safety/material/stock/page', { params }); |
| | | } |
| | | export function createMaterialStock(data: SafetyApi.MaterialStock) { |
| | | return requestClient.post<number>('/safety/material/stock/create', data); |
| | | } |
| | | |
| | | // ==================== å¹è® / æ¼ç» / äºæ
==================== |
| | | export function getTrainingPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Training>>('/safety/training/page', { params }); |
| | | } |
| | | export function getTraining(id: number) { |
| | | return requestClient.get<SafetyApi.Training>('/safety/training/get', { params: { id } }); |
| | | } |
| | | export function createTraining(data: SafetyApi.Training) { |
| | | return requestClient.post<number>('/safety/training/create', data); |
| | | } |
| | | export function updateTraining(data: SafetyApi.Training) { |
| | | return requestClient.put('/safety/training/update', data); |
| | | } |
| | | export function deleteTraining(id: number) { |
| | | return requestClient.delete('/safety/training/delete', { params: { id } }); |
| | | } |
| | | |
| | | export function getDrillPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Drill>>('/safety/training/drill/page', { params }); |
| | | } |
| | | export function getDrill(id: number) { |
| | | return requestClient.get<SafetyApi.Drill>('/safety/training/drill/get', { params: { id } }); |
| | | } |
| | | export function createDrill(data: SafetyApi.Drill) { |
| | | return requestClient.post<number>('/safety/training/drill/create', data); |
| | | } |
| | | export function updateDrill(data: SafetyApi.Drill) { |
| | | return requestClient.put('/safety/training/drill/update', data); |
| | | } |
| | | export function deleteDrill(id: number) { |
| | | return requestClient.delete('/safety/training/drill/delete', { params: { id } }); |
| | | } |
| | | |
| | | export function getAccidentPage(params: PageQuery) { |
| | | return requestClient.get<PageResult<SafetyApi.Accident>>('/safety/training/accident/page', { params }); |
| | | } |
| | | export function getAccident(id: number) { |
| | | return requestClient.get<SafetyApi.Accident>('/safety/training/accident/get', { params: { id } }); |
| | | } |
| | | export function createAccident(data: SafetyApi.Accident) { |
| | | return requestClient.post<number>('/safety/training/accident/create', data); |
| | | } |
| | | export function updateAccident(data: SafetyApi.Accident) { |
| | | return requestClient.put('/safety/training/accident/update', data); |
| | | } |
| | | export function deleteAccident(id: number) { |
| | | return requestClient.delete('/safety/training/accident/delete', { params: { id } }); |
| | | } |
| | | |
| | | // ==================== å°è´¦ä¸æ»è§ ==================== |
| | | export function getSafetyOverview() { |
| | | return requestClient.get<SafetyApi.Overview>('/safety/ledger/overview'); |
| | | } |
| | | export function exportSafetyLedger(params: { type?: string; startDate?: string; endDate?: string }) { |
| | | return requestClient.download('/safety/ledger/export', { params }); |
| | | } |
| | | |
| | | // ==================== ç§»å¨ç«¯æç
§ä¸ä¼ ==================== |
| | | // 说æï¼requestClient.upload 对æ°ç»å段ä¼å±å¼æ files[0]ãfiles[1]ï¼ |
| | | // èå端æ¹éæ¥å£ç¨ @RequestPart("files") List<MultipartFile>ï¼å æ¤èµ° FormData ç´ä¼ ã |
| | | export function uploadSafetyFile(file: File, bizType = 'safety') { |
| | | return requestClient.upload<string>('/safety/file/upload', { file, bizType }); |
| | | } |
| | | export function uploadSafetyFiles(files: File[], bizType = 'safety') { |
| | | const formData = new FormData(); |
| | | files.forEach((file) => formData.append('files', file)); |
| | | if (bizType) formData.append('bizType', bizType); |
| | | return requestClient.post<string[]>('/safety/file/upload-batch', formData, { |
| | | headers: { 'Content-Type': 'multipart/form-data' }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop/index'; |
| | | import { |
| | | createAccident, |
| | | deleteAccident, |
| | | getAccidentPage, |
| | | updateAccident, |
| | | } from '#/api/safety'; |
| | | |
| | | import { |
| | | ACCIDENT_LEVEL_COLOR, |
| | | ACCIDENT_LEVEL_OPTIONS, |
| | | ACCIDENT_STATUS_OPTIONS, |
| | | ACCIDENT_TYPE_OPTIONS, |
| | | } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyAccident' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Accident[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const workshopOptions = ref<{ label: string; value: number }[]>([]); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.Accident => ({ |
| | | title: '', |
| | | accidentType: 'ELECTRIC_SHOCK', |
| | | level: 'GENERAL', |
| | | occurTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | location: '', |
| | | workshopId: undefined, |
| | | casualtyDesc: '', |
| | | economicLoss: 0, |
| | | involvedPersons: '', |
| | | description: '', |
| | | causeAnalysis: '', |
| | | measures: '', |
| | | status: 'REPORTED', |
| | | attachment: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Accident>(initial()); |
| | | |
| | | const rules = { |
| | | title: [{ required: true, whitespace: true, message: '请è¾å
¥äºæ
åç§°' }], |
| | | accidentType: [{ required: true, message: 'è¯·éæ©äºæ
ç±»å' }], |
| | | occurTime: [{ required: true, message: 'è¯·éæ©åçæ¶é´' }], |
| | | }; |
| | | |
| | | const columns = [ |
| | | { title: 'äºæ
ç¼å·', dataIndex: 'code', width: 165, fixed: 'left' as const }, |
| | | { title: 'äºæ
åç§°', dataIndex: 'title', minWidth: 220, ellipsis: true }, |
| | | { title: 'ç±»å', dataIndex: 'accidentType', width: 120 }, |
| | | { title: 'ç级', dataIndex: 'level', width: 110, align: 'center' as const }, |
| | | { title: 'åçæ¶é´', dataIndex: 'occurTime', width: 165 }, |
| | | { title: 'åçå°ç¹', dataIndex: 'location', width: 160, ellipsis: true }, |
| | | { title: '车é´', dataIndex: 'workshopName', width: 130, ellipsis: true }, |
| | | { title: '伤亡æ
åµ', dataIndex: 'casualtyDesc', width: 160, ellipsis: true }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 110, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const params: any = { ...filters, pageNo: pagination.current, pageSize: pagination.pageSize }; |
| | | if (params.occurTimeRange?.length === 2) { |
| | | params.occurTime = params.occurTimeRange; |
| | | delete params.occurTimeRange; |
| | | } |
| | | const data = await getAccidentPage(params); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½äºæ
è®°å½å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadWorkshops() { |
| | | try { |
| | | const list = await getWorkshopSimpleList(); |
| | | workshopOptions.value = (list || []).map((w) => ({ label: w.name!, value: w.id! })); |
| | | } catch { |
| | | workshopOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.Accident) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, title: model.title.trim() }; |
| | | // æå±è½¦é´åç§° / ç»è®°äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä¸åä¸æäº¤ |
| | | delete data.workshopName; |
| | | delete data.reporter; |
| | | await (data.id ? updateAccident(data) : createAccident(data)); |
| | | message.success('äºæ
è®°å½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Accident) { |
| | | await deleteAccident(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | void load(); |
| | | void loadWorkshops(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="äºæ
ç»è®°" |
| | | description="çµåè¡ä¸å®å
¨äºæ
çç»è®°ãè°æ¥ãå¤çä¸ç»æ¡å½æ¡£ï¼è§¦çµãç«ç¾ãé«å¤å è½çï¼ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="åçæ¶é´"> |
| | | <DatePicker.RangePicker v-model:value="filters.occurTimeRange" show-time format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" /> |
| | | </FormItem> |
| | | <FormItem label="åç§°"> |
| | | <Input v-model:value="filters.title" allow-clear placeholder="äºæ
åç§°" style="width: 170px" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å"> |
| | | <Select v-model:value="filters.accidentType" :options="ACCIDENT_TYPE_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="ç¶æ"> |
| | | <Select v-model:value="filters.status" :options="ACCIDENT_STATUS_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">äºæ
è®°å½</span> |
| | | <Button v-access:code="['safety:accident:create']" type="primary" @click="showForm()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | ç»è®°äºæ
|
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1530 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'accidentType'"> |
| | | <Tag color="red">{{ record.accidentTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'level'"> |
| | | <Tag :color="ACCIDENT_LEVEL_COLOR[record.level ?? '']">{{ record.levelName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'occurTime'"> |
| | | {{ record.occurTime ? String(record.occurTime).replace('T', ' ') : '-' }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'casualtyDesc'"> |
| | | <span :class="{ 'text-red-500': !!record.casualtyDesc }">{{ record.casualtyDesc || '-' }}</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag>{{ record.statusName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'workshopName'"> |
| | | {{ record.workshopName || '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button v-access:code="['safety:accident:update']" type="link" size="small" @click="showForm(record as SafetyApi.Accident)">ç¼è¾</Button> |
| | | <Popconfirm title="ç¡®å®å é¤è¯¥äºæ
è®°å½ï¼" ok-text="ç¡®å®" cancel-text="åæ¶" @confirm="remove(record as SafetyApi.Accident)"> |
| | | <Button v-access:code="['safety:accident:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾äºæ
è®°å½' : 'äºæ
ç»è®°'" |
| | | :width="780" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form ref="formRef" :model="model" :rules="rules" :disabled="saving" layout="vertical" class="safety-form"> |
| | | <FormItem label="äºæ
åç§°" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="å¦ 10kV å¼å
³æè¯¯æä½è§¦çµ" /> |
| | | </FormItem> |
| | | <FormItem label="äºæ
ç±»å" name="accidentType"> |
| | | <Select v-model:value="model.accidentType" :options="ACCIDENT_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="äºæ
ç级" name="level"> |
| | | <Select v-model:value="model.level" :options="ACCIDENT_LEVEL_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="åçæ¶é´" name="occurTime"> |
| | | <DatePicker v-model:value="model.occurTime" show-time format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="åçå°ç¹" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ 2#é«åé
çµå®¤" /> |
| | | </FormItem> |
| | | <FormItem label="æå±è½¦é´/åºå" name="workshopId"> |
| | | <Select |
| | | v-model:value="model.workshopId" |
| | | :options="workshopOptions" |
| | | show-search |
| | | :filter-option="(i: string, o: any) => String(o.label).includes(i)" |
| | | placeholder="è¯·éæ©è½¦é´" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="伤亡æ
åµ" name="casualtyDesc"> |
| | | <Input v-model:value="model.casualtyDesc" :maxlength="500" placeholder="å¦ 1 人轻伤" /> |
| | | </FormItem> |
| | | <FormItem label="ç´æ¥ç»æµæå¤±ï¼å
ï¼" name="economicLoss"> |
| | | <InputNumber v-model:value="model.economicLoss" :min="0" :precision="2" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å¤çç¶æ" name="status"> |
| | | <Select v-model:value="model.status" :options="ACCIDENT_STATUS_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="æ¶äºäººå" name="involvedPersons" class="col-span-full"> |
| | | <Input v-model:value="model.involvedPersons" :maxlength="500" placeholder="å¤ä¸ªå§åç¨éå·åé" /> |
| | | </FormItem> |
| | | <FormItem label="äºæ
ç»è¿" name="description" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.description" :maxlength="2000" :rows="3" show-count placeholder="æè¿°äºæ
åççæ¶é´ãå°ç¹ä¸ç»è¿" /> |
| | | </FormItem> |
| | | <FormItem label="åå åæ" name="causeAnalysis" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.causeAnalysis" :maxlength="2000" :rows="2" placeholder="ç´æ¥åå ä¸é´æ¥åå " /> |
| | | </FormItem> |
| | | <FormItem label="å¤çæªæ½ä¸æ´æ¹è¦æ±" name="measures" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.measures" :maxlength="2000" :rows="2" placeholder="责任认å®ãå¤çå³å®ä¸æ´æ¹è¦æ±" /> |
| | | </FormItem> |
| | | <FormItem label="ç°åºç
§ç/è°æ¥æ¥å" name="attachment" class="col-span-full"> |
| | | <Input v-model:value="model.attachment" :maxlength="2000" placeholder="å¤ä¸ª URL ç¨éå·åé" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance, TablePaginationConfig } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | createCertificate, |
| | | deleteCertificate, |
| | | getCertificatePage, |
| | | updateCertificate, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { CERT_STATUS_COLOR, CERT_STATUS_OPTIONS, CERT_TYPE_OPTIONS } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyCert' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Certificate[]>([]); |
| | | const loading = ref(false); |
| | | const loadError = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const detail = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.Certificate => ({ |
| | | employeeId: undefined, |
| | | certType: 'HIGH_VOLTAGE_OPERATION', |
| | | certNo: '', |
| | | issuer: '', |
| | | issueDate: undefined, |
| | | expireDate: '', |
| | | reviewDate: undefined, |
| | | warnDays: 30, |
| | | status: 'VALID', |
| | | attachment: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Certificate>(initial()); |
| | | |
| | | const rules = { |
| | | employeeId: [{ required: true, message: 'è¯·éæ©æè¯äºº' }], |
| | | certType: [{ required: true, message: 'è¯·éæ©è¯ä»¶ç±»å' }], |
| | | certNo: [{ required: true, whitespace: true, message: '请è¾å
¥è¯ä»¶ç¼å·' }], |
| | | expireDate: [{ required: true, message: 'è¯·éæ©æææè³' }], |
| | | }; |
| | | |
| | | const columns = [ |
| | | { title: 'æè¯äºº', dataIndex: 'employeeName', width: 150, fixed: 'left' as const }, |
| | | { title: 'é¨é¨', dataIndex: 'deptName', width: 120 }, |
| | | { title: 'è¯ä»¶ç±»å', dataIndex: 'certType', width: 160 }, |
| | | { title: 'è¯ä»¶ç¼å·', dataIndex: 'certNo', width: 190 }, |
| | | { title: 'åè¯æºå
³', dataIndex: 'issuer', width: 160 }, |
| | | { title: 'æææè³', dataIndex: 'expireDate', width: 120 }, |
| | | { title: 'å©ä½å¤©æ°', dataIndex: 'remainingDays', width: 110, align: 'center' as const }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 100, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 170, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | let requestId = 0; |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | const currentId = ++requestId; |
| | | loading.value = true; |
| | | loadError.value = false; |
| | | try { |
| | | const data = await getCertificatePage({ |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }); |
| | | if (currentId !== requestId) return; |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | if (currentId === requestId) { |
| | | rows.value = []; |
| | | loadError.value = true; |
| | | } |
| | | } finally { |
| | | if (currentId === requestId) loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: TablePaginationConfig) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.Certificate, readonly = false) { |
| | | detail.value = readonly; |
| | | Object.assign(model, initial(), row ?? {}); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { |
| | | ...model, |
| | | certNo: model.certNo.trim(), |
| | | }; |
| | | await (data.id ? updateCertificate(data) : createCertificate(data)); |
| | | message.success('ç¹ç§ä½ä¸è¯å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | const deletingId = ref<number>(); |
| | | async function remove(row: SafetyApi.Certificate) { |
| | | deletingId.value = row.id; |
| | | try { |
| | | await deleteCertificate(row.id!); |
| | | message.success('å·²å é¤'); |
| | | if (rows.value.length === 1 && pagination.current > 1) pagination.current--; |
| | | await load(); |
| | | } finally { |
| | | deletingId.value = undefined; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="æè¯äºº"> |
| | | <Input v-model:value="filters.employeeName" allow-clear placeholder="å§å" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem label="å·¥å·"> |
| | | <Input v-model:value="filters.employeeNo" allow-clear placeholder="å·¥å·" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem label="é¨é¨"> |
| | | <Input v-model:value="filters.deptName" allow-clear placeholder="é¨é¨" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="è¯ä»¶ç±»å"> |
| | | <Select |
| | | v-model:value="filters.certType" |
| | | :options="CERT_TYPE_OPTIONS" |
| | | allow-clear |
| | | placeholder="å
¨é¨ç±»å" |
| | | style="width: 170px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="ç¶æ"> |
| | | <Select |
| | | v-model:value="filters.status" |
| | | :options="CERT_STATUS_OPTIONS" |
| | | allow-clear |
| | | placeholder="å
¨é¨ç¶æ" |
| | | style="width: 130px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">ç¹ç§ä½ä¸è¯å°è´¦</span> |
| | | <Button |
| | | v-access:code="['safety:cert:create']" |
| | | type="primary" |
| | | @click="showForm()" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:plus" /> |
| | | </template> |
| | | æ°å¢è¯ä»¶ |
| | | </Button> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="loadError" |
| | | type="error" |
| | | message="è¯ä»¶æ°æ®å 载失败ï¼è¯·ç¹å»æ¥è¯¢éè¯ã" |
| | | show-icon |
| | | class="mb-4" |
| | | /> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1400 }" |
| | | size="middle" |
| | | :locale="{ emptyText: 'ææ ç¹ç§ä½ä¸è¯è®°å½' }" |
| | | :header-cell-style="{ |
| | | background: '#fafafa', |
| | | color: '#606266', |
| | | fontWeight: 600, |
| | | padding: '12px 16px', |
| | | whiteSpace: 'nowrap', |
| | | }" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'employeeName'"> |
| | | <div class="flex flex-col"> |
| | | <span class="font-medium">{{ record.employeeName || '-' }}</span> |
| | | <span class="text-muted-foreground font-mono text-xs"> |
| | | {{ record.employeeNo || '' }} |
| | | </span> |
| | | </div> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'deptName'"> |
| | | {{ record.deptName || '-' }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'certType'"> |
| | | <Tag color="blue">{{ record.certTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'remainingDays'"> |
| | | <span |
| | | v-if="record.remainingDays !== null && record.remainingDays !== undefined" |
| | | :class="{ |
| | | 'font-semibold text-red-500': record.remainingDays < 0, |
| | | 'font-semibold text-orange-500': record.remainingDays >= 0 && record.remainingDays <= 30, |
| | | }" |
| | | > |
| | | {{ record.remainingDays < 0 ? `å·²è¿æ ${Math.abs(record.remainingDays)} 天` : `${record.remainingDays} 天` }} |
| | | </span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag :color="CERT_STATUS_COLOR[record.status ?? '']"> |
| | | {{ record.statusName }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button type="link" size="small" @click="showForm(record as SafetyApi.Certificate, true)">详æ
</Button> |
| | | <Button |
| | | v-access:code="['safety:cert:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="showForm(record as SafetyApi.Certificate)" |
| | | >ç¼è¾</Button> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¿æ¬ç¹ç§ä½ä¸è¯ï¼" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.Certificate)" |
| | | > |
| | | <Button |
| | | v-access:code="['safety:cert:delete']" |
| | | type="link" |
| | | danger |
| | | size="small" |
| | | :loading="deletingId === record.id" |
| | | >å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="detail ? 'ç¹ç§ä½ä¸è¯è¯¦æ
' : model.id ? 'ç¼è¾ç¹ç§ä½ä¸è¯' : 'æ°å¢ç¹ç§ä½ä¸è¯'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | :footer="detail ? null : undefined" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form |
| | | ref="formRef" |
| | | :model="model" |
| | | :rules="rules" |
| | | :disabled="detail || saving" |
| | | layout="vertical" |
| | | class="safety-form" |
| | | > |
| | | <FormItem label="æè¯äºº" name="employeeId" class="col-span-full"> |
| | | <EmployeeSelect v-model="model.employeeId" placeholder="è¾å
¥å§åæå·¥å·æç´¢æè¯äºº" /> |
| | | <div class="text-muted-foreground mt-1 text-xs"> |
| | | æè¯äººæ¥èªäººåèµæºçå工档æ¡ï¼å§å / å·¥å· / é¨é¨èªå¨å¸¦åºï¼åå·¥è°å²åå°è´¦ä¼åæ¥æ´æ°ã |
| | | </div> |
| | | </FormItem> |
| | | <FormItem label="è¯ä»¶ç±»å" name="certType"> |
| | | <Select v-model:value="model.certType" :options="CERT_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="è¯ä»¶ç¼å·" name="certNo"> |
| | | <Input v-model:value="model.certNo" :maxlength="64" placeholder="请è¾å
¥è¯ä»¶ç¼å·" /> |
| | | </FormItem> |
| | | <FormItem label="åè¯æºå
³" name="issuer"> |
| | | <Input v-model:value="model.issuer" :maxlength="100" placeholder="å¦ åºæ¥ç®¡çå±" /> |
| | | </FormItem> |
| | | <FormItem label="åè¯æ¥æ" name="issueDate"> |
| | | <DatePicker v-model:value="model.issueDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="æææè³" name="expireDate"> |
| | | <DatePicker v-model:value="model.expireDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="䏿¬¡å¤å®¡æ¥æ" name="reviewDate"> |
| | | <DatePicker v-model:value="model.reviewDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="å°æé¢è¦æå天æ°" name="warnDays"> |
| | | <InputNumber v-model:value="model.warnDays" :min="0" :max="365" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="2" show-count placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { |
| | | gap: 8px 0; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .safety-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 0 20px; |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | | .safety-form { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | createDrill, |
| | | deleteDrill, |
| | | getDrillPage, |
| | | updateDrill, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { DRILL_TYPE_OPTIONS } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyDrill' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Drill[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.Drill => ({ |
| | | title: '', |
| | | drillType: 'FIRE', |
| | | drillDate: dayjs().format('YYYY-MM-DD'), |
| | | location: '', |
| | | organizerId: undefined, |
| | | participants: '', |
| | | participantCount: 0, |
| | | planDesc: '', |
| | | processDesc: '', |
| | | summary: '', |
| | | durationMinutes: 30, |
| | | attachment: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Drill>(initial()); |
| | | |
| | | const rules = { |
| | | title: [{ required: true, whitespace: true, message: '请è¾å
¥æ¼ç»åç§°' }], |
| | | drillDate: [{ required: true, message: 'è¯·éæ©æ¼ç»æ¥æ' }], |
| | | }; |
| | | |
| | | const columns = [ |
| | | { title: 'æ¼ç»æ¥æ', dataIndex: 'drillDate', width: 130, fixed: 'left' as const }, |
| | | { title: 'æ¼ç»åç§°', dataIndex: 'title', minWidth: 220, ellipsis: true }, |
| | | { title: 'ç±»å', dataIndex: 'drillType', width: 140 }, |
| | | { title: 'æ¶é¿(å)', dataIndex: 'durationMinutes', width: 100, align: 'center' as const }, |
| | | { title: 'ç»ç»äºº', dataIndex: 'organizer', width: 120 }, |
| | | { title: 'æ¼ç»å°ç¹', dataIndex: 'location', width: 160, ellipsis: true }, |
| | | { title: 'åå 人æ°', dataIndex: 'participantCount', width: 110, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const params: any = { ...filters, pageNo: pagination.current, pageSize: pagination.pageSize }; |
| | | if (params.drillDateRange?.length === 2) { |
| | | params.drillDate = params.drillDateRange; |
| | | delete params.drillDateRange; |
| | | } |
| | | const data = await getDrillPage(params); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½æ¼ç»è®°å½å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.Drill) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, title: model.title.trim() }; |
| | | // ç»ç»äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä¸åä¸æäº¤ |
| | | delete data.organizer; |
| | | await (data.id ? updateDrill(data) : createDrill(data)); |
| | | message.success('æ¼ç»è®°å½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Drill) { |
| | | await deleteDrill(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="æ¶é²æ¼ç»" |
| | | description="æ¶é²ãåºæ¥çæ£ã触çµäºæ
ãå±ååæ³æ¼çæ¼ç»ççµåå彿¡£ï¼çåæ¹æ¡ãè¿ç¨ä¸æ»ç»ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="æ¼ç»æ¥æ"> |
| | | <DatePicker.RangePicker v-model:value="filters.drillDateRange" value-format="YYYY-MM-DD" /> |
| | | </FormItem> |
| | | <FormItem label="åç§°"> |
| | | <Input v-model:value="filters.title" allow-clear placeholder="æ¼ç»åç§°" style="width: 170px" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å"> |
| | | <Select v-model:value="filters.drillType" :options="DRILL_TYPE_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 160px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">æ¼ç»è®°å½</span> |
| | | <Button v-access:code="['safety:drill:create']" type="primary" @click="showForm()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å¢æ¼ç» |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1200 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'drillType'"> |
| | | <Tag color="orange">{{ record.drillTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'organizer'"> |
| | | <span v-if="record.organizer">{{ record.organizer }}</span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'durationMinutes'"> |
| | | {{ record.durationMinutes ?? '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button v-access:code="['safety:drill:update']" type="link" size="small" @click="showForm(record as SafetyApi.Drill)">ç¼è¾</Button> |
| | | <Popconfirm title="ç¡®å®å é¤è¯¥æ¼ç»è®°å½ï¼" ok-text="ç¡®å®" cancel-text="åæ¶" @confirm="remove(record as SafetyApi.Drill)"> |
| | | <Button v-access:code="['safety:drill:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾æ¼ç»è®°å½' : 'æ°å¢æ¼ç»è®°å½'" |
| | | :width="760" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form ref="formRef" :model="model" :rules="rules" :disabled="saving" layout="vertical" class="safety-form"> |
| | | <FormItem label="æ¼ç»åç§°" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="å¦ 2026 年度æ¶é²çæ£æ¼ç»" /> |
| | | </FormItem> |
| | | <FormItem label="æ¼ç»ç±»å" name="drillType"> |
| | | <Select v-model:value="model.drillType" :options="DRILL_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="æ¼ç»æ¥æ" name="drillDate"> |
| | | <DatePicker v-model:value="model.drillDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="æ¼ç»å°ç¹" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ ååºä¸ä¾§æ¶é²éé" /> |
| | | </FormItem> |
| | | <FormItem label="ç»ç»äºº" name="organizerId"> |
| | | <EmployeeSelect v-model="model.organizerId" placeholder="è¾å
¥å§åæå·¥å·æç´¢ç»ç»äºº" /> |
| | | </FormItem> |
| | | <FormItem label="æ¶é¿ï¼åéï¼" name="durationMinutes"> |
| | | <InputNumber v-model:value="model.durationMinutes" :min="0" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="åå 人æ°" name="participantCount"> |
| | | <InputNumber v-model:value="model.participantCount" :min="0" style="width: 100%" placeholder="ç空æååèªå¨ç»è®¡" /> |
| | | </FormItem> |
| | | <FormItem label="åå 人å" name="participants" class="col-span-full"> |
| | | <Input v-model:value="model.participants" :maxlength="2000" placeholder="å¤ä¸ªå§åç¨éå·åé" /> |
| | | </FormItem> |
| | | <FormItem label="æ¼ç»æ¹æ¡" name="planDesc" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.planDesc" :maxlength="2000" :rows="2" show-count placeholder="æ¼ç»çç®çãåºæ¯ä¸æµç¨å®æ" /> |
| | | </FormItem> |
| | | <FormItem label="æ¼ç»è¿ç¨è®°å½" name="processDesc" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.processDesc" :maxlength="2000" :rows="3" show-count placeholder="å®é
æ¼ç»è¿ç¨çè®°å½" /> |
| | | </FormItem> |
| | | <FormItem label="æ»ç»ä¸æ¹è¿" name="summary" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.summary" :maxlength="2000" :rows="2" placeholder="æ¼ç»åå¾çææä¸åç»æ¹è¿æªæ½" /> |
| | | </FormItem> |
| | | <FormItem label="éä»¶ï¼ç
§ç/è®°å½è¡¨ï¼" name="attachment" class="col-span-full"> |
| | | <Input v-model:value="model.attachment" :maxlength="2000" placeholder="å¤ä¸ª URL ç¨éå·åé" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Descriptions, |
| | | Form, |
| | | FormItem, |
| | | Image, |
| | | Input, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Steps, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | assignHazard, |
| | | createHazard, |
| | | deleteHazard, |
| | | getHazardPage, |
| | | recheckHazard, |
| | | rectifyHazard, |
| | | updateHazard, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { |
| | | HAZARD_LEVEL_COLOR, |
| | | HAZARD_LEVEL_OPTIONS, |
| | | HAZARD_SOURCE_OPTIONS, |
| | | HAZARD_STATUS_COLOR, |
| | | HAZARD_STATUS_OPTIONS, |
| | | splitUrls, |
| | | } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyHazard' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Hazard[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const editOpen = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | const uploading = ref(false); |
| | | const photoList = ref<string[]>([]); |
| | | |
| | | /** 夿¥ç»è®ºé项 */ |
| | | const recheckOptions = [ |
| | | { label: 'éè¿ï¼éæ£éç¯ï¼', value: 'PASSED' }, |
| | | { label: 'ä¸éè¿ï¼éåæ´æ¹ï¼', value: 'REJECTED' }, |
| | | ]; |
| | | |
| | | /** éç¯æµç¨æ¥éª¤ */ |
| | | const stepItems = [ |
| | | { title: '䏿¥' }, |
| | | { title: 'åæ´¾' }, |
| | | { title: 'æ´æ¹' }, |
| | | { title: '夿¥' }, |
| | | { title: 'éç¯' }, |
| | | ]; |
| | | |
| | | const initial = (): SafetyApi.Hazard => ({ |
| | | title: '', |
| | | level: 'GENERAL', |
| | | source: 'SELF_CHECK', |
| | | location: '', |
| | | description: '', |
| | | photoUrls: '', |
| | | reporterId: undefined, |
| | | // 䏿¥äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä»
ç¨äºå±ç¤º |
| | | reporter: undefined, |
| | | reportTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | rectifyDeadline: undefined, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Hazard>(initial()); |
| | | |
| | | const editRules = { |
| | | title: [{ required: true, whitespace: true, message: '请è¾å
¥éæ£æ é¢' }], |
| | | reporterId: [{ required: true, message: 'è¯·éæ©ä¸æ¥äºº' }], |
| | | }; |
| | | |
| | | const assignOpen = ref(false); |
| | | const assignModel = reactive<{ id: number; assigneeId?: number; rectifyDeadline?: string; remark?: string }>({ |
| | | id: 0, |
| | | assigneeId: undefined, |
| | | }); |
| | | |
| | | const rectifyOpen = ref(false); |
| | | const rectifyModel = reactive<{ id: number; rectifyDesc: string; rectifyPhotoUrls?: string }>({ |
| | | id: 0, |
| | | rectifyDesc: '', |
| | | }); |
| | | const rectifyPhotos = ref<string[]>([]); |
| | | |
| | | const recheckOpen = ref(false); |
| | | const recheckModel = reactive<{ id: number; recheckerId?: number; recheckResult: string; recheckRemark?: string }>({ |
| | | id: 0, |
| | | recheckerId: undefined, |
| | | recheckResult: 'PASSED', |
| | | }); |
| | | |
| | | /** å½å夿¥ç鿣ï¼ç¨äºæ ¡éªã夿¥äººä¸è½ä¸æ´æ¹è´£ä»»äººä¸ºåä¸äººã */ |
| | | const recheckRow = ref<SafetyApi.Hazard>(); |
| | | |
| | | const detailOpen = ref(false); |
| | | const detailRow = ref<null | SafetyApi.Hazard>(null); |
| | | |
| | | const columns = [ |
| | | { title: '鿣ç¼å·', dataIndex: 'code', width: 165, fixed: 'left' as const }, |
| | | { title: 'æ é¢', dataIndex: 'title', minWidth: 200, ellipsis: true }, |
| | | { title: 'ç级', dataIndex: 'level', width: 100, align: 'center' as const }, |
| | | { title: 'æ¥æº', dataIndex: 'source', width: 110 }, |
| | | { title: 'ä½ç½®', dataIndex: 'location', width: 160, ellipsis: true }, |
| | | { title: '䏿¥äºº', dataIndex: 'reporter', width: 100 }, |
| | | { title: '䏿¥æ¶é´', dataIndex: 'reportTime', width: 165 }, |
| | | { title: 'æ´æ¹è´£ä»»äºº', dataIndex: 'assignee', width: 120 }, |
| | | { title: 'æ´æ¹æé', dataIndex: 'rectifyDeadline', width: 150 }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 110, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 230, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getHazardPage({ |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½éæ£å表失败'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showEdit(row?: SafetyApi.Hazard) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | photoList.value = splitUrls(model.photoUrls); |
| | | editOpen.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function handleUpload(e: Event, target: any) { |
| | | const input = e.target as HTMLInputElement; |
| | | const files = Array.from(input.files ?? []); |
| | | if (!files.length) return; |
| | | uploading.value = true; |
| | | try { |
| | | const urls = await uploadSafetyFiles(files, 'safety/hazard'); |
| | | target.value = [...target.value, ...urls]; |
| | | message.success(`å·²ä¸ä¼ ${urls.length} å¼ ç
§ç`); |
| | | } catch { |
| | | message.error('ç
§çä¸ä¼ 失败'); |
| | | } finally { |
| | | uploading.value = false; |
| | | input.value = ''; |
| | | } |
| | | } |
| | | |
| | | async function saveEdit() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { |
| | | ...model, |
| | | title: model.title.trim(), |
| | | photoUrls: photoList.value.join(','), |
| | | }; |
| | | // 䏿¥äººå§å为åªè¯»å¿«ç
§ï¼ä¸åæäº¤ |
| | | delete data.reporter; |
| | | await (data.id ? updateHazard(data) : createHazard(data)); |
| | | message.success('鿣已ä¿å'); |
| | | editOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | function openAssign(row: SafetyApi.Hazard) { |
| | | assignModel.id = row.id!; |
| | | assignModel.assigneeId = row.assigneeId; |
| | | assignModel.rectifyDeadline = row.rectifyDeadline; |
| | | assignModel.remark = ''; |
| | | assignOpen.value = true; |
| | | } |
| | | |
| | | async function saveAssign() { |
| | | if (!assignModel.assigneeId) { |
| | | message.warning('è¯·éæ©æ´æ¹è´£ä»»äºº'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await assignHazard({ |
| | | id: assignModel.id, |
| | | assigneeId: assignModel.assigneeId, |
| | | rectifyDeadline: assignModel.rectifyDeadline, |
| | | remark: assignModel.remark, |
| | | }); |
| | | message.success('已忴¾'); |
| | | assignOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | function openRectify(row: SafetyApi.Hazard) { |
| | | rectifyModel.id = row.id!; |
| | | rectifyModel.rectifyDesc = row.rectifyDesc ?? ''; |
| | | rectifyPhotos.value = splitUrls(row.rectifyPhotoUrls); |
| | | rectifyOpen.value = true; |
| | | } |
| | | |
| | | async function saveRectify() { |
| | | if (!rectifyModel.rectifyDesc.trim()) { |
| | | message.warning('è¯·å¡«åæ´æ¹æªæ½ä¸ç»æ'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await rectifyHazard({ |
| | | ...rectifyModel, |
| | | rectifyDesc: rectifyModel.rectifyDesc.trim(), |
| | | rectifyPhotoUrls: rectifyPhotos.value.join(','), |
| | | }); |
| | | message.success('æ´æ¹ç»æå·²æäº¤ï¼çå¾
夿¥'); |
| | | rectifyOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | function openRecheck(row: SafetyApi.Hazard) { |
| | | recheckModel.id = row.id!; |
| | | recheckModel.recheckerId = undefined; |
| | | recheckModel.recheckResult = 'PASSED'; |
| | | recheckModel.recheckRemark = ''; |
| | | recheckRow.value = row; |
| | | recheckOpen.value = true; |
| | | } |
| | | |
| | | async function saveRecheck() { |
| | | if (!recheckModel.recheckerId) { |
| | | message.warning('è¯·éæ©å¤æ¥äºº'); |
| | | return; |
| | | } |
| | | if (recheckModel.recheckerId === recheckRow.value?.assigneeId) { |
| | | message.warning('夿¥äººä¸è½ä¸æ´æ¹è´£ä»»äººä¸ºåä¸äºº'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await recheckHazard({ |
| | | id: recheckModel.id, |
| | | recheckerId: recheckModel.recheckerId, |
| | | recheckResult: recheckModel.recheckResult, |
| | | recheckRemark: recheckModel.recheckRemark, |
| | | }); |
| | | message.success(recheckModel.recheckResult === 'PASSED' ? '夿¥éè¿ï¼éæ£å·²éç¯' : '夿¥ä¸éè¿ï¼å·²éåæ´æ¹'); |
| | | recheckOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | function openDetail(row: SafetyApi.Hazard) { |
| | | detailRow.value = row; |
| | | detailOpen.value = true; |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Hazard) { |
| | | await deleteHazard(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | function stepIndex(status?: string): number { |
| | | const order = ['REPORTED', 'ASSIGNED', 'RECTIFYING', 'RECHECKING', 'CLOSED']; |
| | | const normalized = status === 'REJECTED' ? 'RECTIFYING' : (status ?? ''); |
| | | const idx = order.indexOf(normalized); |
| | | return idx < 0 ? 0 : idx; |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="鿣å°è´¦" |
| | | description="鿣䏿¥ â åæ´¾ â æ´æ¹ â 夿¥å
¨æµç¨éç¯ç®¡çï¼æ¯æç°åºæç
§åè¯ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="æ é¢"> |
| | | <Input v-model:value="filters.title" allow-clear placeholder="鿣æ é¢" style="width: 160px" /> |
| | | </FormItem> |
| | | <FormItem label="ç级"> |
| | | <Select v-model:value="filters.level" :options="HAZARD_LEVEL_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="ç¶æ"> |
| | | <Select v-model:value="filters.status" :options="HAZARD_STATUS_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="责任人"> |
| | | <Input v-model:value="filters.assignee" allow-clear placeholder="å§å" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">鿣å表</span> |
| | | <Button v-access:code="['safety:hazard:create']" type="primary" @click="showEdit()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | 鿣䏿¥ |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1700 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'level'"> |
| | | <Tag :color="HAZARD_LEVEL_COLOR[record.level ?? '']">{{ record.levelName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'source'">{{ record.sourceName }}</template> |
| | | <template v-else-if="column.dataIndex === 'reportTime'"> |
| | | {{ record.reportTime ? String(record.reportTime).replace('T', ' ') : '-' }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'rectifyDeadline'"> |
| | | <span :class="{ 'font-semibold text-red-500': record.overdue }"> |
| | | {{ record.rectifyDeadline || '-' }} |
| | | <Tag v-if="record.overdue" color="red" class="ml-1">è¶
æ</Tag> |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag :color="HAZARD_STATUS_COLOR[record.status ?? '']">{{ record.statusName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button type="link" size="small" @click="openDetail(record as SafetyApi.Hazard)">详æ
</Button> |
| | | <Button |
| | | v-if="record.status === 'REPORTED' || record.status === 'REJECTED'" |
| | | v-access:code="['safety:hazard:assign']" |
| | | type="link" |
| | | size="small" |
| | | @click="openAssign(record as SafetyApi.Hazard)" |
| | | >åæ´¾</Button> |
| | | <Button |
| | | v-if="record.status === 'RECTIFYING' || record.status === 'REJECTED'" |
| | | v-access:code="['safety:hazard:rectify']" |
| | | type="link" |
| | | size="small" |
| | | @click="openRectify(record as SafetyApi.Hazard)" |
| | | >æ´æ¹</Button> |
| | | <Button |
| | | v-if="record.status === 'RECHECKING'" |
| | | v-access:code="['safety:hazard:recheck']" |
| | | type="link" |
| | | size="small" |
| | | @click="openRecheck(record as SafetyApi.Hazard)" |
| | | >夿¥</Button> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¯¥éæ£ï¼" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.Hazard)" |
| | | > |
| | | <Button v-access:code="['safety:hazard:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="editOpen" |
| | | :title="model.id ? 'ç¼è¾éæ£' : '鿣䏿¥'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="saveEdit" |
| | | > |
| | | <Form |
| | | ref="formRef" |
| | | :model="model" |
| | | :rules="editRules" |
| | | :disabled="saving" |
| | | layout="vertical" |
| | | class="safety-form" |
| | | > |
| | | <FormItem label="鿣æ é¢" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="å¦ 1#é
çµå®¤é¨ç¦æåï¼æ å
³äººåå¯éæè¿å
¥" /> |
| | | </FormItem> |
| | | <FormItem label="鿣ç级" name="level"> |
| | | <Select v-model:value="model.level" :options="HAZARD_LEVEL_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="éæ£æ¥æº" name="source"> |
| | | <Select v-model:value="model.source" :options="HAZARD_SOURCE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="鿣ä½ç½®" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ 1#é«åé
çµå®¤" /> |
| | | </FormItem> |
| | | <FormItem label="æ´æ¹æé" name="rectifyDeadline"> |
| | | <DatePicker v-model:value="model.rectifyDeadline" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="䏿¥äºº" name="reporterId"> |
| | | <EmployeeSelect v-model="model.reporterId" placeholder="è¾å
¥å§åæå·¥å·æç´¢ä¸æ¥äºº" /> |
| | | </FormItem> |
| | | <FormItem label="䏿¥æ¶é´" name="reportTime"> |
| | | <DatePicker |
| | | v-model:value="model.reportTime" |
| | | show-time |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | class="w-full" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="鿣æè¿°" name="description" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.description" :maxlength="2000" :rows="3" show-count placeholder="æè¿°éæ£çå
·ä½æ
åµåå¯è½é æçåæ" /> |
| | | </FormItem> |
| | | <FormItem label="ç°åºç
§çï¼ç§»å¨ç«¯å¯ç´æ¥æç
§ä¸ä¼ ï¼" class="col-span-full"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <Image.PreviewGroup> |
| | | <Image v-for="url in photoList" :key="url" :src="url" :width="72" :height="72" class="rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <label class="photo-btn"> |
| | | <IconifyIcon class="size-5" icon="lucide:camera" /> |
| | | <span class="mt-1 text-xs">{{ uploading ? 'ä¸ä¼ ä¸' : 'æç
§' }}</span> |
| | | <input type="file" accept="image/*" capture="environment" multiple class="hidden" :disabled="uploading" @change="handleUpload($event, photoList)" /> |
| | | </label> |
| | | </div> |
| | | <div class="text-muted-foreground mt-1 text-xs">ææºä¸ç¹å»ãæç
§ãç´æ¥è°ç¨æåå¤´ï¼æå¤ä¸æ¬¡ 9 å¼ ã</div> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="assignOpen" title="åæ´¾æ´æ¹è´£ä»»äºº" :confirm-loading="saving" ok-text="ç¡®è®¤åæ´¾" cancel-text="åæ¶" @ok="saveAssign"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="æ´æ¹è´£ä»»äºº" required> |
| | | <EmployeeSelect v-model="assignModel.assigneeId" placeholder="è¾å
¥å§åæå·¥å·æç´¢æ´æ¹è´£ä»»äºº" /> |
| | | </FormItem> |
| | | <FormItem label="æ´æ¹æé"> |
| | | <DatePicker v-model:value="assignModel.rectifyDeadline" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="åæ´¾è¯´æ"> |
| | | <Input.TextArea v-model:value="assignModel.remark" :maxlength="500" :rows="2" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="rectifyOpen" title="æäº¤æ´æ¹ç»æ" :confirm-loading="saving" ok-text="æäº¤æ´æ¹" cancel-text="åæ¶" @ok="saveRectify"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="æ´æ¹æªæ½ä¸ç»æ" required> |
| | | <Input.TextArea v-model:value="rectifyModel.rectifyDesc" :maxlength="2000" :rows="4" show-count placeholder="æè¿°å·²éåçæ´æ¹æªæ½å宿æ
åµ" /> |
| | | </FormItem> |
| | | <FormItem label="æ´æ¹åç
§ç"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <Image.PreviewGroup> |
| | | <Image v-for="url in rectifyPhotos" :key="url" :src="url" :width="72" :height="72" class="rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <label class="photo-btn"> |
| | | <IconifyIcon class="size-5" icon="lucide:camera" /> |
| | | <span class="mt-1 text-xs">{{ uploading ? 'ä¸ä¼ ä¸' : 'æç
§' }}</span> |
| | | <input type="file" accept="image/*" capture="environment" multiple class="hidden" :disabled="uploading" @change="handleUpload($event, rectifyPhotos)" /> |
| | | </label> |
| | | </div> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="recheckOpen" title="鿣夿¥" :confirm-loading="saving" ok-text="æäº¤å¤æ¥" cancel-text="åæ¶" @ok="saveRecheck"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="夿¥äºº" required> |
| | | <EmployeeSelect v-model="recheckModel.recheckerId" placeholder="è¾å
¥å§åæå·¥å·æç´¢å¤æ¥äºº" /> |
| | | <div class="text-muted-foreground mt-1 text-xs">夿¥äººä¸è½ä¸æ´æ¹è´£ä»»äººä¸ºåä¸äººã</div> |
| | | </FormItem> |
| | | <FormItem label="夿¥ç»è®º" required> |
| | | <Select v-model:value="recheckModel.recheckResult" :options="recheckOptions" /> |
| | | </FormItem> |
| | | <FormItem label="夿¥æè§"> |
| | | <Input.TextArea v-model:value="recheckModel.recheckRemark" :maxlength="500" :rows="2" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="detailOpen" title="éæ£è¯¦æ
" :width="820" :footer="null"> |
| | | <template v-if="detailRow"> |
| | | <Steps :current="stepIndex(detailRow.status)" size="small" class="mb-5 mt-2" :items="stepItems" /> |
| | | <Descriptions bordered size="small" :column="2"> |
| | | <Descriptions.Item label="鿣ç¼å·">{{ detailRow.code }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç¶æ"> |
| | | <Tag :color="HAZARD_STATUS_COLOR[detailRow.status ?? '']">{{ detailRow.statusName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="æ é¢" :span="2">{{ detailRow.title }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç级"> |
| | | <Tag :color="HAZARD_LEVEL_COLOR[detailRow.level ?? '']">{{ detailRow.levelName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="æ¥æº">{{ detailRow.sourceName }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä½ç½®">{{ detailRow.location || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="䏿¥äºº">{{ detailRow.reporter }}</Descriptions.Item> |
| | | <Descriptions.Item label="䏿¥æ¶é´">{{ detailRow.reportTime ? String(detailRow.reportTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ´æ¹è´£ä»»äºº">{{ detailRow.assignee || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ´æ¹æé"> |
| | | {{ detailRow.rectifyDeadline || '-' }} |
| | | <Tag v-if="detailRow.overdue" color="red" class="ml-1">è¶
æ</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="鿣æè¿°" :span="2">{{ detailRow.description || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç°åºç
§ç" :span="2"> |
| | | <Image.PreviewGroup v-if="splitUrls(detailRow.photoUrls).length"> |
| | | <Image v-for="url in splitUrls(detailRow.photoUrls)" :key="url" :src="url" :width="80" :height="80" class="mr-2 rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <span v-else>-</span> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="æ´æ¹æªæ½" :span="2">{{ detailRow.rectifyDesc || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ´æ¹åç
§ç" :span="2"> |
| | | <Image.PreviewGroup v-if="splitUrls(detailRow.rectifyPhotoUrls).length"> |
| | | <Image v-for="url in splitUrls(detailRow.rectifyPhotoUrls)" :key="url" :src="url" :width="80" :height="80" class="mr-2 rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <span v-else>-</span> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="夿¥äºº">{{ detailRow.rechecker || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="夿¥ç»è®º">{{ detailRow.recheckResultName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="夿¥æè§" :span="2">{{ detailRow.recheckRemark || '-' }}</Descriptions.Item> |
| | | </Descriptions> |
| | | </template> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | .photo-btn { |
| | | display: flex; |
| | | width: 72px; |
| | | height: 72px; |
| | | cursor: pointer; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 6px; |
| | | border: 1px dashed hsl(var(--border)); |
| | | color: #909399; |
| | | } |
| | | .photo-btn:hover { border-color: #60a5fa; color: #3b82f6; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Switch, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop/index'; |
| | | import { |
| | | createInspectionPoint, |
| | | deleteInspectionPoint, |
| | | getInspectionPointPage, |
| | | updateInspectionPoint, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { INSPECT_CYCLE_OPTIONS, POINT_TYPE_OPTIONS } from '../../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyInspectionPoint' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.InspectionPoint[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.InspectionPoint => ({ |
| | | code: '', |
| | | name: '', |
| | | pointType: 'HIGH_VOLTAGE_ROOM', |
| | | workshopId: undefined, |
| | | // è½¦é´ / 责任人为å端åå¡«çå¿«ç
§å段ï¼ä»
ç¨äºå±ç¤º |
| | | workshopName: undefined, |
| | | location: '', |
| | | inspectItems: '1. é¨éä¸è¦ç¤ºæ è¯å®å¥½\n2. ç»ç¼å·¥å¨å
·å¨æææå
\n3. æ¶é²å¨æå¨ä½å¯ç¨\n4. è®°å½æ¬å¡«å宿´', |
| | | inspectCycle: 'DAILY', |
| | | ownerId: undefined, |
| | | ownerName: undefined, |
| | | status: 0, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.InspectionPoint>(initial()); |
| | | |
| | | /** 车é´ä¸æéé¡¹ï¼æ¥æºï¼MES 车é´ä¸»æ°æ®ï¼ */ |
| | | const workshopOptions = ref<{ label: string; value: number }[]>([]); |
| | | |
| | | async function loadWorkshops() { |
| | | try { |
| | | const list = await getWorkshopSimpleList(); |
| | | workshopOptions.value = (list || []).map((w) => ({ |
| | | label: w.name!, |
| | | value: w.id!, |
| | | })); |
| | | } catch { |
| | | message.error('å 载车é´å表失败'); |
| | | } |
| | | } |
| | | |
| | | const rules = { |
| | | code: [{ required: true, whitespace: true, message: '请è¾å
¥ç¹ä½ç¼ç ' }], |
| | | name: [{ required: true, whitespace: true, message: '请è¾å
¥ç¹ä½åç§°' }], |
| | | pointType: [{ required: true, message: 'è¯·éæ©ç¹ä½ç±»å' }], |
| | | }; |
| | | |
| | | const columns = [ |
| | | { title: 'ç¹ä½ç¼ç ', dataIndex: 'code', width: 130, fixed: 'left' as const }, |
| | | { title: 'ç¹ä½åç§°', dataIndex: 'name', width: 170 }, |
| | | { title: 'ç±»å', dataIndex: 'pointType', width: 130 }, |
| | | { title: 'æå±åºå', dataIndex: 'workshopName', width: 130 }, |
| | | { title: 'å
·ä½ä½ç½®', dataIndex: 'location', width: 170, ellipsis: true }, |
| | | { title: 'å·¡æ£å¨æ', dataIndex: 'inspectCycle', width: 110, align: 'center' as const }, |
| | | { title: '责任人', dataIndex: 'ownerName', width: 110 }, |
| | | { title: 'æè¿å·¡æ£', dataIndex: 'lastInspectTime', width: 165 }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 90, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getInspectionPointPage({ |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å 载巡æ£ç¹ä½å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.InspectionPoint) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, code: model.code.trim(), name: model.name.trim() }; |
| | | // 车é´åç§° / 责任人å§å为åªè¯»å¿«ç
§ï¼ä¸åæäº¤ |
| | | delete data.workshopName; |
| | | delete data.ownerName; |
| | | await (data.id ? updateInspectionPoint(data) : createInspectionPoint(data)); |
| | | message.success('å·¡æ£ç¹ä½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.InspectionPoint) { |
| | | await deleteInspectionPoint(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | void loadWorkshops(); |
| | | void load(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="å·¡æ£ç¹ä½" |
| | | description="é«åé
çµå®¤ãæºç»åºãå±åååºçæ¥å¸¸å®å
¨å·¡æ£ç¹ä½çå®ä¹ä¸å·¡æ£å¨æã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="ç¼ç / åç§°"> |
| | | <Input v-model:value="filters.code" allow-clear placeholder="ç¹ä½ç¼ç " style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å"> |
| | | <Select |
| | | v-model:value="filters.pointType" |
| | | :options="POINT_TYPE_OPTIONS" |
| | | allow-clear |
| | | placeholder="å
¨é¨ç±»å" |
| | | style="width: 160px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="åºå"> |
| | | <Input v-model:value="filters.workshopName" allow-clear placeholder="æå±åºå" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">å·¡æ£ç¹ä½å表</span> |
| | | <Button |
| | | v-access:code="['safety:inspection-point:create']" |
| | | type="primary" |
| | | @click="showForm()" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:plus" /> |
| | | </template> |
| | | æ°å¢ç¹ä½ |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1450 }" |
| | | size="middle" |
| | | :locale="{ emptyText: 'ææ å·¡æ£ç¹ä½ï¼è¯·å
æ°å¢' }" |
| | | :header-cell-style="{ |
| | | background: '#fafafa', |
| | | color: '#606266', |
| | | fontWeight: 600, |
| | | padding: '12px 16px', |
| | | }" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'pointType'"> |
| | | <Tag color="blue">{{ record.pointTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'inspectCycle'"> |
| | | <Tag>{{ record.inspectCycleName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'lastInspectTime'"> |
| | | <span v-if="record.lastInspectTime">{{ String(record.lastInspectTime).replace('T', ' ') }}</span> |
| | | <span v-else class="text-gray-400">仿ªå·¡æ£</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag :color="record.status === 0 ? 'green' : 'default'"> |
| | | {{ record.status === 0 ? 'å¯ç¨' : 'åç¨' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button |
| | | v-access:code="['safety:inspection-point:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="showForm(record as SafetyApi.InspectionPoint)" |
| | | >ç¼è¾</Button> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¯¥å·¡æ£ç¹ä½ï¼" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.InspectionPoint)" |
| | | > |
| | | <Button |
| | | v-access:code="['safety:inspection-point:delete']" |
| | | type="link" |
| | | danger |
| | | size="small" |
| | | >å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾å·¡æ£ç¹ä½' : 'æ°å¢å·¡æ£ç¹ä½'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form |
| | | ref="formRef" |
| | | :model="model" |
| | | :rules="rules" |
| | | :disabled="saving" |
| | | layout="vertical" |
| | | class="safety-form" |
| | | > |
| | | <FormItem label="ç¹ä½ç¼ç " name="code"> |
| | | <Input v-model:value="model.code" :maxlength="64" placeholder="å¦ PD-001" /> |
| | | </FormItem> |
| | | <FormItem label="ç¹ä½åç§°" name="name"> |
| | | <Input v-model:value="model.name" :maxlength="100" placeholder="å¦ 1#é«åé
çµå®¤" /> |
| | | </FormItem> |
| | | <FormItem label="ç¹ä½ç±»å" name="pointType"> |
| | | <Select v-model:value="model.pointType" :options="POINT_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£å¨æ" name="inspectCycle"> |
| | | <Select v-model:value="model.inspectCycle" :options="INSPECT_CYCLE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="æå±è½¦é´/åºå" name="workshopId"> |
| | | <Select |
| | | v-model:value="model.workshopId" |
| | | :options="workshopOptions" |
| | | show-search |
| | | :filter-option="(i: string, o: any) => String(o.label).includes(i)" |
| | | placeholder="è¯·éæ©è½¦é´" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="责任人" name="ownerId"> |
| | | <EmployeeSelect v-model="model.ownerId" placeholder="è¾å
¥å§åæå·¥å·æç´¢è´£ä»»äºº" /> |
| | | </FormItem> |
| | | <FormItem label="å
·ä½ä½ç½®" name="location" class="col-span-full"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ ååºè¥¿åè§ 1 å·é
çµæ¿" /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£é¡¹ç®æ¸
å" name="inspectItems" class="col-span-full"> |
| | | <Input.TextArea |
| | | v-model:value="model.inspectItems" |
| | | :maxlength="2000" |
| | | :rows="5" |
| | | show-count |
| | | placeholder="æ¯è¡ä¸é¡¹æ£æ¥å
容" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="å¯ç¨" name="status"> |
| | | <Switch |
| | | :checked="model.status === 0" |
| | | @change="(v: any) => (model.status = v ? 0 : 1)" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { |
| | | gap: 8px 0; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .safety-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 0 20px; |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | | .safety-form { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Image, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | createInspection, |
| | | deleteInspection, |
| | | getInspectionPage, |
| | | getInspectionPointSimpleList, |
| | | updateInspection, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { INSPECTION_RESULT_OPTIONS, splitUrls } from '../../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyInspectionRecord' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Inspection[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const points = ref<SafetyApi.InspectionPoint[]>([]); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | const uploading = ref(false); |
| | | const photoList = ref<string[]>([]); |
| | | |
| | | const initial = (): SafetyApi.Inspection => ({ |
| | | pointId: 0, |
| | | inspectTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | inspectorId: undefined, |
| | | // å·¡æ£äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä»
ç¨äºå±ç¤º |
| | | inspector: undefined, |
| | | result: 'NORMAL', |
| | | content: '', |
| | | photoUrls: '', |
| | | hazardCount: 0, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Inspection>(initial()); |
| | | |
| | | const rules = { |
| | | pointId: [{ required: true, message: 'è¯·éæ©å·¡æ£ç¹ä½' }], |
| | | inspectTime: [{ required: true, message: 'è¯·éæ©å·¡æ£æ¶é´' }], |
| | | inspectorId: [{ required: true, message: 'è¯·éæ©å·¡æ£äºº' }], |
| | | }; |
| | | |
| | | const pointOptions = computed(() => |
| | | points.value.map((p) => ({ |
| | | label: `${p.name}ï¼${p.pointTypeName}ï¼`, |
| | | value: p.id, |
| | | })), |
| | | ); |
| | | |
| | | const columns = [ |
| | | { title: 'å·¡æ£æ¶é´', dataIndex: 'inspectTime', width: 165, fixed: 'left' as const }, |
| | | { title: 'ç¹ä½åç§°', dataIndex: 'pointName', width: 170 }, |
| | | { title: 'ç±»å', dataIndex: 'pointType', width: 130 }, |
| | | { title: 'å·¡æ£äºº', dataIndex: 'inspector', width: 110 }, |
| | | { title: 'ç»æ', dataIndex: 'result', width: 95, align: 'center' as const }, |
| | | { title: 'å·¡æ£æ
åµ', dataIndex: 'content', minWidth: 240, ellipsis: true }, |
| | | { title: 'ç°åºç
§ç', dataIndex: 'photoUrls', width: 130, align: 'center' as const }, |
| | | { title: '鿣æ°', dataIndex: 'hazardCount', width: 90, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const params: any = { |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }; |
| | | if (params.inspectTimeRange?.length === 2) { |
| | | params.inspectTime = params.inspectTimeRange; |
| | | delete params.inspectTimeRange; |
| | | } |
| | | const data = await getInspectionPage(params); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å 载巡æ£è®°å½å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadPoints() { |
| | | try { |
| | | points.value = await getInspectionPointSimpleList(); |
| | | } catch { |
| | | message.error('å 载巡æ£ç¹ä½å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.Inspection) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | photoList.value = splitUrls(model.photoUrls); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | /** ç§»å¨ç«¯é£æ ¼ï¼éæ©ç
§çå³ä¸ä¼ ï¼è¿å URL ç´¯ç§¯å° photoList */ |
| | | async function handleUpload(e: Event) { |
| | | const input = e.target as HTMLInputElement; |
| | | const files = Array.from(input.files ?? []); |
| | | if (files.length === 0) return; |
| | | uploading.value = true; |
| | | try { |
| | | const urls = await uploadSafetyFiles(files, 'safety/inspection'); |
| | | photoList.value = [...photoList.value, ...urls]; |
| | | message.success(`å·²ä¸ä¼ ${urls.length} å¼ ç
§ç`); |
| | | } catch { |
| | | message.error('ç
§çä¸ä¼ 失败'); |
| | | } finally { |
| | | uploading.value = false; |
| | | input.value = ''; |
| | | } |
| | | } |
| | | |
| | | function removePhoto(url: string) { |
| | | photoList.value = photoList.value.filter((u) => u !== url); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { |
| | | ...model, |
| | | photoUrls: photoList.value.join(','), |
| | | }; |
| | | // å·¡æ£äººå§å为åªè¯»å¿«ç
§ï¼ä¸åæäº¤ |
| | | delete data.inspector; |
| | | await (data.id ? updateInspection(data) : createInspection(data)); |
| | | message.success('å·¡æ£è®°å½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Inspection) { |
| | | await deleteInspection(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | void loadPoints(); |
| | | void load(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="å·¡æ£è®°å½" |
| | | description="é«åé
çµå®¤ãæºç»åºçç¹ä½çæ¥å¸¸å·¡æ£è®°å½ï¼ç°åºå¯æç
§ä¸ä¼ ãå¼å¸¸æ
åµè¯·å°ã鿣å°è´¦ã䏿¥ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="å·¡æ£æ¶é´"> |
| | | <DatePicker.RangePicker |
| | | v-model:value="filters.inspectTimeRange" |
| | | show-time |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="ç¹ä½"> |
| | | <Select |
| | | v-model:value="filters.pointId" |
| | | :options="pointOptions" |
| | | allow-clear |
| | | show-search |
| | | placeholder="å
¨é¨ç¹ä½" |
| | | style="width: 210px" |
| | | :filter-option="(input: string, opt: any) => String(opt.label).includes(input)" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£äºº"> |
| | | <Input v-model:value="filters.inspector" allow-clear placeholder="å§å" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem label="ç»æ"> |
| | | <Select |
| | | v-model:value="filters.result" |
| | | :options="INSPECTION_RESULT_OPTIONS" |
| | | allow-clear |
| | | placeholder="å
¨é¨" |
| | | style="width: 110px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">å·¡æ£è®°å½</span> |
| | | <Button |
| | | v-access:code="['safety:inspection:create']" |
| | | type="primary" |
| | | @click="showForm()" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:plus" /> |
| | | </template> |
| | | æ°å¢å·¡æ£ |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1350 }" |
| | | size="middle" |
| | | :locale="{ emptyText: 'ææ å·¡æ£è®°å½' }" |
| | | :header-cell-style="{ |
| | | background: '#fafafa', |
| | | color: '#606266', |
| | | fontWeight: 600, |
| | | padding: '12px 16px', |
| | | }" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'inspectTime'"> |
| | | {{ String(record.inspectTime).replace('T', ' ') }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'pointType'"> |
| | | <Tag color="blue">{{ record.pointTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'result'"> |
| | | <Tag :color="record.result === 'NORMAL' ? 'green' : 'red'"> |
| | | {{ record.resultName }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'photoUrls'"> |
| | | <Image.PreviewGroup v-if="splitUrls(record.photoUrls).length"> |
| | | <Image |
| | | v-for="(url, idx) in splitUrls(record.photoUrls)" |
| | | :key="url" |
| | | :src="url" |
| | | :width="28" |
| | | :height="28" |
| | | class="mr-1 rounded object-cover" |
| | | :style="{ display: idx > 2 ? 'none' : 'inline-block' }" |
| | | /> |
| | | </Image.PreviewGroup> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'hazardCount'"> |
| | | <Tag v-if="(record.hazardCount ?? 0) > 0" color="red">{{ record.hazardCount }}</Tag> |
| | | <span v-else class="text-gray-300">0</span> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button |
| | | v-access:code="['safety:inspection:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="showForm(record as SafetyApi.Inspection)" |
| | | >ç¼è¾</Button> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¯¥å·¡æ£è®°å½ï¼" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.Inspection)" |
| | | > |
| | | <Button |
| | | v-access:code="['safety:inspection:delete']" |
| | | type="link" |
| | | danger |
| | | size="small" |
| | | >å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾å·¡æ£è®°å½' : 'æ°å¢å·¡æ£è®°å½'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form |
| | | ref="formRef" |
| | | :model="model" |
| | | :rules="rules" |
| | | :disabled="saving" |
| | | layout="vertical" |
| | | class="safety-form" |
| | | > |
| | | <FormItem label="å·¡æ£ç¹ä½" name="pointId"> |
| | | <Select |
| | | v-model:value="model.pointId" |
| | | :options="pointOptions" |
| | | show-search |
| | | placeholder="è¯·éæ©å·¡æ£ç¹ä½" |
| | | :filter-option="(input: string, opt: any) => String(opt.label).includes(input)" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£æ¶é´" name="inspectTime"> |
| | | <DatePicker |
| | | v-model:value="model.inspectTime" |
| | | show-time |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | class="w-full" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£äºº" name="inspectorId"> |
| | | <EmployeeSelect v-model="model.inspectorId" placeholder="è¾å
¥å§åæå·¥å·æç´¢å·¡æ£äºº" /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£ç»æ" name="result"> |
| | | <Select v-model:value="model.result" :options="INSPECTION_RESULT_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="æ¬æ¬¡åç°éæ£æ°" name="hazardCount"> |
| | | <InputNumber v-model:value="model.hazardCount" :min="0" :max="999" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å·¡æ£æ
åµ" name="content" class="col-span-full"> |
| | | <Input.TextArea |
| | | v-model:value="model.content" |
| | | :maxlength="2000" |
| | | :rows="3" |
| | | show-count |
| | | placeholder="è®°å½å·¡æ£è¿ç¨ä¸çå®é
æ
åµï¼å¼å¸¸é¡¹è¯·å
·ä½æè¿°" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="ç°åºç
§çï¼ç§»å¨ç«¯å¯ç´æ¥æç
§ä¸ä¼ ï¼" class="col-span-full"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <Image.PreviewGroup> |
| | | <div |
| | | v-for="url in photoList" |
| | | :key="url" |
| | | class="group relative" |
| | | > |
| | | <Image :src="url" :width="72" :height="72" class="rounded object-cover" /> |
| | | <button |
| | | type="button" |
| | | class="absolute -right-1 -top-1 hidden size-5 items-center justify-center rounded-full bg-red-500 text-xs text-white group-hover:flex" |
| | | @click="removePhoto(url)" |
| | | > |
| | | Ã |
| | | </button> |
| | | </div> |
| | | </Image.PreviewGroup> |
| | | <label |
| | | class="flex size-[72px] cursor-pointer flex-col items-center justify-center rounded border border-dashed border-[hsl(var(--border))] text-gray-400 hover:border-blue-400 hover:text-blue-500" |
| | | > |
| | | <IconifyIcon class="size-5" icon="lucide:camera" /> |
| | | <span class="mt-1 text-xs">{{ uploading ? 'ä¸ä¼ ä¸' : 'æç
§' }}</span> |
| | | <input |
| | | type="file" |
| | | accept="image/*" |
| | | capture="environment" |
| | | multiple |
| | | class="hidden" |
| | | :disabled="uploading" |
| | | @change="handleUpload" |
| | | /> |
| | | </label> |
| | | </div> |
| | | <div class="text-muted-foreground mt-1 text-xs"> |
| | | ææºä¸ç¹å»ãæç
§ãå¯ç´æ¥è°ç¨æåå¤´ï¼æå¤ä¸æ¬¡ä¸ä¼ 9 å¼ ã |
| | | </div> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { |
| | | gap: 8px 0; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .safety-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 0 20px; |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | | .safety-form { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | message, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { exportSafetyLedger, getSafetyOverview } from '#/api/safety'; |
| | | |
| | | defineOptions({ name: 'SafetyLedgerExport' }); |
| | | |
| | | /** å°è´¦ç±»åé项 */ |
| | | const LEDGER_TYPES = [ |
| | | { label: 'å
¨å¥æ ååå°è´¦', value: 'ALL' }, |
| | | { label: 'ç¹ç§ä½ä¸è¯å°è´¦', value: 'CERT' }, |
| | | { label: 'å®å
¨å·¡æ£å°è´¦', value: 'INSPECTION' }, |
| | | { label: '鿣å°è´¦', value: 'HAZARD' }, |
| | | { label: 'é«å±ä½ä¸ç¥¨å°è´¦', value: 'PERMIT' }, |
| | | { label: 'åºæ¥ç©èµå°è´¦', value: 'MATERIAL' }, |
| | | { label: 'å®å
¨å¹è®å°è´¦', value: 'TRAINING' }, |
| | | { label: 'æ¶é²æ¼ç»å°è´¦', value: 'DRILL' }, |
| | | { label: 'äºæ
ç»è®°å°è´¦', value: 'ACCIDENT' }, |
| | | ]; |
| | | |
| | | const exportType = ref('ALL'); |
| | | const dateRange = ref<[string, string] | undefined>(); |
| | | const exporting = ref(false); |
| | | |
| | | /** é¦é¡µç»è®¡ */ |
| | | const overview = ref<null | SafetyApi.Overview>(null); |
| | | const loadingOverview = ref(false); |
| | | |
| | | const summaryRows = computed(() => { |
| | | const v = overview.value; |
| | | if (!v) return []; |
| | | return [ |
| | | { name: 'ç¹ç§ä½ä¸è¯å°è´¦', total: v.certTotal, detail: `å³å°å°æ ${v.certExpiring} æ¬ / å·²è¿æ ${v.certExpired} æ¬` }, |
| | | { name: '鿣å°è´¦', total: v.hazardTotal, detail: `æªéç¯ ${v.hazardOpen} æ¡ / è¶
æ ${v.hazardOverdue} æ¡` }, |
| | | { name: 'é«å±ä½ä¸ç¥¨å°è´¦', total: (v.permitPending ?? 0) + (v.permitInProgress ?? 0), detail: `å¾
å®¡æ¹ ${v.permitPending} å¼ / ä½ä¸ä¸ ${v.permitInProgress} å¼ ` }, |
| | | { name: 'åºæ¥ç©èµå°è´¦', total: v.materialBelowSafeStock, detail: `ä½äºå®å
¨åºå ${v.materialBelowSafeStock} 项 / ä¸´è¿æææ ${v.materialExpiring} 项` }, |
| | | { name: 'å®å
¨å¹è®å°è´¦', total: v.trainingThisMonth, detail: 'æ¬æåºæ¬¡' }, |
| | | { name: 'æ¶é²æ¼ç»å°è´¦', total: v.drillThisMonth, detail: 'æ¬æåºæ¬¡' }, |
| | | { name: 'äºæ
ç»è®°å°è´¦', total: v.accidentThisMonth, detail: 'æ¬æç»è®°' }, |
| | | ]; |
| | | }); |
| | | |
| | | const columns = [ |
| | | { title: 'å°è´¦åç§°', dataIndex: 'name', width: 220 }, |
| | | { title: 'æ°é', dataIndex: 'total', width: 120, align: 'center' as const }, |
| | | { title: 'æç»', dataIndex: 'detail' }, |
| | | ]; |
| | | |
| | | async function loadOverview() { |
| | | loadingOverview.value = true; |
| | | try { |
| | | overview.value = await getSafetyOverview(); |
| | | } catch { |
| | | message.error('å è½½å°è´¦ç»è®¡å¤±è´¥'); |
| | | } finally { |
| | | loadingOverview.value = false; |
| | | } |
| | | } |
| | | |
| | | async function handleExport() { |
| | | exporting.value = true; |
| | | try { |
| | | const params: any = { type: exportType.value }; |
| | | if (dateRange.value?.length === 2) { |
| | | params.startDate = dateRange.value[0]; |
| | | params.endDate = dateRange.value[1]; |
| | | } |
| | | const fileName = |
| | | LEDGER_TYPES.find((t) => t.value === exportType.value)?.label ?? |
| | | 'å®å
¨ç产æ ååå°è´¦'; |
| | | downloadFileFromBlobPart({ |
| | | fileName: fileName + '.xlsx', |
| | | source: await exportSafetyLedger(params), |
| | | }); |
| | | message.success('å°è´¦å·²å¼å§ä¸è½½'); |
| | | } catch { |
| | | message.error('å°è´¦å¯¼åºå¤±è´¥ï¼è¯·éè¯'); |
| | | } finally { |
| | | exporting.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => loadOverview()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="æ ååå°è´¦" |
| | | description="çµåè¡ä¸å®å
¨æ ååæ£æ¥æéçåç±»å°è´¦ï¼æ¯ææ´å¥ææå类导åºä¸º Excelã" |
| | | auto-content-height |
| | | > |
| | | <Card title="å°è´¦å¯¼åº" class="mb-4"> |
| | | <div class="mb-4 flex flex-wrap items-center gap-3"> |
| | | <span class="text-sm">å°è´¦ç±»åï¼</span> |
| | | <Select |
| | | v-model:value="exportType" |
| | | :options="LEDGER_TYPES" |
| | | style="width: 200px" |
| | | /> |
| | | <span class="ml-4 text-sm">ç»è®¡å¨æï¼</span> |
| | | <DatePicker.RangePicker |
| | | v-model:value="dateRange" |
| | | value-format="YYYY-MM-DD" |
| | | :placeholder="['å¼å§æ¥æ', 'ç»ææ¥æ']" |
| | | /> |
| | | <Space> |
| | | <Button type="primary" :loading="exporting" @click="handleExport"> |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:download" /> |
| | | </template> |
| | | 导åºå°è´¦ |
| | | </Button> |
| | | </Space> |
| | | </div> |
| | | |
| | | <Alert |
| | | type="info" |
| | | show-icon |
| | | message="导åºè¯´æ" |
| | | > |
| | | <template #description> |
| | | <ul class="m-0 pl-4 leading-6"> |
| | | <li>éæ©ãå
¨å¥æ ååå°è´¦ãä¼å¯¼åºå¤ Sheet çå个 Excelï¼å
å«å
¨é¨å°è´¦ï¼</li> |
| | | <li>éæ©åç±»å°è´¦ååªå¯¼åºè¯¥ç±»æ°æ®ï¼ä¾¿äºåç¬å½æ¡£æéæ£ï¼</li> |
| | | <li>å·¡æ£ã鿣ãä½ä¸ç¥¨ãç©èµæµæ°´ãå¹è®ãæ¼ç»ãäºæ
æ¯æææ¥æèå´çéï¼</li> |
| | | <li>ç¹ç§ä½ä¸è¯ä¸ç©èµå°è´¦ä¸åæ¶é´èå´ï¼å§ç»å¯¼åºå
¨é¨ã</li> |
| | | </ul> |
| | | </template> |
| | | </Alert> |
| | | </Card> |
| | | |
| | | <Card title="å°è´¦æ°æ®æ¦è§"> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="summaryRows" |
| | | :loading="loadingOverview" |
| | | :pagination="false" |
| | | row-key="name" |
| | | size="middle" |
| | | /> |
| | | </Card> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tabs, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | createMaterial, |
| | | createMaterialStock, |
| | | deleteMaterial, |
| | | getMaterialPage, |
| | | getMaterialStockPage, |
| | | updateMaterial, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { MATERIAL_CATEGORY_COLOR, MATERIAL_CATEGORY_OPTIONS, numberText } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyMaterial' }); |
| | | |
| | | /** å½åé¡µç¾ */ |
| | | const activeTab = ref('material'); |
| | | |
| | | /* ==================== ç©èµå°è´¦ ==================== */ |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Material[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const editOpen = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | /** æææä»
å¨ç»ç¼ç¨å
·/å±ååæ¶å¿
å¡« */ |
| | | const expireRequired = computed(() => |
| | | ['INSULATING', 'HAZARD_CHEMICAL'].includes(model.category), |
| | | ); |
| | | |
| | | const initial = (): SafetyApi.Material => ({ |
| | | code: '', |
| | | name: '', |
| | | category: 'INSULATING', |
| | | spec: '', |
| | | unit: '个', |
| | | stockQty: 0, |
| | | safeStock: 0, |
| | | storageLocation: '', |
| | | expireDate: undefined, |
| | | validityMonths: undefined, |
| | | managerId: undefined, |
| | | status: 0, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Material>(initial()); |
| | | |
| | | const rules = { |
| | | code: [{ required: true, whitespace: true, message: '请è¾å
¥ç©èµç¼ç ' }], |
| | | name: [{ required: true, whitespace: true, message: '请è¾å
¥ç©èµåç§°' }], |
| | | category: [{ required: true, message: 'è¯·éæ©ç©èµç±»å«' }], |
| | | }; |
| | | |
| | | /* ==================== åºå
¥åº ==================== */ |
| | | const stockOpen = ref(false); |
| | | const stockModel = reactive<SafetyApi.MaterialStock>({ |
| | | materialId: 0, |
| | | bizType: 'IN', |
| | | quantity: 1, |
| | | bizDate: dayjs().format('YYYY-MM-DD'), |
| | | operator: '', |
| | | receiver: '', |
| | | purpose: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const stockFilters = reactive<Record<string, any>>({}); |
| | | const stockRows = ref<SafetyApi.MaterialStock[]>([]); |
| | | const stockLoading = ref(false); |
| | | const stockPagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const materialColumns = [ |
| | | { title: 'ç©èµç¼ç ', dataIndex: 'code', width: 140, fixed: 'left' as const }, |
| | | { title: 'ç©èµåç§°', dataIndex: 'name', width: 190 }, |
| | | { title: 'ç±»å«', dataIndex: 'category', width: 130 }, |
| | | { title: 'è§æ ¼åå·', dataIndex: 'spec', width: 150, ellipsis: true }, |
| | | { title: 'åä½', dataIndex: 'unit', width: 80, align: 'center' as const }, |
| | | { title: 'åºå', dataIndex: 'stockQty', width: 110, align: 'right' as const }, |
| | | { title: 'å®å
¨åºå', dataIndex: 'safeStock', width: 110, align: 'right' as const }, |
| | | { title: 'æææè³', dataIndex: 'expireDate', width: 120 }, |
| | | { title: 'åæ¾ä½ç½®', dataIndex: 'storageLocation', width: 130 }, |
| | | { title: 'ä¿ç®¡äºº', dataIndex: 'manager', width: 100 }, |
| | | { title: 'æä½', key: 'action', width: 200, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | const stockColumns = [ |
| | | { title: 'ä¸å¡æ¥æ', dataIndex: 'bizDate', width: 130, fixed: 'left' as const }, |
| | | { title: 'ç©èµåç§°', dataIndex: 'materialName', width: 190 }, |
| | | { title: 'ç±»å«', dataIndex: 'category', width: 130 }, |
| | | { title: 'ä¸å¡ç±»å', dataIndex: 'bizType', width: 100, align: 'center' as const }, |
| | | { title: 'æ°é', dataIndex: 'quantity', width: 100, align: 'right' as const }, |
| | | { title: 'æä½ååºå', dataIndex: 'stockBefore', width: 120, align: 'right' as const }, |
| | | { title: 'æä½ååºå', dataIndex: 'stockAfter', width: 120, align: 'right' as const }, |
| | | { title: 'ç»å人', dataIndex: 'operator', width: 100 }, |
| | | { title: 'é¢ç¨äºº', dataIndex: 'receiver', width: 100 }, |
| | | { title: 'ç¨é/å»å', dataIndex: 'purpose', minWidth: 160, ellipsis: true }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getMaterialPage({ |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½ç©èµå°è´¦å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadStock(reset = false) { |
| | | if (reset) stockPagination.current = 1; |
| | | stockLoading.value = true; |
| | | try { |
| | | const params: any = { |
| | | ...stockFilters, |
| | | pageNo: stockPagination.current, |
| | | pageSize: stockPagination.pageSize, |
| | | }; |
| | | if (params.bizDateRange?.length === 2) { |
| | | params.bizDate = params.bizDateRange; |
| | | delete params.bizDateRange; |
| | | } |
| | | const data = await getMaterialStockPage(params); |
| | | stockRows.value = data.list; |
| | | stockPagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½åºå
¥åºæµæ°´å¤±è´¥'); |
| | | stockRows.value = []; |
| | | } finally { |
| | | stockLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function stockTableChange(page: any) { |
| | | stockPagination.current = page.current ?? 1; |
| | | stockPagination.pageSize = page.pageSize ?? 10; |
| | | void loadStock(); |
| | | } |
| | | |
| | | function showEdit(row?: SafetyApi.Material) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | editOpen.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function saveEdit() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, code: model.code.trim(), name: model.name.trim() }; |
| | | // ä¿ç®¡äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä¸åä¸æäº¤ |
| | | delete data.manager; |
| | | await (data.id ? updateMaterial(data) : createMaterial(data)); |
| | | message.success('ç©èµå·²ä¿å'); |
| | | editOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | function openStock(row: SafetyApi.Material, bizType: string) { |
| | | stockModel.materialId = row.id!; |
| | | stockModel.bizType = bizType; |
| | | stockModel.quantity = 1; |
| | | stockModel.bizDate = dayjs().format('YYYY-MM-DD'); |
| | | stockModel.operator = ''; |
| | | stockModel.receiver = ''; |
| | | stockModel.purpose = ''; |
| | | stockModel.remark = ''; |
| | | stockOpen.value = true; |
| | | } |
| | | |
| | | async function saveStock() { |
| | | if (!stockModel.quantity || stockModel.quantity <= 0) { |
| | | message.warning('æ°éå¿
须大äºé¶'); |
| | | return; |
| | | } |
| | | if (!stockModel.operator.trim()) { |
| | | message.warning('请填åç»å人'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await createMaterialStock({ ...stockModel, operator: stockModel.operator.trim() }); |
| | | message.success(stockModel.bizType === 'IN' ? 'å
¥åºæå' : 'åºåºæå'); |
| | | stockOpen.value = false; |
| | | await load(); |
| | | if (activeTab.value === 'stock') await loadStock(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Material) { |
| | | await deleteMaterial(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | void load(); |
| | | void loadStock(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="åºæ¥ç©èµ" |
| | | description="çµåç»ç¼ç¨å
·ãå±ååãæ¶é²ç©èµççµåå°è´¦ä¸åºå
¥åºæµæ°´ï¼è½åºèªå¨çç¹ãæ¡ä»¶ï¼ç©èµå°è´¦ + åºå
¥åºæµæ°´ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Tabs v-model:active-key="activeTab"> |
| | | <Tabs.TabPane key="material" tab="ç©èµå°è´¦"> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="ç¼ç /åç§°"> |
| | | <Input v-model:value="filters.name" allow-clear placeholder="ç©èµåç§°" style="width: 160px" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å«"> |
| | | <Select v-model:value="filters.category" :options="MATERIAL_CATEGORY_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 150px" /> |
| | | </FormItem> |
| | | <FormItem label="åæ¾ä½ç½®"> |
| | | <Input v-model:value="filters.storageLocation" allow-clear placeholder="ä½ç½®" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">ç©èµå°è´¦</span> |
| | | <Button v-access:code="['safety:material:create']" type="primary" @click="showEdit()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å¢ç©èµ |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="materialColumns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1600 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'category'"> |
| | | <Tag :color="MATERIAL_CATEGORY_COLOR[record.category ?? '']">{{ record.categoryName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'manager'"> |
| | | <span v-if="record.manager">{{ record.manager }}</span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'stockQty'"> |
| | | <span :class="{ 'font-semibold text-red-500': record.belowSafeStock }"> |
| | | {{ numberText(record.stockQty) }} |
| | | </span> |
| | | <Tag v-if="record.belowSafeStock" color="red" class="ml-1">ä½åºå</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'safeStock'"> |
| | | {{ numberText(record.safeStock) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'expireDate'"> |
| | | <span |
| | | v-if="record.expireDate" |
| | | :class="{ 'font-semibold text-orange-500': (record.expireRemainingDays ?? 999) <= 30, 'text-red-500': (record.expireRemainingDays ?? 999) < 0 }" |
| | | > |
| | | {{ record.expireDate }} |
| | | </span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button v-access:code="['safety:material-stock:in']" type="link" size="small" @click="openStock(record as SafetyApi.Material, 'IN')">å
¥åº</Button> |
| | | <Button v-access:code="['safety:material-stock:out']" type="link" size="small" @click="openStock(record as SafetyApi.Material, 'OUT')">åºåº</Button> |
| | | <Button v-access:code="['safety:material:update']" type="link" size="small" @click="showEdit(record as SafetyApi.Material)">ç¼è¾</Button> |
| | | <Popconfirm title="ç¡®å®å é¤è¯¥ç©èµï¼" ok-text="ç¡®å®" cancel-text="åæ¶" @confirm="remove(record as SafetyApi.Material)"> |
| | | <Button v-access:code="['safety:material:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Tabs.TabPane> |
| | | |
| | | <Tabs.TabPane key="stock" tab="åºå
¥åºæµæ°´"> |
| | | <Form :model="stockFilters" layout="inline" class="safety-filters" @finish="loadStock(true)"> |
| | | <FormItem label="ä¸å¡æ¥æ"> |
| | | <DatePicker.RangePicker v-model:value="stockFilters.bizDateRange" value-format="YYYY-MM-DD" /> |
| | | </FormItem> |
| | | <FormItem label="ç©èµåç§°"> |
| | | <Input v-model:value="stockFilters.materialName" allow-clear placeholder="åç§°" style="width: 150px" /> |
| | | </FormItem> |
| | | <FormItem label="ä¸å¡ç±»å"> |
| | | <Select |
| | | v-model:value="stockFilters.bizType" |
| | | :options="[ |
| | | { label: 'å
¥åº', value: 'IN' }, |
| | | { label: 'åºåº', value: 'OUT' }, |
| | | ]" |
| | | allow-clear |
| | | placeholder="å
¨é¨" |
| | | style="width: 110px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="stockLoading" @click="loadStock(true)">æ¥è¯¢</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <Table |
| | | :columns="stockColumns" |
| | | :data-source="stockRows" |
| | | row-key="id" |
| | | :loading="stockLoading" |
| | | :pagination="stockPagination" |
| | | :scroll="{ x: 1400 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="stockTableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'category'"> |
| | | <Tag :color="MATERIAL_CATEGORY_COLOR[record.category ?? '']">{{ record.categoryName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'bizType'"> |
| | | <Tag :color="record.bizType === 'IN' ? 'green' : 'orange'">{{ record.bizTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'quantity'"> |
| | | {{ numberText(record.quantity) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'stockBefore'"> |
| | | {{ numberText(record.stockBefore) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'stockAfter'"> |
| | | <span class="font-medium">{{ numberText(record.stockAfter) }}</span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="editOpen" |
| | | :title="model.id ? 'ç¼è¾ç©èµ' : 'æ°å¢ç©èµ'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="saveEdit" |
| | | > |
| | | <Form ref="formRef" :model="model" :rules="rules" :disabled="saving" layout="vertical" class="safety-form"> |
| | | <FormItem label="ç©èµç¼ç " name="code"> |
| | | <Input v-model:value="model.code" :maxlength="64" placeholder="å¦ PM-001" /> |
| | | </FormItem> |
| | | <FormItem label="ç©èµåç§°" name="name"> |
| | | <Input v-model:value="model.name" :maxlength="100" placeholder="å¦ 10kV ç»ç¼æå¥" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å«" name="category"> |
| | | <Select v-model:value="model.category" :options="MATERIAL_CATEGORY_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="è§æ ¼åå·" name="spec"> |
| | | <Input v-model:value="model.spec" :maxlength="100" placeholder="å¦ 12kV/2级" /> |
| | | </FormItem> |
| | | <FormItem label="计éåä½" name="unit"> |
| | | <Input v-model:value="model.unit" :maxlength="20" placeholder="å¦ å / ç¶ / å
·" /> |
| | | </FormItem> |
| | | <FormItem label="å®å
¨åºå" name="safeStock"> |
| | | <InputNumber v-model:value="model.safeStock" :min="0" :precision="3" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="åæ¾ä½ç½®" name="storageLocation"> |
| | | <Input v-model:value="model.storageLocation" :maxlength="100" placeholder="å¦ å®å
¨å·¥å¨å
·å®¤ A æ" /> |
| | | </FormItem> |
| | | <FormItem label="ä¿ç®¡äºº" name="managerId"> |
| | | <EmployeeSelect v-model="model.managerId" placeholder="è¾å
¥å§åæå·¥å·æç´¢ä¿ç®¡äºº" /> |
| | | </FormItem> |
| | | <FormItem :label="expireRequired ? 'æææè³ï¼å¿
å¡«ï¼' : 'æææè³'" name="expireDate"> |
| | | <DatePicker v-model:value="model.expireDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="æææææ°" name="validityMonths"> |
| | | <InputNumber v-model:value="model.validityMonths" :min="0" style="width: 100%" placeholder="å¦ç»ç¼æå¥ 6 个æ" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="2" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | <Alert v-if="model.id" type="info" show-icon class="mt-2" message="åºåæ°éåªè½éè¿åºå
¥åºç»è®°åå¨ï¼ç¼è¾æ¶ä¸å¯ç´æ¥ä¿®æ¹ã" /> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="stockOpen" :title="stockModel.bizType === 'IN' ? 'ç©èµå
¥åº' : 'ç©èµåºåº'" :confirm-loading="saving" :ok-text="stockModel.bizType === 'IN' ? '确认å
¥åº' : '确认åºåº'" cancel-text="åæ¶" @ok="saveStock"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="æ°é" required> |
| | | <InputNumber v-model:value="stockModel.quantity" :min="0.001" :precision="3" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="ä¸å¡æ¥æ"> |
| | | <DatePicker v-model:value="stockModel.bizDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="ç»å人" required> |
| | | <Input v-model:value="stockModel.operator" :maxlength="50" placeholder="请è¾å
¥å§å" /> |
| | | </FormItem> |
| | | <FormItem v-if="stockModel.bizType === 'OUT'" label="é¢ç¨äºº"> |
| | | <Input v-model:value="stockModel.receiver" :maxlength="50" placeholder="请è¾å
¥å§å" /> |
| | | </FormItem> |
| | | <FormItem label="ç¨é/å»å"> |
| | | <Input v-model:value="stockModel.purpose" :maxlength="200" placeholder="å¦ ç°åºæ£ä¿®é¢ç¨" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨"> |
| | | <Input.TextArea v-model:value="stockModel.remark" :maxlength="500" :rows="2" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Button, Card, DatePicker, Form, FormItem, Input, Select, Space, Table, Tag, message } from 'ant-design-vue'; |
| | | |
| | | import { getMaterialStockPage } from '#/api/safety'; |
| | | |
| | | import { MATERIAL_CATEGORY_COLOR, numberText } from '../../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyMaterialStock' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.MaterialStock[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 20, total: 0, showSizeChanger: true }); |
| | | |
| | | const columns = [ |
| | | { title: 'ä¸å¡æ¥æ', dataIndex: 'bizDate', width: 130, fixed: 'left' as const }, |
| | | { title: 'ç©èµåç§°', dataIndex: 'materialName', width: 200 }, |
| | | { title: 'ç±»å«', dataIndex: 'category', width: 140 }, |
| | | { title: 'ä¸å¡ç±»å', dataIndex: 'bizType', width: 110, align: 'center' as const }, |
| | | { title: 'æ°é', dataIndex: 'quantity', width: 110, align: 'right' as const }, |
| | | { title: 'æä½ååºå', dataIndex: 'stockBefore', width: 130, align: 'right' as const }, |
| | | { title: 'æä½ååºå', dataIndex: 'stockAfter', width: 130, align: 'right' as const }, |
| | | { title: 'ç»å人', dataIndex: 'operator', width: 110 }, |
| | | { title: 'é¢ç¨äºº', dataIndex: 'receiver', width: 110 }, |
| | | { title: 'ç¨é/å»å', dataIndex: 'purpose', minWidth: 180, ellipsis: true }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const params: any = { ...filters, pageNo: pagination.current, pageSize: pagination.pageSize }; |
| | | if (params.bizDateRange?.length === 2) { |
| | | params.bizDate = params.bizDateRange; |
| | | delete params.bizDateRange; |
| | | } |
| | | const data = await getMaterialStockPage(params); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½åºå
¥åºæµæ°´å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 20; |
| | | void load(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="åºå
¥åºæµæ°´" |
| | | description="åºæ¥ç©èµçå
¥åºãåºåºæç»å°è´¦ï¼æ¯ä¸æ¬¡åå¨é½è®°å½æä½åååºåï¼å¯è¿½æº¯å¯æ ¸å¯¹ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="ä¸å¡æ¥æ"> |
| | | <DatePicker.RangePicker v-model:value="filters.bizDateRange" value-format="YYYY-MM-DD" /> |
| | | </FormItem> |
| | | <FormItem label="ç©èµåç§°"> |
| | | <Input v-model:value="filters.materialName" allow-clear placeholder="åç§°" style="width: 160px" /> |
| | | </FormItem> |
| | | <FormItem label="ä¸å¡ç±»å"> |
| | | <Select |
| | | v-model:value="filters.bizType" |
| | | :options="[ |
| | | { label: 'å
¥åº', value: 'IN' }, |
| | | { label: 'åºåº', value: 'OUT' }, |
| | | ]" |
| | | allow-clear |
| | | placeholder="å
¨é¨" |
| | | style="width: 110px" |
| | | /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1450 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'category'"> |
| | | <Tag :color="MATERIAL_CATEGORY_COLOR[record.category ?? '']">{{ record.categoryName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'bizType'"> |
| | | <Tag :color="record.bizType === 'IN' ? 'green' : 'orange'">{{ record.bizTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'quantity'"> |
| | | {{ numberText(record.quantity) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'stockBefore'"> |
| | | {{ numberText(record.stockBefore) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'stockAfter'"> |
| | | <span class="font-medium">{{ numberText(record.stockAfter) }}</span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | Empty, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Switch, |
| | | Table, |
| | | Tag, |
| | | Tree, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { createOrg, deleteOrg, getOrgTree, updateOrg } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | defineOptions({ name: 'SafetyOrg' }); |
| | | |
| | | const treeData = ref<SafetyApi.Org[]>([]); |
| | | const expandedKeys = ref<number[]>([]); |
| | | const loading = ref(false); |
| | | |
| | | /** å½åéä¸èç¹ */ |
| | | const selected = ref<null | SafetyApi.Org>(null); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.Org => ({ |
| | | parentId: 0, |
| | | name: '', |
| | | leaderId: undefined, |
| | | // è´è´£äººå§å / çµè¯ä¸ºå端åå¡«çå¿«ç
§å段ï¼ä»
ç¨äºå±ç¤º |
| | | leaderName: undefined, |
| | | leaderPhone: undefined, |
| | | memberNames: '', |
| | | duty: '', |
| | | sort: 1, |
| | | status: 0, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Org>(initial()); |
| | | |
| | | const rules = { |
| | | name: [{ required: true, whitespace: true, message: '请è¾å
¥ç»ç»åç§°' }], |
| | | sort: [{ required: true, message: '请è¾å
¥æ¾ç¤ºé¡ºåº' }], |
| | | }; |
| | | |
| | | /** è¡¨æ ¼å±ç¤ºï¼ææ æå¹³ï¼ä¾¿äºæµè§ */ |
| | | function flatten(nodes: SafetyApi.Org[], depth = 0): any[] { |
| | | const out: any[] = []; |
| | | nodes.forEach((node) => { |
| | | out.push({ ...node, depth }); |
| | | if (node.children?.length) out.push(...flatten(node.children, depth + 1)); |
| | | }); |
| | | return out; |
| | | } |
| | | |
| | | const flatRows = ref<any[]>([]); |
| | | |
| | | async function load() { |
| | | loading.value = true; |
| | | try { |
| | | treeData.value = await getOrgTree(); |
| | | flatRows.value = flatten(treeData.value); |
| | | // é»è®¤å±å¼å
¨é¨ç¶èç¹ |
| | | const parents: number[] = []; |
| | | const walk = (nodes: SafetyApi.Org[]) => { |
| | | nodes.forEach((n) => { |
| | | if (n.children?.length) { |
| | | parents.push(n.id!); |
| | | walk(n.children); |
| | | } |
| | | }); |
| | | }; |
| | | walk(treeData.value); |
| | | expandedKeys.value = parents; |
| | | } catch { |
| | | message.error('å è½½å®å
¨ç»ç»æ¶æå¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function showForm(node?: SafetyApi.Org, parentId?: number) { |
| | | Object.assign(model, initial()); |
| | | if (node) { |
| | | Object.assign(model, node, { parentId: node.parentId ?? 0 }); |
| | | } else if (parentId !== undefined) { |
| | | model.parentId = parentId; |
| | | } |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, name: model.name.trim() }; |
| | | // è´è´£äººå§å / çµè¯ä¸ºåªè¯»å¿«ç
§ï¼ä¸åæäº¤ |
| | | delete data.leaderName; |
| | | delete data.leaderPhone; |
| | | await (data.id ? updateOrg(data) : createOrg(data)); |
| | | message.success('å®å
¨ç»ç»å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(node: SafetyApi.Org) { |
| | | await deleteOrg(node.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | const columns = [ |
| | | { title: 'ç»ç»åç§°', dataIndex: 'name', minWidth: 240 }, |
| | | { title: 'è´è´£äºº', dataIndex: 'leaderName', width: 120 }, |
| | | { title: 'èç³»çµè¯', dataIndex: 'leaderPhone', width: 140 }, |
| | | { title: 'æå', dataIndex: 'memberNames', width: 200, ellipsis: true }, |
| | | { title: 'å®å
¨èè´£', dataIndex: 'duty', minWidth: 240, ellipsis: true }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 90, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 210, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | /** 转æ ant-design-vue Tree éè¦ç { key, title, children } ç»æ */ |
| | | function toTreeNodes(nodes: SafetyApi.Org[]): any[] { |
| | | return nodes.map((n) => ({ |
| | | key: n.id!, |
| | | title: n.name, |
| | | children: n.children?.length ? toTreeNodes(n.children) : undefined, |
| | | })); |
| | | } |
| | | |
| | | const treeNodes = computed(() => toTreeNodes(treeData.value)); |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="å®å
¨ç»ç»æ¶æ" |
| | | description="å®å
¨ç产å§åä¼ãå®å
¨ç®¡çé¨å车é´å®å
¨å°ç»çå±çº§ç»æä¸èè´£åå·¥ã" |
| | | auto-content-height |
| | | > |
| | | <div class="flex gap-4"> |
| | | <!-- 左侧æ --> |
| | | <Card title="ç»ç»å±çº§" class="w-[300px] flex-shrink-0"> |
| | | <Button |
| | | v-access:code="['safety:org:create']" |
| | | type="primary" |
| | | block |
| | | class="mb-3" |
| | | @click="showForm()" |
| | | > |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:plus" /> |
| | | </template> |
| | | æ°å¢é¡¶çº§ç»ç» |
| | | </Button> |
| | | <Empty |
| | | v-if="!treeData.length && !loading" |
| | | :image="Empty.PRESENTED_IMAGE_SIMPLE" |
| | | description="ææ ç»ç»ï¼è¯·å
æ°å¢" |
| | | /> |
| | | <Tree |
| | | v-else |
| | | v-model:expanded-keys="expandedKeys" |
| | | :tree-data="treeNodes" |
| | | block-node |
| | | default-expand-all |
| | | /> |
| | | </Card> |
| | | |
| | | <!-- å³ä¾§è¡¨æ ¼ --> |
| | | <Card class="min-w-0 flex-1" title="ç»ç»æåä¸èè´£"> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="flatRows" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | row-key="id" |
| | | size="middle" |
| | | :scroll="{ x: 1300 }" |
| | | :default-expand-all-rows="true" |
| | | :locale="{ emptyText: 'ææ å®å
¨ç»ç»ï¼è¯·å
æ°å¢' }" |
| | | :header-cell-style="{ |
| | | background: '#fafafa', |
| | | color: '#606266', |
| | | fontWeight: 600, |
| | | padding: '12px 16px', |
| | | }" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'name'"> |
| | | <span |
| | | class="font-medium" |
| | | :style="{ paddingLeft: (record.depth ?? 0) * 20 + 'px' }" |
| | | > |
| | | <IconifyIcon |
| | | v-if="record.depth === 0" |
| | | class="mr-1 inline text-blue-500" |
| | | icon="lucide:building-2" |
| | | /> |
| | | {{ record.name }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag :color="record.status === 0 ? 'green' : 'default'"> |
| | | {{ record.status === 0 ? 'å¯ç¨' : 'åç¨' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button |
| | | v-access:code="['safety:org:create']" |
| | | type="link" |
| | | size="small" |
| | | @click="showForm(undefined, record.id)" |
| | | >æ°å¢ä¸çº§</Button> |
| | | <Button |
| | | v-access:code="['safety:org:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="showForm(record as SafetyApi.Org)" |
| | | >ç¼è¾</Button> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¯¥ç»ç»ï¼åå¨ä¸çº§ç»ç»æ¶æ æ³å é¤ã" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.Org)" |
| | | > |
| | | <Button |
| | | v-access:code="['safety:org:delete']" |
| | | type="link" |
| | | danger |
| | | size="small" |
| | | >å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </div> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾å®å
¨ç»ç»' : 'æ°å¢å®å
¨ç»ç»'" |
| | | :width="680" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form |
| | | ref="formRef" |
| | | :model="model" |
| | | :rules="rules" |
| | | :disabled="saving" |
| | | layout="vertical" |
| | | class="safety-form" |
| | | > |
| | | <FormItem label="ç»ç»åç§°" name="name"> |
| | | <Input v-model:value="model.name" :maxlength="100" placeholder="å¦ å®å
¨ç产å§åä¼" /> |
| | | </FormItem> |
| | | <FormItem label="ä¸çº§ç»ç»" name="parentId"> |
| | | <Select |
| | | v-model:value="model.parentId" |
| | | :field-names="{ label: 'name', value: 'id', options: 'children' }" |
| | | :options="[{ id: 0, name: 'æ ï¼é¡¶çº§ç»ç»ï¼', children: [] }, ...treeData]" |
| | | placeholder="éæ©ä¸çº§ç»ç»" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="è´è´£äºº" name="leaderId"> |
| | | <EmployeeSelect v-model="model.leaderId" placeholder="è¾å
¥å§åæå·¥å·æç´¢è´è´£äºº" /> |
| | | </FormItem> |
| | | <FormItem label="æ¾ç¤ºé¡ºåº" name="sort"> |
| | | <InputNumber v-model:value="model.sort" :min="0" :max="9999" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å¯ç¨" name="status"> |
| | | <Switch |
| | | :checked="model.status === 0" |
| | | @change="(v: any) => (model.status = v ? 0 : 1)" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="æåå§å" name="memberNames" class="col-span-full"> |
| | | <Input v-model:value="model.memberNames" :maxlength="500" placeholder="å¤ä¸ªå§åç¨éå·åéï¼å¦ å¼ ä¸,æå,çäº" /> |
| | | </FormItem> |
| | | <FormItem label="å®å
¨èè´£" name="duty" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.duty" :maxlength="500" :rows="3" show-count placeholder="æè¿°è¯¥ç»ç»çå®å
¨ç产èè´£" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 0 20px; |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | | .safety-form { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Descriptions, |
| | | Drawer, |
| | | Form, |
| | | FormItem, |
| | | Image, |
| | | Input, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tabs, |
| | | Tag, |
| | | Timeline, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop/index'; |
| | | import { |
| | | approveWorkPermit, |
| | | cancelWorkPermit, |
| | | closeWorkPermit, |
| | | createMonitor, |
| | | createWorkPermit, |
| | | deleteMonitor, |
| | | deleteWorkPermit, |
| | | getMonitorList, |
| | | getWorkPermitPage, |
| | | startWorkPermit, |
| | | updateWorkPermit, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { |
| | | PERMIT_STATUS_COLOR, |
| | | PERMIT_STATUS_OPTIONS, |
| | | PERMIT_TYPE_OPTIONS, |
| | | RISK_LEVEL_COLOR, |
| | | RISK_LEVEL_OPTIONS, |
| | | splitUrls, |
| | | } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyPermit' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.WorkPermit[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const workshopOptions = ref<{ label: string; value: number }[]>([]); |
| | | |
| | | const editOpen = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | const uploading = ref(false); |
| | | const photoList = ref<string[]>([]); |
| | | |
| | | const initial = (): SafetyApi.WorkPermit => ({ |
| | | permitType: 'HOT_WORK', |
| | | title: '', |
| | | workshopId: undefined, |
| | | location: '', |
| | | workContent: '', |
| | | applicantId: undefined, |
| | | planStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | planEndTime: dayjs().add(4, 'hour').format('YYYY-MM-DD HH:mm:ss'), |
| | | riskLevel: 'NORMAL', |
| | | safetyMeasures: '', |
| | | guardianId: undefined, |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.WorkPermit>(initial()); |
| | | |
| | | const editRules = { |
| | | permitType: [{ required: true, message: 'è¯·éæ©ä½ä¸ç±»å' }], |
| | | title: [{ required: true, whitespace: true, message: '请è¾å
¥ä½ä¸åç§°' }], |
| | | applicantId: [{ required: true, message: 'è¯·éæ©ç³è¯·äºº' }], |
| | | planStartTime: [{ required: true, message: 'è¯·éæ©è®¡åå¼å§æ¶é´' }], |
| | | planEndTime: [{ required: true, message: 'è¯·éæ©è®¡åç»ææ¶é´' }], |
| | | }; |
| | | |
| | | /** éè¦çæ¤äººçä½ä¸ç±»å */ |
| | | const GUARDIAN_REQUIRED_TYPES = [ |
| | | 'HOT_WORK', |
| | | 'LIVE_HIGH_VOLTAGE', |
| | | 'LIMITED_SPACE', |
| | | ]; |
| | | const guardianRequired = computed(() => GUARDIAN_REQUIRED_TYPES.includes(model.permitType)); |
| | | |
| | | /* å®¡æ¹ */ |
| | | const approveOpen = ref(false); |
| | | const approveModel = reactive<{ |
| | | id: number; |
| | | approverId?: number; |
| | | decision: string; |
| | | approveComment: string; |
| | | }>({ id: 0, approverId: undefined, decision: 'APPROVED', approveComment: '' }); |
| | | |
| | | /* 详æ
æ½å± */ |
| | | const detailOpen = ref(false); |
| | | const detailRow = ref<null | SafetyApi.WorkPermit>(null); |
| | | const monitors = ref<SafetyApi.WorkPermitMonitor[]>([]); |
| | | const monitorLoading = ref(false); |
| | | const monitorForm = reactive<SafetyApi.WorkPermitMonitor>({ |
| | | permitId: 0, |
| | | guardianId: undefined, |
| | | monitorContent: '', |
| | | abnormal: '', |
| | | }); |
| | | const monitorPhotos = ref<string[]>([]); |
| | | |
| | | const columns = [ |
| | | { title: 'ä½ä¸ç¥¨ç¼å·', dataIndex: 'code', width: 165, fixed: 'left' as const }, |
| | | { title: 'ä½ä¸ç±»å', dataIndex: 'permitType', width: 130 }, |
| | | { title: 'ä½ä¸åç§°', dataIndex: 'title', minWidth: 200, ellipsis: true }, |
| | | { title: 'ä½ä¸å°ç¹', dataIndex: 'location', width: 160, ellipsis: true }, |
| | | { title: 'ä½ä¸è½¦é´', dataIndex: 'workshopName', width: 130, ellipsis: true }, |
| | | { title: 'ç³è¯·äºº', dataIndex: 'applicant', width: 100 }, |
| | | { title: '计åå¼å§', dataIndex: 'planStartTime', width: 165 }, |
| | | { title: '计åç»æ', dataIndex: 'planEndTime', width: 165 }, |
| | | { title: 'é£é©ç级', dataIndex: 'riskLevel', width: 100, align: 'center' as const }, |
| | | { title: 'çæ¤äºº', dataIndex: 'guardian', width: 100 }, |
| | | { title: 'ç¶æ', dataIndex: 'status', width: 110, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 260, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getWorkPermitPage({ |
| | | ...filters, |
| | | pageNo: pagination.current, |
| | | pageSize: pagination.pageSize, |
| | | }); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½ä½ä¸ç¥¨å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadWorkshops() { |
| | | try { |
| | | const list = await getWorkshopSimpleList(); |
| | | workshopOptions.value = (list || []).map((w) => ({ label: w.name!, value: w.id! })); |
| | | } catch { |
| | | workshopOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showEdit(row?: SafetyApi.WorkPermit) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | photoList.value = splitUrls(model.photoUrls); |
| | | editOpen.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function handleUpload(e: Event) { |
| | | const input = e.target as HTMLInputElement; |
| | | const files = Array.from(input.files ?? []); |
| | | if (!files.length) return; |
| | | uploading.value = true; |
| | | try { |
| | | const urls = await uploadSafetyFiles(files, 'safety/permit'); |
| | | photoList.value = [...photoList.value, ...urls]; |
| | | message.success(`å·²ä¸ä¼ ${urls.length} å¼ ç
§ç`); |
| | | } catch { |
| | | message.error('ç
§çä¸ä¼ 失败'); |
| | | } finally { |
| | | uploading.value = false; |
| | | input.value = ''; |
| | | } |
| | | } |
| | | |
| | | async function saveEdit() { |
| | | if (guardianRequired.value && !model.guardianId) { |
| | | message.warning('å¨ç« / é«åå¸¦çµ / æé空é´ä½ä¸å¿
é¡»æå®çæ¤äºº'); |
| | | return; |
| | | } |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { |
| | | ...model, |
| | | title: model.title.trim(), |
| | | photoUrls: photoList.value.join(','), |
| | | }; |
| | | // ç³è¯·äºº / 审æ¹äºº / çæ¤äºº / 车é´åç§°å为å端åå¡«çå¿«ç
§å段ï¼ä¸åä¸æäº¤ |
| | | delete data.applicant; |
| | | delete data.approver; |
| | | delete data.guardian; |
| | | delete data.workshopName; |
| | | await (data.id ? updateWorkPermit(data) : createWorkPermit(data)); |
| | | message.success(data.id ? 'ä½ä¸ç¥¨å·²æ´æ°' : 'ä½ä¸ç¥¨å·²æäº¤ï¼çå¾
审æ¹'); |
| | | editOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | /* ---------- å®¡æ¹ ---------- */ |
| | | function openApprove(row: SafetyApi.WorkPermit) { |
| | | // è®°å½å®¡æ¹ç®æ è¡ï¼ç¨äºæ ¡éªâç³è¯·äººä¸è½å®¡æ¹èªå·±çä½ä¸ç¥¨â |
| | | detailRow.value = row; |
| | | approveModel.id = row.id!; |
| | | approveModel.approverId = undefined; |
| | | approveModel.decision = 'APPROVED'; |
| | | approveModel.approveComment = ''; |
| | | approveOpen.value = true; |
| | | } |
| | | |
| | | async function saveApprove() { |
| | | const { approverId } = approveModel; |
| | | if (!approverId) { |
| | | message.warning('è¯·éæ©å®¡æ¹äºº'); |
| | | return; |
| | | } |
| | | if (approverId === detailRow.value?.applicantId) { |
| | | message.warning('ç³è¯·äººä¸è½å®¡æ¹èªå·±çä½ä¸ç¥¨'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await approveWorkPermit({ |
| | | id: approveModel.id, |
| | | approverId, |
| | | decision: approveModel.decision, |
| | | approveComment: approveModel.approveComment, |
| | | }); |
| | | message.success(approveModel.decision === 'APPROVED' ? 'å·²æ¹å' : '已驳å'); |
| | | approveOpen.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | /* ---------- å¼å§ / ç»æ / ä½åº ---------- */ |
| | | async function handleStart(row: SafetyApi.WorkPermit) { |
| | | await startWorkPermit(row.id!); |
| | | message.success('ä½ä¸å·²å¼å§'); |
| | | await load(); |
| | | } |
| | | |
| | | async function handleClose(row: SafetyApi.WorkPermit) { |
| | | await closeWorkPermit(row.id!); |
| | | message.success('ä½ä¸å·²å®ç»å¹¶å½æ¡£'); |
| | | await load(); |
| | | } |
| | | |
| | | async function handleCancel(row: SafetyApi.WorkPermit) { |
| | | await cancelWorkPermit(row.id!); |
| | | message.success('ä½ä¸ç¥¨å·²ä½åº'); |
| | | await load(); |
| | | } |
| | | |
| | | /* ---------- 详æ
/ çæ¤è®°å½ ---------- */ |
| | | async function openDetail(row: SafetyApi.WorkPermit) { |
| | | detailRow.value = row; |
| | | detailOpen.value = true; |
| | | monitorForm.permitId = row.id!; |
| | | monitorForm.guardianId = row.guardianId; |
| | | monitorForm.monitorContent = ''; |
| | | monitorForm.abnormal = ''; |
| | | monitorPhotos.value = []; |
| | | await loadMonitors(row.id!); |
| | | } |
| | | |
| | | async function loadMonitors(permitId: number) { |
| | | monitorLoading.value = true; |
| | | try { |
| | | monitors.value = await getMonitorList(permitId); |
| | | } catch { |
| | | message.error('å è½½çæ¤è®°å½å¤±è´¥'); |
| | | } finally { |
| | | monitorLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function handleMonitorUpload(e: Event) { |
| | | const input = e.target as HTMLInputElement; |
| | | const files = Array.from(input.files ?? []); |
| | | if (!files.length) return; |
| | | uploading.value = true; |
| | | try { |
| | | const urls = await uploadSafetyFiles(files, 'safety/permit-monitor'); |
| | | monitorPhotos.value = [...monitorPhotos.value, ...urls]; |
| | | } catch { |
| | | message.error('ç
§çä¸ä¼ 失败'); |
| | | } finally { |
| | | uploading.value = false; |
| | | input.value = ''; |
| | | } |
| | | } |
| | | |
| | | async function saveMonitor() { |
| | | if (!monitorForm.guardianId) { |
| | | message.warning('è¯·éæ©çæ¤äºº'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await createMonitor({ |
| | | ...monitorForm, |
| | | guardianId: monitorForm.guardianId, |
| | | monitorTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | photoUrls: monitorPhotos.value.join(','), |
| | | }); |
| | | message.success('çæ¤è®°å½å·²æ·»å '); |
| | | monitorForm.monitorContent = ''; |
| | | monitorForm.abnormal = ''; |
| | | monitorPhotos.value = []; |
| | | await loadMonitors(monitorForm.permitId); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function removeMonitor(id: number) { |
| | | await deleteMonitor(id); |
| | | message.success('å·²å é¤'); |
| | | await loadMonitors(monitorForm.permitId); |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.WorkPermit) { |
| | | await deleteWorkPermit(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | void load(); |
| | | void loadWorkshops(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="é«å±ä½ä¸ç¥¨" |
| | | description="å¨ç«ãé«å带çµãæé空é´çé«å±ä½ä¸ç线ä¸å®¡æ¹ï¼ä½ä¸ç¥¨ä¸çæ¤è®°å½å
¨ç¨çµåçåãçææ¡ä»¶ï¼ä½ä¸ç¥¨ + çæ¤è®°å½ã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="ä½ä¸åç§°"> |
| | | <Input v-model:value="filters.title" allow-clear placeholder="åç§°" style="width: 160px" /> |
| | | </FormItem> |
| | | <FormItem label="ä½ä¸ç±»å"> |
| | | <Select v-model:value="filters.permitType" :options="PERMIT_TYPE_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 150px" /> |
| | | </FormItem> |
| | | <FormItem label="ç¶æ"> |
| | | <Select v-model:value="filters.status" :options="PERMIT_STATUS_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 130px" /> |
| | | </FormItem> |
| | | <FormItem label="ç³è¯·äºº"> |
| | | <Input v-model:value="filters.applicant" allow-clear placeholder="å§å" style="width: 120px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">ä½ä¸ç¥¨å表</span> |
| | | <Button v-access:code="['safety:permit:create']" type="primary" @click="showEdit()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æäº¤ä½ä¸ç³è¯· |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1900 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'permitType'"> |
| | | <Tag color="blue">{{ record.permitTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'planStartTime'"> |
| | | {{ record.planStartTime ? String(record.planStartTime).replace('T', ' ') : '-' }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'planEndTime'"> |
| | | {{ record.planEndTime ? String(record.planEndTime).replace('T', ' ') : '-' }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'riskLevel'"> |
| | | <Tag :color="RISK_LEVEL_COLOR[record.riskLevel ?? '']">{{ record.riskLevelName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'applicant'"> |
| | | <span v-if="record.applicant">{{ record.applicant }}</span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'guardian'"> |
| | | <span v-if="record.guardian">{{ record.guardian }}</span> |
| | | <Tag v-else-if="record.guardianRequired" color="red">æªæå®</Tag> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'status'"> |
| | | <Tag :color="PERMIT_STATUS_COLOR[record.status ?? '']">{{ record.statusName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button type="link" size="small" @click="openDetail(record as SafetyApi.WorkPermit)">详æ
</Button> |
| | | <Button |
| | | v-if="record.status === 'PENDING'" |
| | | v-access:code="['safety:permit:approve']" |
| | | type="link" |
| | | size="small" |
| | | @click="openApprove(record as SafetyApi.WorkPermit)" |
| | | >审æ¹</Button> |
| | | <Button |
| | | v-if="record.status === 'DRAFT' || record.status === 'PENDING'" |
| | | v-access:code="['safety:permit:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="showEdit(record as SafetyApi.WorkPermit)" |
| | | >ç¼è¾</Button> |
| | | <Button |
| | | v-if="record.status === 'APPROVED'" |
| | | v-access:code="['safety:permit:update']" |
| | | type="link" |
| | | size="small" |
| | | @click="handleStart(record as SafetyApi.WorkPermit)" |
| | | >å¼å§ä½ä¸</Button> |
| | | <Popconfirm |
| | | v-if="record.status === 'IN_PROGRESS'" |
| | | title="确认ä½ä¸å·²ç»æå¹¶å½æ¡£ï¼" |
| | | ok-text="确认" |
| | | cancel-text="åæ¶" |
| | | @confirm="handleClose(record as SafetyApi.WorkPermit)" |
| | | > |
| | | <Button v-access:code="['safety:permit:update']" type="link" size="small">ç»æä½ä¸</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | title="ç¡®å®å é¤è¯¥ä½ä¸ç¥¨ï¼çæ¤è®°å½ä¼ä¸å¹¶å é¤ã" |
| | | ok-text="ç¡®å®" |
| | | cancel-text="åæ¶" |
| | | @confirm="remove(record as SafetyApi.WorkPermit)" |
| | | > |
| | | <Button v-access:code="['safety:permit:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="editOpen" |
| | | :title="model.id ? 'ç¼è¾ä½ä¸ç¥¨' : 'æäº¤é«å±ä½ä¸ç³è¯·'" |
| | | :width="760" |
| | | :confirm-loading="saving" |
| | | ok-text="æäº¤" |
| | | cancel-text="åæ¶" |
| | | @ok="saveEdit" |
| | | > |
| | | <Form ref="formRef" :model="model" :rules="editRules" :disabled="saving" layout="vertical" class="safety-form"> |
| | | <FormItem label="ä½ä¸ç±»å" name="permitType"> |
| | | <Select v-model:value="model.permitType" :options="PERMIT_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="é£é©ç级" name="riskLevel"> |
| | | <Select v-model:value="model.riskLevel" :options="RISK_LEVEL_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="ä½ä¸åç§°" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="å¦ 2#主å 10kV å¼å
³ææ£ä¿®å¨ç«" /> |
| | | </FormItem> |
| | | <FormItem label="ä½ä¸åºå/车é´" name="workshopId"> |
| | | <Select |
| | | v-model:value="model.workshopId" |
| | | :options="workshopOptions" |
| | | show-search |
| | | :filter-option="(i: string, o: any) => String(o.label).includes(i)" |
| | | placeholder="è¯·éæ©è½¦é´" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="ä½ä¸å°ç¹" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ 2#é«åé
çµå®¤" /> |
| | | </FormItem> |
| | | <FormItem label="计åå¼å§æ¶é´" name="planStartTime"> |
| | | <DatePicker v-model:value="model.planStartTime" show-time format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="计åç»ææ¶é´" name="planEndTime"> |
| | | <DatePicker v-model:value="model.planEndTime" show-time format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="ç³è¯·äºº" name="applicantId"> |
| | | <EmployeeSelect v-model="model.applicantId" placeholder="è¾å
¥å§åæå·¥å·æç´¢ç³è¯·äºº" /> |
| | | </FormItem> |
| | | <FormItem label="çæ¤äºº" name="guardianId"> |
| | | <EmployeeSelect v-model="model.guardianId" :placeholder="guardianRequired ? 'å¨ç«/é«å带çµ/æé空é´å¿
é' : 'éå¡«'" /> |
| | | </FormItem> |
| | | <FormItem label="ä½ä¸å
容ä¸èå´" name="workContent" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.workContent" :maxlength="2000" :rows="3" show-count placeholder="说æä½ä¸å
å®¹ãæ¶å设å¤ä¸ä½ä¸èå´" /> |
| | | </FormItem> |
| | | <FormItem label="å®å
¨æªæ½" name="safetyMeasures" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.safetyMeasures" :maxlength="2000" :rows="3" show-count placeholder="å¦ åçµéªçµæ¥å°ã佩æ´ç»ç¼æå¥ãè®¾ç½®è¦æå´æ ç" /> |
| | | </FormItem> |
| | | <FormItem label="ç°åºç
§çï¼ä½ä¸ç¥¨éä»¶ï¼" class="col-span-full"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <Image.PreviewGroup> |
| | | <Image v-for="url in photoList" :key="url" :src="url" :width="72" :height="72" class="rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <label class="photo-btn"> |
| | | <IconifyIcon class="size-5" icon="lucide:camera" /> |
| | | <span class="mt-1 text-xs">{{ uploading ? 'ä¸ä¼ ä¸' : 'æç
§' }}</span> |
| | | <input type="file" accept="image/*" capture="environment" multiple class="hidden" :disabled="uploading" @change="handleUpload" /> |
| | | </label> |
| | | </div> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Modal v-model:open="approveOpen" title="ä½ä¸ç¥¨å®¡æ¹" :confirm-loading="saving" ok-text="æäº¤å®¡æ¹" cancel-text="åæ¶" @ok="saveApprove"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="审æ¹äºº" required> |
| | | <EmployeeSelect v-model="approveModel.approverId" placeholder="è¾å
¥å§åæå·¥å·æç´¢å®¡æ¹äººï¼ä¸è½ä¸ç³è¯·äººç¸åï¼" /> |
| | | </FormItem> |
| | | <FormItem label="审æ¹ç»è®º" required> |
| | | <Select |
| | | v-model:value="approveModel.decision" |
| | | :options="[ |
| | | { label: 'åæï¼æ¹åä½ä¸', value: 'APPROVED' }, |
| | | { label: '驳å', value: 'REJECTED' }, |
| | | ]" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="å®¡æ¹æè§"> |
| | | <Input.TextArea v-model:value="approveModel.approveComment" :maxlength="500" :rows="2" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | |
| | | <Drawer v-model:open="detailOpen" title="ä½ä¸ç¥¨è¯¦æ
" :width="760"> |
| | | <template v-if="detailRow"> |
| | | <Tabs default-active-key="info"> |
| | | <Tabs.TabPane key="info" tab="ä½ä¸ç¥¨ä¿¡æ¯"> |
| | | <Descriptions bordered size="small" :column="2"> |
| | | <Descriptions.Item label="ä½ä¸ç¥¨ç¼å·">{{ detailRow.code }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç¶æ"> |
| | | <Tag :color="PERMIT_STATUS_COLOR[detailRow.status ?? '']">{{ detailRow.statusName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="ä½ä¸ç±»å">{{ detailRow.permitTypeName }}</Descriptions.Item> |
| | | <Descriptions.Item label="é£é©ç级"> |
| | | <Tag :color="RISK_LEVEL_COLOR[detailRow.riskLevel ?? '']">{{ detailRow.riskLevelName }}</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="ä½ä¸åç§°" :span="2">{{ detailRow.title }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä½ä¸åºå">{{ detailRow.workshopName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä½ä¸å°ç¹">{{ detailRow.location || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç³è¯·äºº">{{ detailRow.applicant || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç³è¯·æ¶é´">{{ detailRow.applyTime ? String(detailRow.applyTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="计åå¼å§">{{ detailRow.planStartTime ? String(detailRow.planStartTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="计åç»æ">{{ detailRow.planEndTime ? String(detailRow.planEndTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å®é
å¼å§">{{ detailRow.actualStartTime ? String(detailRow.actualStartTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å®é
ç»æ">{{ detailRow.actualEndTime ? String(detailRow.actualEndTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="çæ¤äºº">{{ detailRow.guardian || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="审æ¹äºº">{{ detailRow.approver || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å®¡æ¹æ¶é´">{{ detailRow.approveTime ? String(detailRow.approveTime).replace('T', ' ') : '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å®¡æ¹æè§">{{ detailRow.approveComment || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä½ä¸å
容" :span="2">{{ detailRow.workContent || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å®å
¨æªæ½" :span="2">{{ detailRow.safetyMeasures || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ç°åºç
§ç" :span="2"> |
| | | <Image.PreviewGroup v-if="splitUrls(detailRow.photoUrls).length"> |
| | | <Image v-for="url in splitUrls(detailRow.photoUrls)" :key="url" :src="url" :width="80" :height="80" class="mr-2 rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <span v-else>-</span> |
| | | </Descriptions.Item> |
| | | </Descriptions> |
| | | </Tabs.TabPane> |
| | | |
| | | <Tabs.TabPane key="monitor" :tab="`çæ¤è®°å½ (${monitors.length})`"> |
| | | <div class="mb-4 rounded-lg border border-solid border-[hsl(var(--border))] p-3"> |
| | | <div class="mb-2 text-sm font-semibold">æ·»å çæ¤è®°å½</div> |
| | | <Form layout="vertical"> |
| | | <FormItem label="çæ¤äºº"> |
| | | <EmployeeSelect v-model="monitorForm.guardianId" placeholder="è¾å
¥å§åæå·¥å·æç´¢çæ¤äºº" /> |
| | | </FormItem> |
| | | <FormItem label="çæ¤å
容䏿
åµ"> |
| | | <Input.TextArea v-model:value="monitorForm.monitorContent" :maxlength="1000" :rows="2" placeholder="è®°å½çæ¤æ¶çç°åºæ
åµ" /> |
| | | </FormItem> |
| | | <FormItem label="å¼å¸¸æ
åµä¸å¤ç½®"> |
| | | <Input.TextArea v-model:value="monitorForm.abnormal" :maxlength="500" :rows="2" placeholder="æ å¼å¸¸å¯ç空" /> |
| | | </FormItem> |
| | | <div class="mb-2 flex items-center gap-2"> |
| | | <Image.PreviewGroup> |
| | | <Image v-for="url in monitorPhotos" :key="url" :src="url" :width="56" :height="56" class="rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | <label class="photo-btn !h-14 !w-14"> |
| | | <IconifyIcon class="size-4" icon="lucide:camera" /> |
| | | <span class="text-xs">{{ uploading ? 'ä¸ä¼ ä¸' : 'æç
§' }}</span> |
| | | <input type="file" accept="image/*" capture="environment" multiple class="hidden" :disabled="uploading" @change="handleMonitorUpload" /> |
| | | </label> |
| | | <Button type="primary" :loading="saving" @click="saveMonitor">æ·»å è®°å½</Button> |
| | | </div> |
| | | </Form> |
| | | </div> |
| | | <div v-if="monitorLoading" class="py-4 text-center text-gray-400">å è½½ä¸â¦</div> |
| | | <Timeline v-else-if="monitors.length"> |
| | | <Timeline.Item v-for="m in monitors" :key="m.id"> |
| | | <div class="flex items-start justify-between"> |
| | | <div> |
| | | <div class="font-medium">{{ m.guardian }}</div> |
| | | <div class="text-muted-foreground text-xs"> |
| | | {{ m.monitorTime ? String(m.monitorTime).replace('T', ' ') : '' }} |
| | | </div> |
| | | <div class="mt-1 text-sm">{{ m.monitorContent || 'ï¼æ æè¿°ï¼' }}</div> |
| | | <div v-if="m.abnormal" class="mt-1 text-sm text-red-500">å¼å¸¸ï¼{{ m.abnormal }}</div> |
| | | <Image.PreviewGroup v-if="splitUrls(m.photoUrls).length" class="mt-1"> |
| | | <Image v-for="u in splitUrls(m.photoUrls)" :key="u" :src="u" :width="48" :height="48" class="mr-1 rounded object-cover" /> |
| | | </Image.PreviewGroup> |
| | | </div> |
| | | <Button type="link" danger size="small" @click="removeMonitor(m.id!)">å é¤</Button> |
| | | </div> |
| | | </Timeline.Item> |
| | | </Timeline> |
| | | <div v-else class="py-8 text-center text-gray-400">ææ çæ¤è®°å½</div> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </template> |
| | | </Drawer> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | .photo-btn { |
| | | display: flex; |
| | | width: 72px; |
| | | height: 72px; |
| | | cursor: pointer; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 6px; |
| | | border: 1px dashed hsl(var(--border)); |
| | | color: #909399; |
| | | } |
| | | .photo-btn:hover { border-color: #60a5fa; color: #3b82f6; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** å®å
¨ç产模å - æä¸¾ãé项ä¸éç¨å·¥å
· */ |
| | | |
| | | // ==================== æä¸¾å®ä¹ ==================== |
| | | |
| | | /** ç¹ç§ä½ä¸è¯ç±»å */ |
| | | export const CERT_TYPE_OPTIONS = [ |
| | | { label: 'ä½åçµå·¥è¯', value: 'LOW_VOLTAGE_ELECTRICIAN' }, |
| | | { label: 'é«åæä½è¯', value: 'HIGH_VOLTAGE_OPERATION' }, |
| | | { label: 'çæ¥ä¸çåå²ä½ä¸è¯', value: 'WELDING' }, |
| | | { label: 'èµ·éæºæ¢°ä½ä¸è¯', value: 'HOIST' }, |
| | | { label: 'é«å¤ä½ä¸è¯', value: 'HEIGHT_WORK' }, |
| | | { label: 'æé空é´ä½ä¸è¯', value: 'LIMITED_SPACE' }, |
| | | { label: 'å车ä½ä¸è¯', value: 'FORKLIFT' }, |
| | | { label: 'é
çä½ä¸è¯', value: 'BOILER' }, |
| | | { label: 'å
¶ä»ç¹ç§ä½ä¸è¯', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** è¯ä»¶ç¶æ */ |
| | | export const CERT_STATUS_OPTIONS = [ |
| | | { label: 'ææ', value: 'VALID' }, |
| | | { label: 'å³å°å°æ', value: 'EXPIRING' }, |
| | | { label: 'å·²è¿æ', value: 'EXPIRED' }, |
| | | { label: '已注é', value: 'REVOKED' }, |
| | | ]; |
| | | |
| | | export const CERT_STATUS_COLOR: Record<string, string> = { |
| | | VALID: 'green', |
| | | EXPIRING: 'orange', |
| | | EXPIRED: 'red', |
| | | REVOKED: 'default', |
| | | }; |
| | | |
| | | /** å·¡æ£ç¹ä½ç±»å */ |
| | | export const POINT_TYPE_OPTIONS = [ |
| | | { label: 'é«åé
çµå®¤', value: 'HIGH_VOLTAGE_ROOM' }, |
| | | { label: 'æºç»åº', value: 'UNIT_AREA' }, |
| | | { label: 'å±åååº', value: 'DANGER_CHEMICAL' }, |
| | | { label: 'æ¶é²éç¹é¨ä½', value: 'FIRE_AREA' }, |
| | | { label: 'ç产车é´', value: 'WORKSHOP' }, |
| | | { label: 'å
¶ä»', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** å·¡æ£å¨æ */ |
| | | export const INSPECT_CYCLE_OPTIONS = [ |
| | | { label: 'æ¯ç', value: 'SHIFT' }, |
| | | { label: 'æ¯æ¥', value: 'DAILY' }, |
| | | { label: 'æ¯å¨', value: 'WEEKLY' }, |
| | | { label: 'æ¯æ', value: 'MONTHLY' }, |
| | | ]; |
| | | |
| | | /** å·¡æ£ç»æ */ |
| | | export const INSPECTION_RESULT_OPTIONS = [ |
| | | { label: 'æ£å¸¸', value: 'NORMAL' }, |
| | | { label: 'å¼å¸¸', value: 'ABNORMAL' }, |
| | | ]; |
| | | |
| | | /** 鿣ç级 */ |
| | | export const HAZARD_LEVEL_OPTIONS = [ |
| | | { label: 'ä¸è¬éæ£', value: 'GENERAL' }, |
| | | { label: 'é大鿣', value: 'MAJOR' }, |
| | | ]; |
| | | |
| | | export const HAZARD_LEVEL_COLOR: Record<string, string> = { |
| | | GENERAL: 'blue', |
| | | MAJOR: 'red', |
| | | }; |
| | | |
| | | /** éæ£æ¥æº */ |
| | | export const HAZARD_SOURCE_OPTIONS = [ |
| | | { label: 'å·¡æ£åç°', value: 'INSPECTION' }, |
| | | { label: 'èªæ¥ä¸æ¥', value: 'SELF_CHECK' }, |
| | | { label: 'å工䏿¥', value: 'REPORT' }, |
| | | { label: 'å¤é¨æ£æ¥', value: 'EXTERNAL' }, |
| | | ]; |
| | | |
| | | /** éæ£ç¶æï¼éç¯æµè½¬ï¼ */ |
| | | export const HAZARD_STATUS_OPTIONS = [ |
| | | { label: '已䏿¥', value: 'REPORTED' }, |
| | | { label: '已忴¾', value: 'ASSIGNED' }, |
| | | { label: 'æ´æ¹ä¸', value: 'RECTIFYING' }, |
| | | { label: 'å¾
夿¥', value: 'RECHECKING' }, |
| | | { label: '夿¥ä¸éè¿', value: 'REJECTED' }, |
| | | { label: 'å·²éç¯', value: 'CLOSED' }, |
| | | ]; |
| | | |
| | | export const HAZARD_STATUS_COLOR: Record<string, string> = { |
| | | REPORTED: 'default', |
| | | ASSIGNED: 'blue', |
| | | RECTIFYING: 'processing', |
| | | RECHECKING: 'orange', |
| | | REJECTED: 'red', |
| | | CLOSED: 'success', |
| | | }; |
| | | |
| | | /** ä½ä¸ç¥¨ç±»å */ |
| | | export const PERMIT_TYPE_OPTIONS = [ |
| | | { label: 'å¨ç«ä½ä¸', value: 'HOT_WORK' }, |
| | | { label: 'é«å带çµä½ä¸', value: 'LIVE_HIGH_VOLTAGE' }, |
| | | { label: 'æé空é´ä½ä¸', value: 'LIMITED_SPACE' }, |
| | | { label: 'é«å¤ä½ä¸', value: 'WORK_AT_HEIGHT' }, |
| | | { label: 'åè£
ä½ä¸', value: 'LIFTING' }, |
| | | { label: '临æ¶ç¨çµä½ä¸', value: 'TEMP_ELECTRIC' }, |
| | | { label: 'å
¶ä»ä½ä¸', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** ä½ä¸ç¥¨ç¶æ */ |
| | | export const PERMIT_STATUS_OPTIONS = [ |
| | | { label: 'è稿', value: 'DRAFT' }, |
| | | { label: 'å¾
审æ¹', value: 'PENDING' }, |
| | | { label: 'å·²æ¹å', value: 'APPROVED' }, |
| | | { label: '已驳å', value: 'REJECTED' }, |
| | | { label: 'ä½ä¸ä¸', value: 'IN_PROGRESS' }, |
| | | { label: 'å·²å®ç»', value: 'CLOSED' }, |
| | | { label: 'å·²ä½åº', value: 'CANCELLED' }, |
| | | ]; |
| | | |
| | | export const PERMIT_STATUS_COLOR: Record<string, string> = { |
| | | DRAFT: 'default', |
| | | PENDING: 'orange', |
| | | APPROVED: 'blue', |
| | | REJECTED: 'red', |
| | | IN_PROGRESS: 'processing', |
| | | CLOSED: 'success', |
| | | CANCELLED: 'default', |
| | | }; |
| | | |
| | | /** é£é©ç级 */ |
| | | export const RISK_LEVEL_OPTIONS = [ |
| | | { label: 'ä½', value: 'LOW' }, |
| | | { label: 'ä¸è¬', value: 'NORMAL' }, |
| | | { label: 'é«', value: 'HIGH' }, |
| | | { label: 'æé«', value: 'VERY_HIGH' }, |
| | | ]; |
| | | |
| | | export const RISK_LEVEL_COLOR: Record<string, string> = { |
| | | LOW: 'green', |
| | | NORMAL: 'blue', |
| | | HIGH: 'orange', |
| | | VERY_HIGH: 'red', |
| | | }; |
| | | |
| | | /** ç©èµç±»å« */ |
| | | export const MATERIAL_CATEGORY_OPTIONS = [ |
| | | { label: 'çµåç»ç¼ç¨å
·', value: 'INSULATING' }, |
| | | { label: 'å±åå', value: 'HAZARD_CHEMICAL' }, |
| | | { label: 'æ¶é²ç©èµ', value: 'FIRE' }, |
| | | { label: 'å³ä¿ç¨å', value: 'PPE' }, |
| | | { label: 'å
¶ä»', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | export const MATERIAL_CATEGORY_COLOR: Record<string, string> = { |
| | | INSULATING: 'blue', |
| | | HAZARD_CHEMICAL: 'red', |
| | | FIRE: 'orange', |
| | | PPE: 'green', |
| | | OTHER: 'default', |
| | | }; |
| | | |
| | | /** å¹è®ç±»å */ |
| | | export const TRAINING_TYPE_OPTIONS = [ |
| | | { label: 'å®å
¨å¹è®', value: 'SAFETY' }, |
| | | { label: 'æè½å¹è®', value: 'SKILL' }, |
| | | { label: 'æ³è§å®£è´¯', value: 'LAW' }, |
| | | { label: 'åºæ¥æ¼ç»å¹è®', value: 'EMERGENCY' }, |
| | | { label: 'ä¸çº§å®å
¨æè²', value: 'INDUCTION' }, |
| | | { label: 'å
¶ä»', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** æ¼ç»ç±»å */ |
| | | export const DRILL_TYPE_OPTIONS = [ |
| | | { label: 'æ¶é²æ¼ç»', value: 'FIRE' }, |
| | | { label: 'åºæ¥çæ£æ¼ç»', value: 'EMERGENCY_EVAC' }, |
| | | { label: '触çµäºæ
æ¼ç»', value: 'ELECTRIC_ACCIDENT' }, |
| | | { label: 'å±ååæ³æ¼æ¼ç»', value: 'CHEMICAL_LEAK' }, |
| | | { label: 'å
¶ä»æ¼ç»', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** äºæ
ç±»å */ |
| | | export const ACCIDENT_TYPE_OPTIONS = [ |
| | | { label: '触çµ', value: 'ELECTRIC_SHOCK' }, |
| | | { label: 'ç«ç¾', value: 'FIRE' }, |
| | | { label: 'é«å¤å è½', value: 'FALL' }, |
| | | { label: 'ç©ä½æå»', value: 'OBJECT_STRIKE' }, |
| | | { label: 'æºæ¢°ä¼¤å®³', value: 'MECHANICAL' }, |
| | | { label: '䏿¯çªæ¯', value: 'CHEMICAL' }, |
| | | { label: '车è¾ä¼¤å®³', value: 'VEHICLE' }, |
| | | { label: 'å
¶ä»', value: 'OTHER' }, |
| | | ]; |
| | | |
| | | /** äºæ
ç级 */ |
| | | export const ACCIDENT_LEVEL_OPTIONS = [ |
| | | { label: 'è½»ä¼¤äºæ
', value: 'SLIGHT' }, |
| | | { label: 'ä¸è¬äºæ
', value: 'GENERAL' }, |
| | | { label: 'éä¼¤äºæ
', value: 'SEVERE' }, |
| | | { label: 'éå¤§äºæ
', value: 'MAJOR' }, |
| | | ]; |
| | | |
| | | export const ACCIDENT_LEVEL_COLOR: Record<string, string> = { |
| | | SLIGHT: 'default', |
| | | GENERAL: 'blue', |
| | | SEVERE: 'orange', |
| | | MAJOR: 'red', |
| | | }; |
| | | |
| | | /** äºæ
å¤çç¶æ */ |
| | | export const ACCIDENT_STATUS_OPTIONS = [ |
| | | { label: 'å·²ç»è®°', value: 'REPORTED' }, |
| | | { label: 'è°æ¥ä¸', value: 'INVESTIGATING' }, |
| | | { label: 'å·²å¤ç', value: 'HANDLED' }, |
| | | { label: 'å·²ç»æ¡', value: 'CLOSED' }, |
| | | ]; |
| | | |
| | | // ==================== å·¥å
·å½æ° ==================== |
| | | |
| | | /** ä»é项æ°ç»å label */ |
| | | export function optionLabel( |
| | | options: { label: string; value: string }[], |
| | | value?: string, |
| | | ): string { |
| | | if (!value) return '-'; |
| | | return options.find((o) => o.value === value)?.label ?? value; |
| | | } |
| | | |
| | | /** éå·ä¸²è½¬æ°ç» */ |
| | | export function splitUrls(urls?: string): string[] { |
| | | if (!urls) return []; |
| | | return urls.split(',').filter(Boolean); |
| | | } |
| | | |
| | | /** æ°ç»è½¬éå·ä¸² */ |
| | | export function joinUrls(urls: string[]): string { |
| | | return urls.filter(Boolean).join(','); |
| | | } |
| | | |
| | | /** æ°åæ ¼å¼åï¼å»æå¤ä½ç 0 */ |
| | | export function numberText(value?: number, precision = 3): string { |
| | | if (value === null || value === undefined) return '-'; |
| | | return Number(value) |
| | | .toFixed(precision) |
| | | .replace(/\.?0+$/, ''); |
| | | } |
| | | |
| | | /** 夿æ¯å¦ä¸ºå¾ç URL */ |
| | | export function isImage(url: string): boolean { |
| | | return /\.(jpg|jpeg|png|webp|gif|bmp|heic)$/i.test(url); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormInstance } from 'ant-design-vue'; |
| | | import type { SafetyApi } from '#/api/safety'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Popconfirm, |
| | | Select, |
| | | Space, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | createTraining, |
| | | deleteTraining, |
| | | getTrainingPage, |
| | | updateTraining, |
| | | } from '#/api/safety'; |
| | | import EmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { TRAINING_TYPE_OPTIONS } from '../shared'; |
| | | |
| | | defineOptions({ name: 'SafetyTraining' }); |
| | | |
| | | const filters = reactive<Record<string, any>>({}); |
| | | const rows = ref<SafetyApi.Training[]>([]); |
| | | const loading = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | |
| | | const open = ref(false); |
| | | const saving = ref(false); |
| | | const formRef = ref<FormInstance>(); |
| | | |
| | | const initial = (): SafetyApi.Training => ({ |
| | | title: '', |
| | | trainingType: 'SAFETY', |
| | | trainDate: dayjs().format('YYYY-MM-DD'), |
| | | durationHours: 2, |
| | | trainerId: undefined, |
| | | location: '', |
| | | participants: '', |
| | | participantCount: 0, |
| | | content: '', |
| | | examResult: '', |
| | | attachment: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const model = reactive<SafetyApi.Training>(initial()); |
| | | |
| | | const rules = { |
| | | title: [{ required: true, whitespace: true, message: '请è¾å
¥å¹è®ä¸»é¢' }], |
| | | trainDate: [{ required: true, message: 'è¯·éæ©å¹è®æ¥æ' }], |
| | | }; |
| | | |
| | | const columns = [ |
| | | { title: 'å¹è®æ¥æ', dataIndex: 'trainDate', width: 130, fixed: 'left' as const }, |
| | | { title: 'å¹è®ä¸»é¢', dataIndex: 'title', minWidth: 220, ellipsis: true }, |
| | | { title: 'ç±»å', dataIndex: 'trainingType', width: 140 }, |
| | | { title: '妿¶', dataIndex: 'durationHours', width: 90, align: 'center' as const }, |
| | | { title: '讲å¸/ç»ç»äºº', dataIndex: 'trainer', width: 130 }, |
| | | { title: 'å¹è®å°ç¹', dataIndex: 'location', width: 160, ellipsis: true }, |
| | | { title: 'åå 人æ°', dataIndex: 'participantCount', width: 110, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) pagination.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const params: any = { ...filters, pageNo: pagination.current, pageSize: pagination.pageSize }; |
| | | if (params.trainDateRange?.length === 2) { |
| | | params.trainDate = params.trainDateRange; |
| | | delete params.trainDateRange; |
| | | } |
| | | const data = await getTrainingPage(params); |
| | | rows.value = data.list; |
| | | pagination.total = data.total; |
| | | } catch { |
| | | message.error('å è½½å¹è®è®°å½å¤±è´¥'); |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetFilters() { |
| | | Object.keys(filters).forEach((k) => delete filters[k]); |
| | | void load(true); |
| | | } |
| | | |
| | | function tableChange(page: any) { |
| | | pagination.current = page.current ?? 1; |
| | | pagination.pageSize = page.pageSize ?? 10; |
| | | void load(); |
| | | } |
| | | |
| | | function showForm(row?: SafetyApi.Training) { |
| | | Object.assign(model, initial(), row ?? {}); |
| | | open.value = true; |
| | | formRef.value?.clearValidate(); |
| | | } |
| | | |
| | | async function save() { |
| | | await formRef.value?.validate(); |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...model, title: model.title.trim() }; |
| | | // 讲å¸/ç»ç»äººå§å为å端åå¡«çå¿«ç
§å段ï¼ä¸åä¸æäº¤ |
| | | delete data.trainer; |
| | | await (data.id ? updateTraining(data) : createTraining(data)); |
| | | message.success('å¹è®è®°å½å·²ä¿å'); |
| | | open.value = false; |
| | | await load(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function remove(row: SafetyApi.Training) { |
| | | await deleteTraining(row.id!); |
| | | message.success('å·²å é¤'); |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | title="å®å
¨å¹è®" |
| | | description="å®å
¨å¹è®ãä¸çº§å®å
¨æè²ãæ³è§å®£è´¯çå¹è®è®°å½ççµåå彿¡£ï¼æ¯æç¾å°è¡¨ä¸è¯¾ä»¶éä»¶çåã" |
| | | auto-content-height |
| | | > |
| | | <Card> |
| | | <Form :model="filters" layout="inline" class="safety-filters" @finish="load(true)"> |
| | | <FormItem label="å¹è®æ¥æ"> |
| | | <DatePicker.RangePicker v-model:value="filters.trainDateRange" value-format="YYYY-MM-DD" /> |
| | | </FormItem> |
| | | <FormItem label="主é¢"> |
| | | <Input v-model:value="filters.title" allow-clear placeholder="å¹è®ä¸»é¢" style="width: 170px" /> |
| | | </FormItem> |
| | | <FormItem label="ç±»å"> |
| | | <Select v-model:value="filters.trainingType" :options="TRAINING_TYPE_OPTIONS" allow-clear placeholder="å
¨é¨" style="width: 150px" /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <Space> |
| | | <Button type="primary" :loading="loading" @click="load(true)">æ¥è¯¢</Button> |
| | | <Button @click="resetFilters">éç½®</Button> |
| | | </Space> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <span class="text-base font-semibold">å¹è®è®°å½</span> |
| | | <Button v-access:code="['safety:training:create']" type="primary" @click="showForm()"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å¢å¹è® |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1300 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'trainingType'"> |
| | | <Tag color="blue">{{ record.trainingTypeName }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'trainer'"> |
| | | <span v-if="record.trainer">{{ record.trainer }}</span> |
| | | <span v-else class="text-gray-300">-</span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'durationHours'"> |
| | | {{ record.durationHours ?? '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Space :size="0"> |
| | | <Button v-access:code="['safety:training:update']" type="link" size="small" @click="showForm(record as SafetyApi.Training)">ç¼è¾</Button> |
| | | <Popconfirm title="ç¡®å®å é¤è¯¥å¹è®è®°å½ï¼" ok-text="ç¡®å®" cancel-text="åæ¶" @confirm="remove(record as SafetyApi.Training)"> |
| | | <Button v-access:code="['safety:training:delete']" type="link" danger size="small">å é¤</Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <Modal |
| | | v-model:open="open" |
| | | :title="model.id ? 'ç¼è¾å¹è®è®°å½' : 'æ°å¢å¹è®è®°å½'" |
| | | :width="720" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="save" |
| | | > |
| | | <Form ref="formRef" :model="model" :rules="rules" :disabled="saving" layout="vertical" class="safety-form"> |
| | | <FormItem label="å¹è®ä¸»é¢" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="å¦ 2026 年度å®å
¨ç产è§ç¨å¹è®" /> |
| | | </FormItem> |
| | | <FormItem label="å¹è®ç±»å" name="trainingType"> |
| | | <Select v-model:value="model.trainingType" :options="TRAINING_TYPE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="å¹è®æ¥æ" name="trainDate"> |
| | | <DatePicker v-model:value="model.trainDate" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="å¹è®å¦æ¶" name="durationHours"> |
| | | <InputNumber v-model:value="model.durationHours" :min="0" :step="0.5" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å¹è®å°ç¹" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="å¦ ä¸æ¥¼å¹è®å®¤" /> |
| | | </FormItem> |
| | | <FormItem label="讲å¸/ç»ç»äºº" name="trainerId"> |
| | | <EmployeeSelect v-model="model.trainerId" placeholder="è¾å
¥å§åæå·¥å·æç´¢è®²å¸/ç»ç»äºº" /> |
| | | </FormItem> |
| | | <FormItem label="åå 人æ°" name="participantCount"> |
| | | <InputNumber v-model:value="model.participantCount" :min="0" style="width: 100%" placeholder="ç空æååèªå¨ç»è®¡" /> |
| | | </FormItem> |
| | | <FormItem label="åå 人å" name="participants" class="col-span-full"> |
| | | <Input v-model:value="model.participants" :maxlength="2000" placeholder="å¤ä¸ªå§åç¨éå·åéï¼å¦ å¼ ä¸,æå,çäº" /> |
| | | </FormItem> |
| | | <FormItem label="å¹è®å
容" name="content" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.content" :maxlength="2000" :rows="3" show-count placeholder="å¹è®ç主è¦å
容åè¦æ±" /> |
| | | </FormItem> |
| | | <FormItem label="èæ ¸æ
åµ" name="examResult" class="col-span-full"> |
| | | <Input.TextArea v-model:value="model.examResult" :maxlength="2000" :rows="2" placeholder="å¦ å
¨åèæ ¸åæ ¼ï¼å¹³åå 92" /> |
| | | </FormItem> |
| | | <FormItem label="éä»¶ï¼ç¾å°è¡¨/课件/ç
§çï¼" name="attachment" class="col-span-full"> |
| | | <Input v-model:value="model.attachment" :maxlength="2000" placeholder="å¤ä¸ª URL ç¨éå·åé" /> |
| | | </FormItem> |
| | | <FormItem label="夿³¨" name="remark" class="col-span-full"> |
| | | <Input v-model:value="model.remark" :maxlength="500" placeholder="éå¡«" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .safety-filters { gap: 8px 0; margin-bottom: 20px; } |
| | | .safety-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; margin-top: 16px; } |
| | | @media (max-width: 640px) { .safety-form { grid-template-columns: minmax(0, 1fr); } } |
| | | </style> |