Browse Source

feature:增加故障报修模块

liuzhenlin 6 months ago
parent
commit
25419abf39

+ 4 - 1
components.d.ts

@@ -7,10 +7,13 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
+    copy: typeof import('./src/components/select-inst copy.vue')['default']
     CustomForm: typeof import('./src/components/CustomForm.vue')['default']
     FlowTable: typeof import('./src/components/FlowTable.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SelectInst: typeof import('./src/components/select-inst.vue')['default']
+    SelectInstAppointRecord: typeof import('./src/components/select-inst-appoint-record.vue')['default']
     VanActionBar: typeof import('vant/es')['ActionBar']
     VanActionBarButton: typeof import('vant/es')['ActionBarButton']
     VanActionBarIcon: typeof import('vant/es')['ActionBarIcon']
@@ -20,8 +23,8 @@ declare module 'vue' {
     VanCellGroup: typeof import('vant/es')['CellGroup']
     VanCheckbox: typeof import('vant/es')['Checkbox']
     VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
-    VanCol: typeof import('vant/es')['Col']
     VanDatePicker: typeof import('vant/es')['DatePicker']
+    VanDatetimePicker: typeof import('vant/es')['DatetimePicker']
     VanEmpty: typeof import('vant/es')['Empty']
     VanField: typeof import('vant/es')['Field']
     VanFloatingBubble: typeof import('vant/es')['FloatingBubble']

+ 40 - 0
src/api/instr/repairReport.ts

@@ -0,0 +1,40 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-07-14 17:15:49
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-08-16 11:14:42
+ * @Description: file content
+ * @FilePath: \frontend\packages\vue-next-admin-sub\src\api\inst\notice.ts
+ */
+import request from '/@/utils/micro_request.js';
+const basePath = import.meta.env.VITE_INSTR_ADMIN;
+// 故障报修
+export function useRepairReportApi() {
+  return {
+    // 获取列表
+    getList(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'GetList', query);
+    },
+    // 创建
+    create(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'Create', query);
+    },
+    // 详情
+    getDetail(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'GetEntityById', query);
+    },
+
+    // 确认
+    confirm(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'UpdateByEquipmentId', query);
+    },
+    // 关闭 
+    close(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'UpdateStatusById', query);
+    },
+    // 根据仪器id获取仪器所有的报修记录
+    createInstRepair(query?: object) {
+      return request.postRequest(basePath, 'EqpRepairReport', 'GetListByEquipmentId', query);
+    },
+  };
+}

+ 83 - 0
src/components/select-inst-appoint-record.vue

@@ -0,0 +1,83 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-11 18:02:10
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-04-16 16:19:33
+ * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <van-popup v-model:show="state.showPicker" round position="bottom" :style="{ height: '60%' }">
+    <van-picker
+      :loading="state.loading"
+      title="选择预约记录"
+      show-toolbar
+      :columns="state.columns"
+      :columns-field-names="{ text: 'recordName', value: 'id' }"
+      @confirm="onConfirm"
+      @cancel="onCancel"
+      visible-item-count="8"
+    />
+  </van-popup>
+</template>
+
+<script name="selectUseRecord" lang="ts" setup>
+  import { reactive } from 'vue'
+  import { useUseAppointApi } from '/@/api/instr/useAppoint'
+  import to from 'await-to-js'
+  import { formatDate } from '/@/utils/formatTime'
+
+  const emit = defineEmits(['selectUseRecord'])
+  const useAppointApi = useUseAppointApi()
+  const state = reactive({
+    showPicker: false,
+    searchValue: '',
+    loading: false,
+    columns: [],
+    params: {
+      instId: 0
+    }
+  })
+
+  const openPopup = (id: number) => {
+    state.params.instId = id
+    state.showPicker = true
+    state.columns = []
+    getDataList()
+  }
+
+  const getDataList = async () => {
+    state.loading = true
+    const [err, res]: ToResponse = await to(useAppointApi.list({ ...state.params }))
+    state.loading = false
+    if (err) return
+    state.columns = res.data.list.map((item: any) => ({
+      ...item,
+      recordName: `${item.createdName} - ${formatDate(new Date(item.startTime), 'YYYY-mm-dd HH:MM')} 至 ${formatDate(new Date(item.endTime), 'HH:mm')}`
+    }))
+  }
+  const onConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+    state.showPicker = false
+    const selectedOption = selectedOptions[selectedOptions.length - 1]
+    emit('selectUseRecord', selectedOption)
+  }
+  const onCancel = () => {
+    state.showPicker = false
+  }
+  // 暴露变量
+  defineExpose({
+    openPopup
+  })
+</script>
+
+<style lang="scss" scoped>
+  .search-input {
+    flex: 1;
+  }
+  .search-btn {
+    width: 70px;
+    height: 30px;
+    margin: 0 10px;
+    font-size: 14px;
+  }
+</style>

+ 85 - 0
src/components/select-inst.vue

@@ -0,0 +1,85 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-11 18:02:10
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-04-16 16:19:33
+ * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <van-popup v-model:show="state.showPicker" round position="bottom" :style="{ height: '60%' }">
+    <div class="flex">
+      <van-search class="search-input" v-model="state.params.searchText" placeholder="请输入仪器名称" shape="round" @search="getDataList" />
+      <van-button style="" class="search-btn" type="success" size="small" round @click="getDataList">搜索</van-button>
+    </div>
+    <van-picker
+      title="选择仪器"
+      :loading="state.loading"
+      show-toolbar
+      :columns="state.columns"
+      :columns-field-names="{ text: 'instName', value: 'id' }"
+      @confirm="onConfirm"
+      @cancel="onCancel"
+      visible-item-count="8"
+    />
+  </van-popup>
+</template>
+
+<script name="selectInst" lang="ts" setup>
+  import { reactive } from 'vue'
+  import { useInstrApi } from '/@/api/instr/index'
+  import to from 'await-to-js'
+
+  const emit = defineEmits(['selectInst'])
+  const instrApi = useInstrApi()
+  const state = reactive({
+    showPicker: false,
+    searchValue: '',
+    columns: [],
+    loading: false,
+    params: {
+      instStatus: '10',
+      pageNum: 1,
+      pageSize: 9999,
+      searchText: ''
+    }
+  })
+
+  const openPopup = () => {
+    state.showPicker = true
+    state.columns = []
+    getDataList()
+  }
+
+  const getDataList = async () => {
+    state.loading = true
+    const [err, res]: ToResponse = await to(instrApi.getList({ ...state.params }))
+    state.loading = false
+    if (err) return
+    state.columns = res.data.list
+  }
+  const onConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+    state.showPicker = false
+    const selectedOption = selectedOptions[selectedOptions.length - 1]
+    emit('selectInst', selectedOption)
+  }
+  const onCancel = () => {
+    state.showPicker = false
+  }
+  // 暴露变量
+  defineExpose({
+    openPopup
+  })
+</script>
+
+<style lang="scss" scoped>
+  .search-input {
+    flex: 1;
+  }
+  .search-btn {
+    width: 70px;
+    height: 30px;
+    margin: 0 10px;
+    font-size: 14px;
+  }
+</style>

+ 24 - 0
src/router.ts

@@ -98,6 +98,30 @@ const routes = [
       title: '仪器预约'
     }
   },
+  {
+    name: 'repairReportHome',
+    path: '/inst/repairReport/home',
+    component: () => import('/@/view/instr/repairReport/index.vue'),
+    meta: {
+      title: '仪器故障报修'
+    }
+  },
+  {
+    name: 'repairReportAdd',
+    path: '/inst/repairReport/add',
+    component: () => import('/@/view/instr/repairReport/add.vue'),
+    meta: {
+      title: '新增故障报修'
+    }
+  },
+   {
+    name: 'repairReportConfirm',
+    path: '/inst/repairReport/confirm',
+    component: () => import('/@/view/instr/repairReport/confirm.vue'),
+    meta: {
+      title: '确认故障报修'
+    }
+  },
   {
     name: 'approvalDetail',
     path: '/todo/detail',

+ 5 - 1
src/view/home/index.vue

@@ -55,6 +55,10 @@
           <img src="../../assets/img/更多应用.png" alt="" />
           <p>更多应用</p>
         </li>
+        <li @click="onRouterPush('/inst/repairReport/home')">
+          <img src="../../assets/img/更多应用.png" alt="" />
+          <p>故障报修</p>
+        </li>
       </ul>
     </div>
     <div class="swipe-con flex justify-between mt10">
@@ -66,7 +70,7 @@
           </div>
         </van-swipe-item>
       </van-swipe>
-      <a href="javascript:void(0);" @click="onRouterPush('/notice',)">更多</a>
+      <a href="javascript:void(0);" @click="onRouterPush('/notice')">更多</a>
     </div>
     <!-- <div class="card">
       <h4>

+ 290 - 0
src/view/instr/repairReport/add.vue

@@ -0,0 +1,290 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-18 20:02:42
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-21 09:46:32
+ * @FilePath: \labsop_h5\src\view\training\enroll.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="app-container">
+    <van-form ref="formRef" @submit="onSubmit" class="mt10" required="auto">
+      <h4 class="mb8 mt8">设备信息</h4>
+      <van-cell-group>
+        <van-field
+          v-model="state.form.equipmentName"
+          label="设备名称"
+          placeholder="设备名称"
+          @click="openSelectInst"
+          :rules="[{ required: true, message: '请选择仪器' }]"
+        />
+        <van-field v-model="state.form.equipmentCode" label="设备编号" placeholder="设备编号" readonly />
+        <van-field v-model="state.form.equipmentModel" label="设备型号" placeholder="设备型号" readonly />
+        <van-field v-model="state.form.equipmentManager" label="设备负责人" placeholder="设备负责人" readonly />
+        <van-field v-model="state.form.equipmentLocation" label="存放位置" placeholder="存放位置" readonly />
+        <van-field v-model="state.form.recordName" label="预约记录" placeholder="预约记录" @click="openSelectUsedRecord" />
+      </van-cell-group>
+      <h4 class="mb8 mt8">报修信息</h4>
+      <van-cell-group>
+        <van-field v-model="state.form.repairApplicantName" label="报修人" placeholder="报修人" :rules="[{ required: true }]" />
+        <van-field
+          v-model="state.form.repairDate"
+          label="报修时间"
+          placeholder="报修时间"
+          :rules="[{ required: true, message: '请选择报修时间' }]"
+          @click="showAddDatePicker = true"
+          readonly
+        />
+        <van-field
+          v-model="state.form.faultDesc"
+          rows="3"
+          autosize
+          type="textarea"
+          maxlength="200"
+          show-word-limit
+          label="故障说明"
+          placeholder="故障说明"
+          :rules="[{ required: true, message: '请输入故障说明' }]"
+        />
+        <van-field name="uploader" label="文件上传" :rules="[{ required: true, message: '请上传故障照片' }]">
+          <template #input>
+            <van-uploader v-model="state.form.faultPhoto" :after-read="afterRead" preview-size="60" :preview-full-image="true" :max-count="6" />
+          </template>
+        </van-field>
+      </van-cell-group>
+      <van-action-bar placeholder>
+        <van-action-bar-icon icon="wap-home-o" text="首页" @click="router.push('/home')" />
+        <van-action-bar-icon icon="revoke" text="返回" @click="router.push('/inst/repairReport/home')" />
+        <van-action-bar-button type="primary" text="立即提交" :loading="loading" native-type="submit" />
+      </van-action-bar>
+    </van-form>
+  </div>
+
+  <SelectInstPopup ref="SelectInstRef" @selectInst="getSelectInst"></SelectInstPopup>
+
+  <SelectInstAppointRecordPopup ref="usedRecordRef" @selectUseRecord="getSelectUsedRecord"></SelectInstAppointRecordPopup>
+
+  <!-- 所在时间 -->
+  <van-popup v-model:show="showAddDatePicker" position="bottom">
+    <van-date-picker v-model="date" title="选择日期" @confirm="onConfirmDate" :max-date="maxDate" @cancel="showAddDatePicker = false" />
+  </van-popup>
+</template>
+
+<script name="repairReportConfirm" lang="ts" setup>
+  import to from 'await-to-js'
+  import { onMounted, reactive, ref, defineAsyncComponent } from 'vue'
+  import { storeToRefs } from 'pinia'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { showNotify } from 'vant'
+  import { formatDate } from '/@/utils/formatTime'
+  import { useRouter } from 'vue-router'
+  import { handleUpload } from '/@/utils/upload'
+  import { useRepairReportApi } from '/@/api/instr/repairReport'
+
+  const SelectInstPopup = defineAsyncComponent(() => import('/@/components/select-inst.vue'))
+  const SelectInstAppointRecordPopup = defineAsyncComponent(() => import('/@/components/select-inst-appoint-record.vue'))
+
+  const SelectInstRef = ref()
+  const usedRecordRef = ref()
+
+  const repairReportApi = useRepairReportApi()
+  const storesUseUserInfo = useUserInfo()
+  const router = useRouter()
+
+  const { userInfos } = storeToRefs(storesUseUserInfo)
+
+  const formRef = ref()
+  const state = reactive({
+    form: {
+      id: 0,
+      equipmentCode: '', //设备编码	string
+      equipmentId: '', //关联设备	integer
+      equipmentLocation: '', //设备存放位置	string
+      equipmentManager: '', //设备负责人(带电话)	string
+      equipmentModel: '', //设备型号(带品牌)	string
+      equipmentName: '', //设备名称	string
+      recordId: 0,
+      recordName: '',
+      repairApplicantName: '', //报修人
+      repairDate: formatDate(new Date(), 'YYYY-mm-dd'), //报修时间 DATETIME	string
+      faultDesc: '', //故障说明	string
+      faultPhoto: <any>[], //故障照片	string
+      remark: '' //备注	string
+    }
+  })
+  const loading = ref(false) // 加载状态
+
+  const initForm = async () => {
+    state.form = {
+      id: 0,
+      equipmentCode: '', //设备编码	string
+      equipmentId: '', //关联设备	integer
+      equipmentLocation: '', //设备存放位置	string
+      equipmentManager: '', //设备负责人(带电话)	string
+      equipmentModel: '', //设备型号(带品牌)	string
+      equipmentName: '', //设备名称	string
+      recordId: 0,
+      recordName: '',
+      repairApplicantName: '', //报修人
+      repairDate: formatDate(new Date(), 'YYYY-mm-dd'), //报修时间 DATETIME	string
+      faultDesc: '', //故障说明	string
+      faultPhoto: <any>[], //故障照片	string
+      remark: '' //备注	string
+    }
+    date.value = [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]
+    console.log(userInfos.value)
+    state.form.repairApplicantName = userInfos.value.nickName // 报修人
+  }
+
+  const date = ref([]) // 选择的日期
+  const maxDate = ref(new Date()) // 最大日期
+
+  const showAddDatePicker = ref(false) // 是否显示选择日期的弹窗
+  const onSubmit = async () => {
+    console.log('提交表单')
+    const [errValid] = await to(formRef.value.validate())
+    if (errValid) return
+    let params = JSON.parse(JSON.stringify(state.form))
+    console.log(params)
+    const files = params.faultPhoto.map((item: any) => {
+      return {
+        name: item.name,
+        url: item.url
+      }
+    })
+    params.faultPhoto = JSON.stringify(files) // 将图片数组转换为符合后端要求的格式
+    loading.value = true
+    const [err, res]: ToResponse = await to(repairReportApi.create(params))
+    loading.value = false
+    if (err) return
+    showNotify({
+      type: 'success',
+      message: '报修申请已提交,请耐心等待处理'
+    })
+    setTimeout(() => {
+      router.push('/inst/repairReport/home')
+    }, 1000)
+  }
+
+  const openSelectInst = () => {
+    SelectInstRef.value.openPopup()
+  }
+
+  const openSelectUsedRecord = () => {
+    if (state.form.equipmentId === '') {
+      showNotify({
+        type: 'warning',
+        message: '请先选择设备'
+      })
+      return
+    }
+    usedRecordRef.value.openPopup(state.form.equipmentId)
+  }
+
+  const getSelectInst = (data: any) => {
+    console.log(data)
+    state.form.equipmentCode = data.instCode //设备编码	string
+    state.form.equipmentId = data.id //关联设备	integer
+    state.form.equipmentLocation = data.placeAddress //设备存放位置	string
+    state.form.equipmentManager = data.instHeadName + '-' + data.instHeadTel //设备负责人(带电话)	string
+    state.form.equipmentModel = data.instNameEn //设备型号(带品牌)	string
+    state.form.equipmentName = data.instName //设备名称	string};
+    state.form.recordId = 0
+    state.form.recordName = '' //预约记录
+  }
+
+  const getSelectUsedRecord = (data: any) => {
+    console.log(data)
+    state.form.recordId = data.id
+    state.form.recordName = data.createdName
+  }
+
+  const onConfirmDate = () => {
+    showAddDatePicker.value = false
+    state.form.repairDate = `${date.value[0]}-${date.value[1]}-${date.value[2]}`
+  }
+
+  const afterRead = async (files: any) => {
+    if (files.length) {
+      for (const file of files) {
+        file.status = 'uploading'
+        const [err, res]: ToResponse = await to(handleUpload(file.file))
+        if (err) {
+          file.status = 'failed'
+          return
+        }
+        file.status = 'success'
+        file.url = res
+        file.name = file.file.name
+      }
+    } else {
+      const file = files
+      file.status = 'uploading'
+      const [err, res]: ToResponse = await to(handleUpload(file.file))
+      if (err) {
+        file.status = 'failed'
+        return
+      }
+      file.status = 'success'
+      file.url = res
+      file.name = file.file.name
+    }
+  }
+
+  onMounted(async () => {
+    initForm()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .app-container {
+    header {
+      padding: 14px;
+      background-color: #f9ffff;
+      border-radius: 4px;
+      margin-top: 10px;
+    }
+    h4 {
+      margin: 10px 0;
+      height: 20px;
+      line-height: 20px;
+      &::before {
+        display: inline-block;
+        content: '';
+        background-color: #3c78e3;
+        width: 4px;
+        height: 20px;
+        margin-right: 4px;
+        vertical-align: top;
+      }
+    }
+    :deep(.flow-cell) {
+      margin-left: 22px;
+      display: flex;
+      justify-content: space-between;
+      color: #333;
+    }
+  }
+  .ck-editor {
+    height: calc(100vh - 100px);
+    overflow-y: auto;
+    padding: 0 10px;
+  }
+  .need-to-know {
+    height: calc(100% - 20px);
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    padding: 10px 20px;
+    white-space: pre-wrap;
+    p {
+      flex: 1;
+      overflow-y: auto;
+    }
+    footer {
+      flex: 0 0 45px;
+      margin-top: 4px;
+      border-top: 1px solid #f7f8fa;
+    }
+  }
+</style>

+ 272 - 0
src/view/instr/repairReport/confirm.vue

@@ -0,0 +1,272 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-18 20:02:42
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-21 09:46:32
+ * @FilePath: \labsop_h5\src\view\training\enroll.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="app-container">
+    <van-form class="mt10" readonly>
+      <h4 class="mb8 mt8">基本信息</h4>
+      <van-cell-group>
+        <van-field v-model="state.form.equipmentName" label="设备名称" placeholder="设备名称" />
+        <van-field v-model="state.form.equipmentCode" label="设备编号" placeholder="设备编号" readonly />
+        <van-field v-model="state.form.equipmentModel" label="设备型号" placeholder="设备型号" readonly />
+        <van-field v-model="state.form.equipmentManager" label="设备负责人" placeholder="设备负责人" readonly />
+        <van-field v-model="state.form.equipmentLocation" label="存放位置" placeholder="存放位置" readonly />
+        <van-field v-model="state.form.recordName" label="预约记录" placeholder="预约记录" />
+      </van-cell-group>
+      <h4 class="mb8 mt8">报修信息</h4>
+      <van-cell-group>
+        <van-field v-model="state.form.repairApplicantName" label="报修人" placeholder="报修人" />
+        <van-field v-model="state.form.repairDate" label="报修时间" placeholder="报修时间" readonly />
+        <van-field v-model="state.form.faultDesc" rows="1" autosize type="textarea" label="故障说明" placeholder="故障说明" />
+        <van-field name="uploader" label="故障图片"> </van-field>
+        <div class="faultpic-wrap">
+          <van-image
+            class="mr10 mb10"
+            v-for="(v, i) in state.form.faultPhoto"
+            width="100"
+            height="100"
+            :src="v.url"
+            @click="showPreviewImg(state.form.faultPhoto, i)"
+          />
+        </div>
+      </van-cell-group>
+    </van-form>
+    <van-form ref="formRef" @submit="onSubmit" class="mt10" required="auto">
+      <h4 class="mb8 mt8">故障信息</h4>
+      <van-cell-group>
+        <van-field
+          v-model="state.faultForm.faultDate"
+          label="故障时间"
+          placeholder="故障时间"
+          :rules="[{ required: true, message: '请选择故障时间' }]"
+          @click="showAddDatePicker = true"
+          readonly
+        />
+        <van-field
+          v-model="state.faultForm.faultDesc"
+          rows="3"
+          autosize
+          type="textarea"
+          maxlength="200"
+          show-word-limit
+          label="故障说明"
+          placeholder="故障说明"
+          :rules="[{ required: true, message: '请输入故障说明' }]"
+        />
+        <van-field name="uploader" label="文件上传" :rules="[{ required: true, message: '请上传故障照片' }]">
+          <template #input>
+            <van-uploader v-model="state.faultForm.faultPhoto" :after-read="afterRead" preview-size="60" :preview-full-image="true" :max-count="6" />
+          </template>
+        </van-field>
+      </van-cell-group>
+      <van-action-bar placeholder>
+        <van-action-bar-icon icon="wap-home-o" text="首页" @click="router.push('/home')" />
+        <van-action-bar-icon icon="revoke" text="返回" @click="router.push('/inst/repairReport/home')" />
+        <van-action-bar-button type="primary" text="立即提交" :loading="loading" native-type="submit" />
+      </van-action-bar>
+    </van-form>
+  </div>
+
+  <!-- 所在时间 -->
+  <van-popup v-model:show="showAddDatePicker" position="bottom">
+    <van-date-picker v-model="date" title="选择日期" @confirm="onConfirmDate" :max-date="maxDate" @cancel="showAddDatePicker = false" />
+  </van-popup>
+</template>
+
+<script name="repairReportAdd" lang="ts" setup>
+  import to from 'await-to-js'
+  import { onMounted, reactive, ref } from 'vue'
+  import { showImagePreview, showConfirmDialog, showNotify } from 'vant'
+  import { formatDate } from '/@/utils/formatTime'
+  import { useRouter } from 'vue-router'
+  import { handleUpload } from '/@/utils/upload'
+  import { useRepairReportApi } from '/@/api/instr/repairReport'
+
+  const repairReportApi = useRepairReportApi()
+  const router = useRouter()
+
+  const formRef = ref()
+  const state = reactive({
+    form: {
+      equipmentCode: '', //设备编码	string
+      equipmentId: '', //关联设备	integer
+      equipmentLocation: '', //设备存放位置	string
+      equipmentManager: '', //设备负责人(带电话)	string
+      equipmentModel: '', //设备型号(带品牌)	string
+      equipmentName: '', //设备名称	string
+      recordId: 0,
+      recordName: '',
+      repairApplicantName: '', //报修人
+      repairDate: formatDate(new Date(), 'YYYY-mm-dd'), //报修时间 DATETIME	string
+      faultDesc: '', //故障说明	string
+      faultPhoto: <any>[], //故障照片	string
+      remark: '' //备注	string
+    },
+    faultForm: {
+      equipmentId: '', //关联设备	integer
+      id: 0,
+      faultDate: formatDate(new Date(), 'YYYY-mm-dd'), // 故障时间
+      faultDesc: '', // 故障说明
+      faultPhoto: [] // 故障照片
+    }
+  })
+  const loading = ref(false) // 加载状态
+
+  const initForm = async () => {
+    const [err, res]: ToResponse = await to(repairReportApi.getDetail({ id: state.faultForm.id }))
+    if (err) return
+    state.form = res?.data || {}
+    state.form.faultPhoto = JSON.parse(state.form.faultPhoto || '[]') // 将故障照片转换为数组
+    state.faultForm = {
+      equipmentId: state.form.equipmentId,
+      id: Number(router.currentRoute.value.query.id),
+      faultDate: formatDate(new Date(), 'YYYY-mm-dd'), //报修时间 DATETIME	string
+      faultDesc: '', //故障说明	string
+      faultPhoto: <any>[] //故障照片	string
+    }
+    date.value = [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]
+  }
+
+  const date = ref([]) // 选择的日期
+  const maxDate = ref(new Date()) // 最大日期
+
+  const showAddDatePicker = ref(false) // 是否显示选择日期的弹窗
+  const onSubmit = async () => {
+    console.log('提交表单')
+    const [errValid] = await to(formRef.value.validate())
+    if (errValid) return
+    let params = JSON.parse(JSON.stringify(state.faultForm))
+    console.log(params)
+    const files = params.faultPhoto.map((item: any) => {
+      return {
+        name: item.name,
+        url: item.url
+      }
+    })
+    params.faultPhoto = JSON.stringify(files) // 将图片数组转换为符合后端要求的格式
+    loading.value = true
+    const [err, res]: ToResponse = await to(repairReportApi.confirm(params))
+    loading.value = false
+    if (err) return
+    showNotify({
+      type: 'success',
+      message: '确认提交成功'
+    })
+    setTimeout(() => {
+      router.push('/inst/repairReport/home')
+    }, 1000)
+  }
+
+  const onConfirmDate = () => {
+    showAddDatePicker.value = false
+    state.faultForm.faultDate = `${date.value[0]}-${date.value[1]}-${date.value[2]}`
+  }
+
+  const afterRead = async (files: any) => {
+    if (files.length) {
+      for (const file of files) {
+        file.status = 'uploading'
+        const [err, res]: ToResponse = await to(handleUpload(file.file))
+        if (err) {
+          file.status = 'failed'
+          return
+        }
+        file.status = 'success'
+        file.url = res
+        file.name = file.file.name
+      }
+    } else {
+      const file = files
+      file.status = 'uploading'
+      const [err, res]: ToResponse = await to(handleUpload(file.file))
+      if (err) {
+        file.status = 'failed'
+        return
+      }
+      file.status = 'success'
+      file.url = res
+      file.name = file.file.name
+    }
+  }
+
+  const showPreviewImg = (fullPhoto: any, index: number) => {
+    const images = fullPhoto.map((item: any) => item.url)
+    showImagePreview({
+      images,
+      startPosition: index
+    })
+  }
+
+  onMounted(async () => {
+    const id = Number(router.currentRoute.value.query.id)
+    if (id) {
+      state.faultForm.id = id
+      initForm()
+    } else {
+      showNotify({ type: 'warning', message: '未选择待确认报修记录' })
+    }
+  })
+</script>
+
+<style lang="scss" scoped>
+  .app-container {
+    header {
+      padding: 14px;
+      background-color: #f9ffff;
+      border-radius: 4px;
+      margin-top: 10px;
+    }
+    h4 {
+      margin: 10px 0;
+      height: 20px;
+      line-height: 20px;
+      &::before {
+        display: inline-block;
+        content: '';
+        background-color: #3c78e3;
+        width: 4px;
+        height: 20px;
+        margin-right: 4px;
+        vertical-align: top;
+      }
+    }
+    :deep(.flow-cell) {
+      margin-left: 22px;
+      display: flex;
+      justify-content: space-between;
+      color: #333;
+    }
+  }
+  .ck-editor {
+    height: calc(100vh - 100px);
+    overflow-y: auto;
+    padding: 0 10px;
+  }
+  .need-to-know {
+    height: calc(100% - 20px);
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    padding: 10px 20px;
+    white-space: pre-wrap;
+    p {
+      flex: 1;
+      overflow-y: auto;
+    }
+    footer {
+      flex: 0 0 45px;
+      margin-top: 4px;
+      border-top: 1px solid #f7f8fa;
+    }
+  }
+  .faultpic-wrap {
+    padding: 10px;
+    display: flex;
+    flex-wrap: wrap; /* 允许换行 */
+  }
+</style>

+ 244 - 0
src/view/instr/repairReport/index.vue

@@ -0,0 +1,244 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-11 18:02:10
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-04-16 16:19:33
+ * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="entry-container">
+    <van-tabs v-model:active="state.queryParams.status" @change="changeType">
+      <van-tab title="待确认" name="10"></van-tab>
+      <van-tab title="已确认" name="20"></van-tab>
+      <van-tab title="关闭" name="30"></van-tab>
+      <van-tab title="全部" name=""></van-tab>
+    </van-tabs>
+    <div class="list-container">
+      <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+        <van-cell v-for="item in state.list" :key="item">
+          <template #default>
+            <div class="list">
+              <header class="flex justify-between">
+                <strong class="title">{{ `${item.equipmentName}(${item.equipmentCode})` }}</strong>
+                <van-tag v-if="item.status == 10" type="primary">待确认</van-tag>
+                <van-tag v-else-if="item.status == 20" type="success">已确认</van-tag>
+                <van-tag v-else-if="item.status == 30" type="danger">关闭</van-tag>
+              </header>
+              <p class="inst-title">
+                <span>设备负责人</span>
+                <span class="title ml8">
+                  {{ item.equipmentManager.split('-')[0] }} <span class="ml10">(</span> {{ item.equipmentManager.split('-')[1] }} )</span
+                >
+              </p>
+              <p class="inst-title">
+                <span>报修人</span>
+                <span class="title ml8"> {{ item.repairApplicantName }}</span>
+              </p>
+              <p class="inst-title">
+                <span>报修时间</span>
+                <span class="title ml8"> {{ formatDate(new Date(item.repairDate), 'YYYY-mm-dd') }}</span>
+              </p>
+              <p class="inst-title">
+                <span>故障说明</span>
+                <span class="title ml8">{{ item.faultDesc }}</span>
+              </p>
+              <div class="pic-col">
+                <span>故障图片</span>
+                <div>
+                  <van-image
+                    class="mr10 mb10"
+                    v-for="(v, i) in JSON.parse(item.faultPhoto)"
+                    width="100"
+                    height="100"
+                    :src="v.url"
+                    @click="showPreviewImg(item.faultPhoto, i)"
+                  />
+                </div>
+              </div>
+              <footer class="flex justify-between mt4">
+                <span class="title">{{ item.createdName }}</span>
+                <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
+              </footer>
+              <div class="flex mt20 btns">
+                <van-button
+                  style="width: 70px; height: 30px; margin: 0; font-size: 14px"
+                  class="mr20"
+                  type="danger"
+                  size="small"
+                  v-if="item.status == 10"
+                  @click.native.stop="onClose(item)"
+                >
+                  关闭
+                </van-button>
+                <van-button
+                  style="width: 70px; height: 30px; margin: 0; font-size: 14px"
+                  type="primary"
+                  size="small"
+                  v-if="item.status == 10"
+                  @click.native.stop="onConfirm(item.id)"
+                >
+                  确认
+                </van-button>
+              </div>
+            </div>
+          </template>
+        </van-cell>
+      </van-list>
+    </div>
+    <van-floating-bubble v-model:offset="offset" icon="plus" @click="onAdd" axis="y" />
+  </div>
+</template>
+
+<script name="repairReportHome" lang="ts" setup>
+  import to from 'await-to-js'
+  import { formatDate } from '/@/utils/formatTime'
+  import { onMounted, reactive, ref } from 'vue'
+  import { useRouter, useRoute } from 'vue-router'
+  import { useRepairReportApi } from '/@/api/instr/repairReport'
+  import { showImagePreview, showConfirmDialog, showNotify } from 'vant'
+  import 'vant/es/image-preview/style'
+
+  const repairReportApi = useRepairReportApi()
+  const router = useRouter()
+  const route = useRoute()
+  const offset = ref({ x: -80, y: 450 })
+  const state = reactive({
+    queryParams: {
+      equipmentName: '',
+      status: '10',
+      pageNum: 1,
+      pageSize: 10
+    },
+    finished: false,
+    loading: true,
+    list: [] as any[]
+  })
+
+  const changeType = () => {
+    state.queryParams.pageNum = 1
+    state.list = []
+    onLoad()
+  }
+
+  const onLoad = async () => {
+    const [err, res]: ToResponse = await to(repairReportApi.getList(state.queryParams))
+    if (err) return
+    const list = res?.data?.list || []
+    for (const item of list) {
+      state.list.push(item)
+    }
+    state.loading = false
+    state.queryParams.pageNum++
+    if (list.length < state.queryParams.pageSize) {
+      state.finished = true
+    }
+  }
+
+  onMounted(() => {
+    const type = route.query.type
+    if (type) {
+      state.queryParams.status = type as string
+    }
+    onLoad()
+  })
+
+  const onClose = async (item: any) => {
+    showConfirmDialog({
+      title: '提示',
+      message: '确认关闭当前报修记录?'
+    }).then(async () => {
+      const [err, res]: ToResponse = await to(repairReportApi.close({ id: item.id }))
+      if (err) return
+      showNotify({ type: 'success', message: '关闭成功' })
+      changeType() // 刷新列表
+    })
+  }
+
+  const showPreviewImg = (fullPhoto: string, index: number) => {
+    console.log('fullPhoto', fullPhoto)
+    const images = JSON.parse(fullPhoto).map((item: any) => item.url)
+    showImagePreview({
+      images,
+      startPosition: index
+    })
+  }
+
+  const onAdd = () => {
+    router.push('/inst/repairReport/add')
+  }
+
+  const onConfirm = (id: number) => {
+    router.push('/inst/repairReport/confirm?id=' + id)
+  }
+</script>
+
+<style lang="scss" scoped>
+  .entry-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    .list-container {
+      overflow-y: auto;
+      padding: 10px;
+      border-radius: 4px;
+      flex: 1;
+    }
+    .van-list {
+      .van-cell {
+        background-color: #fff;
+        + .van-cell {
+          margin-top: 10px;
+        }
+        header,
+        footer {
+          color: #333;
+        }
+        .title {
+          flex: 1;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          text-align: left;
+        }
+        .inst-title {
+          color: #333;
+          text-align: left;
+          flex: 1;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          margin-top: 4px;
+          span:first-child {
+            color: rgb(120, 120, 120);
+          }
+        }
+        .time {
+          color: #f69a4d;
+        }
+      }
+    }
+  }
+  .pic-col {
+    color: #333;
+    text-align: left;
+    flex: 1;
+    margin-top: 4px;
+    display: flex;
+    span:first-child {
+      width: 100px;
+      color: rgb(120, 120, 120);
+    }
+  }
+  .btns {
+    justify-content: flex-end;
+  }
+  .search-wrap {
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    padding: 0 15px;
+    margin: 10px 0 0;
+    background: #fff;
+  }
+</style>