refactor(safety): 人员与车间字段改用员工选择器与车间下拉
- 安全组织架构:负责人改 HrmEmployeeSelect;删除负责人电话手填项(改由员工档案带出)
- 巡检点位:所属车间改车间下拉(getWorkshopSimpleList)、责任人改员工选择器
- 巡检记录:巡检人改员工选择器
- 隐患台账:上报人 / 分派责任人 / 复查人全部改员工选择器;
同人校验改为按 assigneeId 比较(原按姓名,同名会误判)
- 高危作业票:作业车间改车间下拉,申请人/监护人改员工选择器;
审批弹窗审批人改员工选择器,自批校验按 applicantId 比较;
监护记录监护人改员工选择器
- 应急物资:保管人改员工选择器
- 安全培训:讲师/组织人改员工选择器
- 消防演练:组织人改员工选择器
- 事故登记:所属车间改车间下拉,表格新增车间列
提交时只送 xxxId,不再送姓名快照(快照由后端从主数据带出)。
验证:vue-tsc 类型检查 0 错误;vite build 成功
| | |
| | | id?: number; |
| | | parentId?: number; |
| | | name: string; |
| | | /** 负责人(员工编号,关联 hrm_employee) */ |
| | | leaderId?: number; |
| | | leaderName?: string; |
| | | leaderPhone?: string; |
| | | memberNames?: 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; |
| | |
| | | pointType?: string; |
| | | pointTypeName?: string; |
| | | inspectTime: string; |
| | | inspector: string; |
| | | /** 巡检人(员工编号,关联 hrm_employee) */ |
| | | inspectorId?: number; |
| | | inspector?: string; |
| | | result?: string; |
| | | resultName?: string; |
| | | content?: string; |
| | |
| | | location?: string; |
| | | description?: string; |
| | | photoUrls?: string; |
| | | reporter: string; |
| | | /** 上报人(员工编号,关联 hrm_employee) */ |
| | | reporterId?: number; |
| | | reporter?: string; |
| | | reportTime?: string; |
| | | /** 整改责任人(员工编号,关联 hrm_employee) */ |
| | | assigneeId?: number; |
| | | assignee?: string; |
| | | assignTime?: string; |
| | | rectifyDeadline?: string; |
| | |
| | | rectifyDesc?: string; |
| | | rectifyPhotoUrls?: string; |
| | | rectifyTime?: string; |
| | | /** 复查人(员工编号,关联 hrm_employee) */ |
| | | recheckerId?: number; |
| | | rechecker?: string; |
| | | recheckTime?: string; |
| | | recheckResult?: string; |
| | |
| | | permitType: string; |
| | | permitTypeName?: string; |
| | | title: string; |
| | | /** 作业车间(关联 mes_md_workshop) */ |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | location?: string; |
| | | workContent?: string; |
| | | applicant: string; |
| | | /** 申请人(员工编号,关联 hrm_employee) */ |
| | | applicantId?: number; |
| | | applicant?: string; |
| | | applyTime?: string; |
| | | planStartTime: string; |
| | | planEndTime: 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; |
| | |
| | | id?: number; |
| | | permitId: number; |
| | | monitorTime?: string; |
| | | guardian: string; |
| | | /** 监护人(员工编号,关联 hrm_employee) */ |
| | | guardianId?: number; |
| | | guardian?: string; |
| | | monitorContent?: string; |
| | | abnormal?: string; |
| | | photoUrls?: string; |
| | |
| | | expireDate?: string; |
| | | expireRemainingDays?: number; |
| | | validityMonths?: number; |
| | | /** 保管人(员工编号,关联 hrm_employee) */ |
| | | managerId?: number; |
| | | manager?: string; |
| | | status: number; |
| | | remark?: string; |
| | |
| | | trainingTypeName?: string; |
| | | trainDate: string; |
| | | durationHours?: number; |
| | | /** 讲师/组织人(员工编号,关联 hrm_employee) */ |
| | | trainerId?: number; |
| | | trainer?: string; |
| | | location?: string; |
| | | participants?: string; |
| | |
| | | drillTypeName?: string; |
| | | drillDate: string; |
| | | location?: string; |
| | | /** 组织人(员工编号,关联 hrm_employee) */ |
| | | organizerId?: number; |
| | | organizer?: string; |
| | | participants?: string; |
| | | participantCount?: number; |
| | |
| | | levelName?: string; |
| | | occurTime: string; |
| | | location?: string; |
| | | /** 所属车间(关联 mes_md_workshop) */ |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | casualtyDesc?: string; |
| | | economicLoss?: number; |
| | |
| | | description?: string; |
| | | causeAnalysis?: string; |
| | | measures?: string; |
| | | /** 登记人(员工编号,关联 hrm_employee) */ |
| | | reporterId?: number; |
| | | reporter?: string; |
| | | reportTime?: string; |
| | | status?: string; |
| | |
| | | export function deleteHazard(id: number) { |
| | | return requestClient.delete('/safety/hazard/delete', { params: { id } }); |
| | | } |
| | | export function assignHazard(data: { id: number; assignee: string; rectifyDeadline?: string; remark?: string }) { |
| | | 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 }) { |
| | |
| | | } |
| | | export function recheckHazard(data: { |
| | | id: number; |
| | | rechecker: string; |
| | | recheckerId: number; |
| | | recheckResult: string; |
| | | recheckRemark?: string; |
| | | }) { |
| | |
| | | } |
| | | export function approveWorkPermit(data: { |
| | | id: number; |
| | | approver: string; |
| | | approverId: number; |
| | | decision: string; |
| | | approveComment?: string; |
| | | }) { |
| | |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop'; |
| | | import { |
| | | createAccident, |
| | | deleteAccident, |
| | |
| | | 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>(); |
| | |
| | | level: 'GENERAL', |
| | | occurTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | location: '', |
| | | workshopName: '', |
| | | workshopId: undefined, |
| | | casualtyDesc: '', |
| | | economicLoss: 0, |
| | | involvedPersons: '', |
| | | description: '', |
| | | causeAnalysis: '', |
| | | measures: '', |
| | | reporter: '', |
| | | status: 'REPORTED', |
| | | attachment: '', |
| | | remark: '', |
| | |
| | | { 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 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); |
| | |
| | | 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(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | onMounted(() => { |
| | | void load(); |
| | | void loadWorkshops(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1400 }" |
| | | :scroll="{ x: 1530 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | |
| | | </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"> |
| | |
| | | <FormItem label="发生地点" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="如 2#高压配电室" /> |
| | | </FormItem> |
| | | <FormItem label="所属车间/区域" name="workshopName"> |
| | | <Input v-model:value="model.workshopName" :maxlength="100" placeholder="如 电气车间" /> |
| | | <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 人轻伤" /> |
| | |
| | | getDrillPage, |
| | | updateDrill, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { DRILL_TYPE_OPTIONS } from '../shared'; |
| | | |
| | |
| | | drillType: 'FIRE', |
| | | drillDate: dayjs().format('YYYY-MM-DD'), |
| | | location: '', |
| | | organizer: '', |
| | | organizerId: undefined, |
| | | participants: '', |
| | | participantCount: 0, |
| | | planDesc: '', |
| | |
| | | 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; |
| | |
| | | <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> |
| | |
| | | <FormItem label="演练地点" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="如 厂区东侧消防通道" /> |
| | | </FormItem> |
| | | <FormItem label="组织人" name="organizer"> |
| | | <Input v-model:value="model.organizer" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="组织人" name="organizerId"> |
| | | <HrmEmployeeSelect v-model="model.organizerId" placeholder="从员工档案中选择组织人" /> |
| | | </FormItem> |
| | | <FormItem label="时长(分钟)" name="durationMinutes"> |
| | | <InputNumber v-model:value="model.durationMinutes" :min="0" style="width: 100%" /> |
| | |
| | | updateHazard, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { |
| | | HAZARD_LEVEL_COLOR, |
| | |
| | | location: '', |
| | | description: '', |
| | | photoUrls: '', |
| | | reporter: '', |
| | | reporterId: undefined, |
| | | // 上报人姓名为后端回填的快照字段,仅用于展示 |
| | | reporter: undefined, |
| | | reportTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | rectifyDeadline: undefined, |
| | | remark: '', |
| | |
| | | |
| | | const editRules = { |
| | | title: [{ required: true, whitespace: true, message: '请输入隐患标题' }], |
| | | reporter: [{ required: true, whitespace: true, message: '请输入上报人' }], |
| | | reporterId: [{ required: true, message: '请选择上报人' }], |
| | | }; |
| | | |
| | | const assignOpen = ref(false); |
| | | const assignModel = reactive<{ id: number; assignee: string; rectifyDeadline?: string; remark?: string }>({ |
| | | const assignModel = reactive<{ id: number; assigneeId?: number; rectifyDeadline?: string; remark?: string }>({ |
| | | id: 0, |
| | | assignee: '', |
| | | assigneeId: undefined, |
| | | }); |
| | | |
| | | const rectifyOpen = ref(false); |
| | |
| | | const rectifyPhotos = ref<string[]>([]); |
| | | |
| | | const recheckOpen = ref(false); |
| | | const recheckModel = reactive<{ id: number; rechecker: string; recheckResult: string; recheckRemark?: string }>({ |
| | | const recheckModel = reactive<{ id: number; recheckerId?: number; recheckResult: string; recheckRemark?: string }>({ |
| | | id: 0, |
| | | rechecker: '', |
| | | recheckerId: undefined, |
| | | recheckResult: 'PASSED', |
| | | }); |
| | | |
| | | /** 当前复查的隐患,用于校验「复查人不能与整改责任人为同一人」 */ |
| | | const recheckRow = ref<SafetyApi.Hazard>(); |
| | | |
| | | const detailOpen = ref(false); |
| | | const detailRow = ref<null | SafetyApi.Hazard>(null); |
| | |
| | | const data = { |
| | | ...model, |
| | | title: model.title.trim(), |
| | | reporter: model.reporter.trim(), |
| | | photoUrls: photoList.value.join(','), |
| | | }; |
| | | // 上报人姓名为只读快照,不再提交 |
| | | delete data.reporter; |
| | | await (data.id ? updateHazard(data) : createHazard(data)); |
| | | message.success('隐患已保存'); |
| | | editOpen.value = false; |
| | |
| | | |
| | | function openAssign(row: SafetyApi.Hazard) { |
| | | assignModel.id = row.id!; |
| | | assignModel.assignee = row.assignee ?? ''; |
| | | assignModel.assigneeId = row.assigneeId; |
| | | assignModel.rectifyDeadline = row.rectifyDeadline; |
| | | assignModel.remark = ''; |
| | | assignOpen.value = true; |
| | | } |
| | | |
| | | async function saveAssign() { |
| | | if (!assignModel.assignee.trim()) { |
| | | message.warning('请填写整改责任人'); |
| | | if (!assignModel.assigneeId) { |
| | | message.warning('请选择整改责任人'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await assignHazard({ ...assignModel, assignee: assignModel.assignee.trim() }); |
| | | await assignHazard({ |
| | | id: assignModel.id, |
| | | assigneeId: assignModel.assigneeId, |
| | | rectifyDeadline: assignModel.rectifyDeadline, |
| | | remark: assignModel.remark, |
| | | }); |
| | | message.success('已分派'); |
| | | assignOpen.value = false; |
| | | await load(); |
| | |
| | | |
| | | function openRecheck(row: SafetyApi.Hazard) { |
| | | recheckModel.id = row.id!; |
| | | recheckModel.rechecker = ''; |
| | | recheckModel.recheckerId = undefined; |
| | | recheckModel.recheckResult = 'PASSED'; |
| | | recheckModel.recheckRemark = ''; |
| | | recheckRow.value = row; |
| | | recheckOpen.value = true; |
| | | } |
| | | |
| | | async function saveRecheck() { |
| | | if (!recheckModel.rechecker.trim()) { |
| | | message.warning('请填写复查人'); |
| | | if (!recheckModel.recheckerId) { |
| | | message.warning('请选择复查人'); |
| | | return; |
| | | } |
| | | if (recheckModel.recheckerId === recheckRow.value?.assigneeId) { |
| | | message.warning('复查人不能与整改责任人为同一人'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await recheckHazard({ ...recheckModel, rechecker: recheckModel.rechecker.trim() }); |
| | | await recheckHazard({ |
| | | id: recheckModel.id, |
| | | recheckerId: recheckModel.recheckerId, |
| | | recheckResult: recheckModel.recheckResult, |
| | | recheckRemark: recheckModel.recheckRemark, |
| | | }); |
| | | message.success(recheckModel.recheckResult === 'PASSED' ? '复查通过,隐患已闭环' : '复查不通过,已退回整改'); |
| | | recheckOpen.value = false; |
| | | await load(); |
| | |
| | | <FormItem label="整改期限" name="rectifyDeadline"> |
| | | <DatePicker v-model:value="model.rectifyDeadline" value-format="YYYY-MM-DD" class="w-full" /> |
| | | </FormItem> |
| | | <FormItem label="上报人" name="reporter"> |
| | | <Input v-model:value="model.reporter" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="上报人" name="reporterId"> |
| | | <HrmEmployeeSelect v-model="model.reporterId" placeholder="从员工档案中选择上报人" /> |
| | | </FormItem> |
| | | <FormItem label="上报时间" name="reportTime"> |
| | | <DatePicker |
| | |
| | | <Modal v-model:open="assignOpen" title="分派整改责任人" :confirm-loading="saving" ok-text="确认分派" cancel-text="取消" @ok="saveAssign"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="整改责任人" required> |
| | | <Input v-model:value="assignModel.assignee" :maxlength="50" placeholder="请输入整改责任人姓名" /> |
| | | <HrmEmployeeSelect v-model="assignModel.assigneeId" placeholder="从员工档案中选择整改责任人" /> |
| | | </FormItem> |
| | | <FormItem label="整改期限"> |
| | | <DatePicker v-model:value="assignModel.rectifyDeadline" value-format="YYYY-MM-DD" class="w-full" /> |
| | |
| | | <Modal v-model:open="recheckOpen" title="隐患复查" :confirm-loading="saving" ok-text="提交复查" cancel-text="取消" @ok="saveRecheck"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="复查人" required> |
| | | <Input v-model:value="recheckModel.rechecker" :maxlength="50" placeholder="请输入复查人姓名(不能与整改责任人相同)" /> |
| | | <HrmEmployeeSelect 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" /> |
| | |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop'; |
| | | import { |
| | | createInspectionPoint, |
| | | deleteInspectionPoint, |
| | | getInspectionPointPage, |
| | | updateInspectionPoint, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { INSPECT_CYCLE_OPTIONS, POINT_TYPE_OPTIONS } from '../../shared'; |
| | | |
| | |
| | | code: '', |
| | | name: '', |
| | | pointType: 'HIGH_VOLTAGE_ROOM', |
| | | workshopName: '', |
| | | workshopId: undefined, |
| | | // 车间 / 责任人为后端回填的快照字段,仅用于展示 |
| | | workshopName: undefined, |
| | | location: '', |
| | | inspectItems: '1. 门锁与警示标识完好\n2. 绝缘工器具在有效期内\n3. 消防器材在位可用\n4. 记录本填写完整', |
| | | inspectCycle: 'DAILY', |
| | | ownerName: '', |
| | | 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: '请输入点位编码' }], |
| | |
| | | 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(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | onMounted(() => { |
| | | void loadWorkshops(); |
| | | void load(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <FormItem label="巡检周期" name="inspectCycle"> |
| | | <Select v-model:value="model.inspectCycle" :options="INSPECT_CYCLE_OPTIONS" /> |
| | | </FormItem> |
| | | <FormItem label="所属车间/区域" name="workshopName"> |
| | | <Input v-model:value="model.workshopName" :maxlength="100" placeholder="如 电气车间" /> |
| | | <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="ownerName"> |
| | | <Input v-model:value="model.ownerName" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="责任人" name="ownerId"> |
| | | <HrmEmployeeSelect v-model="model.ownerId" placeholder="从员工档案中选择责任人" /> |
| | | </FormItem> |
| | | <FormItem label="具体位置" name="location" class="col-span-full"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="如 厂区西北角 1 号配电房" /> |
| | |
| | | updateInspection, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { INSPECTION_RESULT_OPTIONS, splitUrls } from '../../shared'; |
| | | |
| | |
| | | const initial = (): SafetyApi.Inspection => ({ |
| | | pointId: 0, |
| | | inspectTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | inspector: '', |
| | | inspectorId: undefined, |
| | | // 巡检人姓名为后端回填的快照字段,仅用于展示 |
| | | inspector: undefined, |
| | | result: 'NORMAL', |
| | | content: '', |
| | | photoUrls: '', |
| | |
| | | const rules = { |
| | | pointId: [{ required: true, message: '请选择巡检点位' }], |
| | | inspectTime: [{ required: true, message: '请选择巡检时间' }], |
| | | inspector: [{ required: true, whitespace: true, message: '请输入巡检人' }], |
| | | inspectorId: [{ required: true, message: '请选择巡检人' }], |
| | | }; |
| | | |
| | | const pointOptions = computed(() => |
| | |
| | | try { |
| | | const data = { |
| | | ...model, |
| | | inspector: model.inspector.trim(), |
| | | photoUrls: photoList.value.join(','), |
| | | }; |
| | | // 巡检人姓名为只读快照,不再提交 |
| | | delete data.inspector; |
| | | await (data.id ? updateInspection(data) : createInspection(data)); |
| | | message.success('巡检记录已保存'); |
| | | open.value = false; |
| | |
| | | class="w-full" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="巡检人" name="inspector"> |
| | | <Input v-model:value="model.inspector" :maxlength="50" placeholder="请输入巡检人姓名" /> |
| | | <FormItem label="巡检人" name="inspectorId"> |
| | | <HrmEmployeeSelect v-model="model.inspectorId" placeholder="从员工档案中选择巡检人" /> |
| | | </FormItem> |
| | | <FormItem label="巡检结果" name="result"> |
| | | <Select v-model:value="model.result" :options="INSPECTION_RESULT_OPTIONS" /> |
| | |
| | | getMaterialStockPage, |
| | | updateMaterial, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { MATERIAL_CATEGORY_COLOR, MATERIAL_CATEGORY_OPTIONS, numberText } from '../shared'; |
| | | |
| | |
| | | storageLocation: '', |
| | | expireDate: undefined, |
| | | validityMonths: undefined, |
| | | manager: '', |
| | | managerId: undefined, |
| | | status: 0, |
| | | remark: '', |
| | | }); |
| | |
| | | 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; |
| | |
| | | <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 }"> |
| | |
| | | <FormItem label="存放位置" name="storageLocation"> |
| | | <Input v-model:value="model.storageLocation" :maxlength="100" placeholder="如 安全工器具室 A 柜" /> |
| | | </FormItem> |
| | | <FormItem label="保管人" name="manager"> |
| | | <Input v-model:value="model.manager" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="保管人" name="managerId"> |
| | | <HrmEmployeeSelect 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" /> |
| | |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { createOrg, deleteOrg, getOrgTree, updateOrg } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | defineOptions({ name: 'SafetyOrg' }); |
| | | |
| | |
| | | const initial = (): SafetyApi.Org => ({ |
| | | parentId: 0, |
| | | name: '', |
| | | leaderName: '', |
| | | leaderPhone: '', |
| | | leaderId: undefined, |
| | | // 负责人姓名 / 电话为后端回填的快照字段,仅用于展示 |
| | | leaderName: undefined, |
| | | leaderPhone: undefined, |
| | | memberNames: '', |
| | | duty: '', |
| | | sort: 1, |
| | |
| | | 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; |
| | |
| | | placeholder="选择上级组织" |
| | | /> |
| | | </FormItem> |
| | | <FormItem label="负责人姓名" name="leaderName"> |
| | | <Input v-model:value="model.leaderName" :maxlength="50" placeholder="请输入姓名" /> |
| | | </FormItem> |
| | | <FormItem label="负责人电话" name="leaderPhone"> |
| | | <Input v-model:value="model.leaderPhone" :maxlength="30" placeholder="选填" /> |
| | | <FormItem label="负责人" name="leaderId"> |
| | | <HrmEmployeeSelect v-model="model.leaderId" placeholder="从员工档案中选择负责人" /> |
| | | </FormItem> |
| | | <FormItem label="显示顺序" name="sort"> |
| | | <InputNumber v-model:value="model.sort" :min="0" :max="9999" style="width: 100%" /> |
| | |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop'; |
| | | import { |
| | | approveWorkPermit, |
| | | cancelWorkPermit, |
| | |
| | | updateWorkPermit, |
| | | uploadSafetyFiles, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { |
| | | PERMIT_STATUS_COLOR, |
| | |
| | | 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 initial = (): SafetyApi.WorkPermit => ({ |
| | | permitType: 'HOT_WORK', |
| | | title: '', |
| | | workshopName: '', |
| | | workshopId: undefined, |
| | | location: '', |
| | | workContent: '', |
| | | applicant: '', |
| | | 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: '', |
| | | guardian: '', |
| | | guardianId: undefined, |
| | | remark: '', |
| | | }); |
| | | |
| | |
| | | const editRules = { |
| | | permitType: [{ required: true, message: '请选择作业类型' }], |
| | | title: [{ required: true, whitespace: true, message: '请输入作业名称' }], |
| | | applicant: [{ required: true, whitespace: true, message: '请输入申请人' }], |
| | | applicantId: [{ required: true, message: '请选择申请人' }], |
| | | planStartTime: [{ required: true, message: '请选择计划开始时间' }], |
| | | planEndTime: [{ required: true, message: '请选择计划结束时间' }], |
| | | }; |
| | |
| | | |
| | | /* 审批 */ |
| | | const approveOpen = ref(false); |
| | | const approveModel = reactive({ id: 0, approver: '', decision: 'APPROVED', approveComment: '' }); |
| | | 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, guardian: '', monitorContent: '', abnormal: '' }); |
| | | const monitorForm = reactive<SafetyApi.WorkPermitMonitor>({ |
| | | permitId: 0, |
| | | guardianId: undefined, |
| | | monitorContent: '', |
| | | abnormal: '', |
| | | }); |
| | | const monitorPhotos = ref<string[]>([]); |
| | | |
| | | const columns = [ |
| | |
| | | { 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 }, |
| | |
| | | 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 = []; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | async function saveEdit() { |
| | | if (guardianRequired.value && !model.guardian?.trim()) { |
| | | if (guardianRequired.value && !model.guardianId) { |
| | | message.warning('动火 / 高压带电 / 有限空间作业必须指定监护人'); |
| | | return; |
| | | } |
| | |
| | | const data = { |
| | | ...model, |
| | | title: model.title.trim(), |
| | | applicant: model.applicant.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; |
| | |
| | | |
| | | /* ---------- 审批 ---------- */ |
| | | function openApprove(row: SafetyApi.WorkPermit) { |
| | | // 记录审批目标行,用于校验“申请人不能审批自己的作业票” |
| | | detailRow.value = row; |
| | | approveModel.id = row.id!; |
| | | approveModel.approver = ''; |
| | | approveModel.approverId = undefined; |
| | | approveModel.decision = 'APPROVED'; |
| | | approveModel.approveComment = ''; |
| | | approveOpen.value = true; |
| | | } |
| | | |
| | | async function saveApprove() { |
| | | if (!approveModel.approver.trim()) { |
| | | message.warning('请填写审批人'); |
| | | const { approverId } = approveModel; |
| | | if (!approverId) { |
| | | message.warning('请选择审批人'); |
| | | return; |
| | | } |
| | | if (approverId === detailRow.value?.applicantId) { |
| | | message.warning('申请人不能审批自己的作业票'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await approveWorkPermit({ ...approveModel, approver: approveModel.approver.trim() }); |
| | | await approveWorkPermit({ |
| | | id: approveModel.id, |
| | | approverId, |
| | | decision: approveModel.decision, |
| | | approveComment: approveModel.approveComment, |
| | | }); |
| | | message.success(approveModel.decision === 'APPROVED' ? '已批准' : '已驳回'); |
| | | approveOpen.value = false; |
| | | await load(); |
| | |
| | | detailRow.value = row; |
| | | detailOpen.value = true; |
| | | monitorForm.permitId = row.id!; |
| | | monitorForm.guardian = row.guardian ?? ''; |
| | | monitorForm.guardianId = row.guardianId; |
| | | monitorForm.monitorContent = ''; |
| | | monitorForm.abnormal = ''; |
| | | monitorPhotos.value = []; |
| | |
| | | } |
| | | |
| | | async function saveMonitor() { |
| | | if (!monitorForm.guardian.trim()) { |
| | | message.warning('请填写监护人'); |
| | | if (!monitorForm.guardianId) { |
| | | message.warning('请选择监护人'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await createMonitor({ |
| | | ...monitorForm, |
| | | guardian: monitorForm.guardian.trim(), |
| | | guardianId: monitorForm.guardianId, |
| | | monitorTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | photoUrls: monitorPhotos.value.join(','), |
| | | }); |
| | |
| | | await load(); |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | onMounted(() => { |
| | | void load(); |
| | | void loadWorkshops(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | row-key="id" |
| | | :loading="loading" |
| | | :pagination="pagination" |
| | | :scroll="{ x: 1750 }" |
| | | :scroll="{ x: 1900 }" |
| | | size="middle" |
| | | :cell-style="{ padding: '12px 16px' }" |
| | | @change="tableChange" |
| | |
| | | </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> |
| | |
| | | <FormItem label="作业名称" name="title" class="col-span-full"> |
| | | <Input v-model:value="model.title" :maxlength="200" placeholder="如 2#主变 10kV 开关柜检修动火" /> |
| | | </FormItem> |
| | | <FormItem label="作业区域/车间" name="workshopName"> |
| | | <Input v-model:value="model.workshopName" :maxlength="100" placeholder="如 电气车间" /> |
| | | <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 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="applicant"> |
| | | <Input v-model:value="model.applicant" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="申请人" name="applicantId"> |
| | | <HrmEmployeeSelect v-model="model.applicantId" placeholder="从员工档案中选择申请人" /> |
| | | </FormItem> |
| | | <FormItem label="监护人" name="guardian"> |
| | | <Input v-model:value="model.guardian" :maxlength="50" :placeholder="guardianRequired ? '动火/高压带电/有限空间必填' : '选填'" /> |
| | | <FormItem label="监护人" name="guardianId"> |
| | | <HrmEmployeeSelect 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="说明作业内容、涉及设备与作业范围" /> |
| | |
| | | <Modal v-model:open="approveOpen" title="作业票审批" :confirm-loading="saving" ok-text="提交审批" cancel-text="取消" @ok="saveApprove"> |
| | | <Form layout="vertical" class="mt-4"> |
| | | <FormItem label="审批人" required> |
| | | <Input v-model:value="approveModel.approver" :maxlength="50" placeholder="请输入审批人姓名(不能与申请人相同)" /> |
| | | <HrmEmployeeSelect v-model="approveModel.approverId" placeholder="从员工档案中选择审批人(不能与申请人相同)" /> |
| | | </FormItem> |
| | | <FormItem label="审批结论" required> |
| | | <Select |
| | |
| | | <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.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> |
| | |
| | | <div class="mb-2 text-sm font-semibold">添加监护记录</div> |
| | | <Form layout="vertical"> |
| | | <FormItem label="监护人"> |
| | | <Input v-model:value="monitorForm.guardian" :maxlength="50" placeholder="姓名" /> |
| | | <HrmEmployeeSelect v-model="monitorForm.guardianId" placeholder="从员工档案中选择监护人" /> |
| | | </FormItem> |
| | | <FormItem label="监护内容与情况"> |
| | | <Input.TextArea v-model:value="monitorForm.monitorContent" :maxlength="1000" :rows="2" placeholder="记录监护时的现场情况" /> |
| | |
| | | getTrainingPage, |
| | | updateTraining, |
| | | } from '#/api/safety'; |
| | | import HrmEmployeeSelect from '#/views/hrm/employee/components/select.vue'; |
| | | |
| | | import { TRAINING_TYPE_OPTIONS } from '../shared'; |
| | | |
| | |
| | | trainingType: 'SAFETY', |
| | | trainDate: dayjs().format('YYYY-MM-DD'), |
| | | durationHours: 2, |
| | | trainer: '', |
| | | trainerId: undefined, |
| | | location: '', |
| | | participants: '', |
| | | participantCount: 0, |
| | |
| | | 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; |
| | |
| | | <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> |
| | |
| | | <FormItem label="培训地点" name="location"> |
| | | <Input v-model:value="model.location" :maxlength="200" placeholder="如 三楼培训室" /> |
| | | </FormItem> |
| | | <FormItem label="讲师/组织人" name="trainer"> |
| | | <Input v-model:value="model.trainer" :maxlength="50" placeholder="请输入姓名" /> |
| | | <FormItem label="讲师/组织人" name="trainerId"> |
| | | <HrmEmployeeSelect v-model="model.trainerId" placeholder="从员工档案中选择讲师/组织人" /> |
| | | </FormItem> |
| | | <FormItem label="参加人数" name="participantCount"> |
| | | <InputNumber v-model:value="model.participantCount" :min="0" style="width: 100%" placeholder="留空按名单自动统计" /> |