ソースを参照

feature:增加审批单据信息,跳转小程序增加上下机类型参数

liuzhenlin 1 週間 前
コミット
9467920d0f

+ 9 - 1
src/api/platform/animal/index.ts

@@ -9,7 +9,7 @@
 import request from '/@/utils/micro_request.js';
 const basePath = import.meta.env.VITE_PLATFORM_API;
 export function usePlatAnimalCageApplicationApi() {
-	return {
+  return {
     // 创建
     create(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'Create', params)
@@ -46,6 +46,14 @@ export function usePlatAnimalCageApplicationApi() {
     getVendorToken(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimals', 'GetVendorToken', params)
     },
+    // 获取关联项目第三方
+    getProjectSourceListThirdParty(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimals', 'GetProjects', params);
+    },
+    // 动物伦理信息登记详情
+    getEthicsById(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalsEthics', 'GetById', params);
+    },
     // 导出笼位退还列表
     getCageReleaseApplicationsExport(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetCageReleaseApplicationsExport', params)

+ 18 - 0
src/api/platform/techService/commission.ts

@@ -0,0 +1,18 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2023-07-19 13:42:40
+ * @LastEditors: wanglj
+ * @LastEditTime: 2025-01-20 10:43:14
+ * @Description: file content
+ * @FilePath: \labsop_backup2\frontend\components\labsop-api\src\api\platform\home\index.ts
+ */
+import request from '/@/utils/micro_request.js';
+const basePath = import.meta.env.VITE_PLATFORM_API;
+export function usePlatCommissionApi() {
+  return {
+    // 详情
+    getEntityById(params?: Object) {
+      return request.postRequest(basePath, 'PlatCommission', 'GetEntityById', params);
+    },
+  };
+}

+ 5 - 2
src/constants/pageConstants.ts

@@ -161,6 +161,7 @@ export interface CreateAnimalApplyLeavePayload {
   phone: string // 用户电话
   deptId: string // 用户部门
   deptName: string // 用户部门名称
+  categoryName: string //动物类别
 }
 
 export interface TurnBackPayload {
@@ -214,8 +215,10 @@ export const SUPPORT_FILE_UPLOAD_TYPE_MAX = ".jpg,.jpeg,.png,.doc,.docx,.xls,.xl
 const APPID = 'wx979e7a671a498586'
 
 const PATH = 'pages/appointList/user'
-export const scanCodeWxUrl = (query: string) => {
-  return `weixin://dl/business/?appid=${APPID}&path=${PATH}&query=scene=${query}`
+export const scanCodeWxUrl = (terminal: string, type: 'StartRun' | 'EndRun') => {
+  // 将 terminal 和 type 作为 query 传入小程序,需进行编码避免特殊字符截断
+  const query = encodeURIComponent(`terminal=${terminal}&type=${type}`)
+  return `weixin://dl/business/?appid=${APPID}&path=${PATH}&query=${query}`
 }
 
 // 用户 头像大小 / M

+ 2 - 2
src/view/instr/appointList/inProgress/index.vue

@@ -214,10 +214,10 @@ export default {
     async handleBluetoothDeCode(content) {
       if (content.includes('id')) {
         const terminal = JSON.parse(content)?.terminal
-        const url = scanCodeWxUrl(terminal)
+        const url = scanCodeWxUrl(terminal, 'EndRun')
         window.location.href = url
       } else {
-        const url = scanCodeWxUrl(content)
+        const url = scanCodeWxUrl(content, 'EndRun')
         window.location.href = url
       }
     },

+ 2 - 2
src/view/instr/appointList/onlineInfo/index.vue

@@ -267,10 +267,10 @@ export default {
     async handleBluetoothDeCode(content) {
       if (content.includes('id')) {
         const terminal = JSON.parse(content)?.terminal
-        const url = scanCodeWxUrl(terminal)
+        const url = scanCodeWxUrl(terminal, 'EndRun')
         window.location.href = url
       } else {
-        const url = scanCodeWxUrl(content)
+        const url = scanCodeWxUrl(content, 'EndRun')
         window.location.href = url
       }
     },

+ 2 - 2
src/view/instr/appointList/soonGeton/index.vue

@@ -226,10 +226,10 @@ export default {
     async handleBluetoothDeCode(content) {
       if (content.includes('id')) {
         const terminal = JSON.parse(content)?.terminal
-        const url = scanCodeWxUrl(terminal)
+        const url = scanCodeWxUrl(terminal, 'StartRun')
         window.location.href = url
       } else {
-        const url = scanCodeWxUrl(content)
+        const url = scanCodeWxUrl(content, 'StartRun')
         window.location.href = url
       }
 

+ 208 - 0
src/view/todo/component/plat_animal_takeway_applications.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="facilities-dialog-container">
+    <h4 class="mb12 mt8">申请人信息</h4>
+    <van-cell-group>
+      <van-cell
+        title="申请人姓名"
+        :value="state.form.userName"
+      />
+      <van-cell
+        title="联系电话"
+        :value="state.form.phone"
+      />
+      <van-cell
+        title="课题组"
+        :value="state.form.projectGroupName"
+      />
+      <van-cell
+        title="科室"
+        :value="state.form.deptName"
+      />
+    </van-cell-group>
+
+    <h4 class="mb12 mt20">转出情况</h4>
+    <van-cell-group>
+      <van-cell
+        title="动物类别"
+        :value="state.form.categoryName"
+      />
+      <van-cell
+        title="品种品系"
+        :value="state.form.variety"
+      />
+      <van-cell
+        title="转出日期"
+        :value="state.form.takeawayDate"
+      />
+      <van-cell
+        title="送往地点"
+        :value="state.form.takeawayAddress"
+      />
+      <van-cell
+        title="带出原因"
+        :value="state.form.takeawayReason"
+      />
+      <van-cell
+        title="运输方式"
+        :value="state.form.takeawayTransport"
+      />
+      <van-cell
+        title="门禁卡序列号"
+        :value="state.form.accessCardNumber"
+      />
+      <van-cell title="数量(雄性+雌性=总数)">
+        <template #value>
+          <div class="num-row">
+            <span>{{ state.form.takeawayMaleNumber || 0 }}</span>
+            <span class="num-sep">+</span>
+            <span>{{ state.form.takewayFemaleNumber || 0 }}</span>
+            <span class="num-sep">=</span>
+            <span class="num-total">{{ animalNumber }}</span>
+          </div>
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import { reactive, ref, computed, watch, nextTick } from 'vue'
+  import to from 'await-to-js'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { CreateAnimalApplyLeavePayload, TakeawayList, ActionType } from '/@/constants/pageConstants'
+  import { useUserInfos } from '/@/hooks/useUserInfos'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  const { userInfos } = useUserInfos()
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const animalTypeList = ref<{ id: string; name: string }[]>([])
+  const safePromiseStatus = ref<boolean>(false)
+
+  const defaultFormFields: CreateAnimalApplyLeavePayload = {
+    accessCardNumber: '',
+    categoryId: null,
+    categoryName: null,
+    projectGroupId: null,
+    projectGroupName: '',
+    takeawayDate: '',
+    takeawayAddress: '',
+    takeawayReason: '',
+    takeawayTransport: '',
+    takewayFemaleNumber: 0,
+    takeawayMaleNumber: 0,
+    variety: '',
+    userName: '',
+    phone: '',
+    deptId: '',
+    deptName: '',
+  }
+
+  const state = reactive<{
+    form: CreateAnimalApplyLeavePayload
+    safePromise: boolean
+    safeRead: boolean
+    dialog: { isShowDialog: boolean; type: string; title: string; submitTxt: string }
+  }>({
+    form: defaultFormFields,
+    safePromise: false,
+    safeRead: false,
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+  })
+
+  const animalNumber = computed(() => {
+    const maleNumber = state.form.takeawayMaleNumber || 0
+    const famaleNumber = state.form.takewayFemaleNumber || 0
+    return maleNumber + famaleNumber
+  })
+
+  const getDicts = async () => {
+    const [_, res]: ToResponse = await to(platAnimalCageApplicationApi.getAnimalTypeList({}))
+
+    if (res) {
+      animalTypeList.value = res.data
+    }
+  }
+
+  // 打开弹窗
+  const initForm = async (code: string) => {
+    await getDicts()
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getAnimalTakeawayApplicationsDetail({ id: parseInt(code) }),
+    )
+    if (err) return
+    await nextTick()
+    state.form.userName = userInfos.value.nickName
+    state.form.phone = userInfos.value.phone
+    state.form.deptName = userInfos.value.deptName
+    state.form.projectGroupId = userInfos.value.projectGroup?.id
+    state.form.projectGroupName = userInfos.value.projectGroup?.pgName
+    state.form.deptName = userInfos.value.deptName
+
+    state.form = {
+      ...state.form,
+      ...res.data,
+    }
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+</script>
+<style lang="scss" scoped>
+  :deep(.disUoloadSty .van-uploader__upload) {
+    display: none;
+    /* 上传按钮隐藏 */
+  }
+
+  .form-row {
+    display: flex;
+    flex-wrap: wrap;
+    margin: 0 -10px;
+  }
+
+  .form-col {
+    flex: 0 0 50%;
+    padding: 0 10px;
+    margin-bottom: 20px;
+  }
+
+  .field-label {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 14px;
+    color: #323233;
+  }
+
+  .num-row {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+  }
+
+  .num-sep {
+    color: #969799;
+  }
+
+  .num-total {
+    font-weight: 600;
+    color: #323233;
+  }
+</style>

+ 255 - 0
src/view/todo/component/plat_cage_applications.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="facilities-dialog-container">
+    <van-cell-group>
+      <van-cell
+        title="申请人姓名"
+        :value="state.form.userName"
+      />
+      <van-cell
+        title="课题名称"
+        :value="state.form.projectGroupName"
+      />
+      <van-cell
+        title="申请时间"
+        :value="state.form.createdTime"
+      />
+      <van-cell
+        title="申请状态"
+        :value="state.form.approveStatus"
+      />
+      <van-cell
+        title="申请笼位(个)"
+        :value="state.form.number"
+      />
+      <van-cell
+        title="动物类别"
+        :value="state.form.categoryName"
+      />
+      <van-cell
+        title="品种品系"
+        :value="state.form.variety"
+      />
+      <van-cell
+        title="采购渠道"
+        :value="checkProcurementChannels"
+      />
+      <van-cell
+        title="雄性数量(只)"
+        :value="state.form.maleNumber"
+      />
+      <van-cell
+        title="雌性数量(只)"
+        :value="state.form.famaleNumber"
+      />
+      <van-cell
+        title="动物体重"
+        :value="state.form.weight"
+      />
+      <van-cell
+        title="动物周龄"
+        :value="state.form.age"
+      />
+      <van-cell
+        title="饲养总天数"
+        :value="state.form.feedingDay"
+      />
+      <van-cell
+        title="动物到达时间"
+        :value="state.form.comeTime"
+      />
+      <van-cell
+        title="是否有特殊饲养要求"
+        :value="checkSpecialFeedingRequirements"
+      />
+      <van-cell
+        v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL"
+        title="饲养要求描述"
+        :value="state.form.feedingSpecialDesc"
+      />
+      <van-cell
+        title="外购来源单位"
+        :value="state.form.comeFromUnit"
+      />
+      <van-cell
+        v-if="state.form.ethicsCheckFile"
+        title="伦理审查表"
+      >
+        <template #value>
+          <a
+            :href="parseFileInfo(state.form.ethicsCheckFile).url"
+            target="_blank"
+            class="file-link"
+          >
+            {{ parseFileInfo(state.form.ethicsCheckFile).name }}
+          </a>
+        </template>
+      </van-cell>
+      <van-cell
+        v-if="state.form.ethicsAdviceFile"
+        title="伦理意见表"
+      >
+        <template #value>
+          <a
+            :href="parseFileInfo(state.form.ethicsAdviceFile).url"
+            target="_blank"
+            class="file-link"
+          >
+            {{ parseFileInfo(state.form.ethicsAdviceFile).name }}
+          </a>
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import to from 'await-to-js'
+  import { nextTick, reactive, watch, computed } from 'vue'
+  import dayjs from 'dayjs'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { ProcurementChannels, FeedingSpecial } from '/@/constants/pageConstants'
+
+  const checkProcurementChannels = computed(() => {
+    return state.form.buyFrom === ProcurementChannels.PURCHASED_BY_OTHERS ? '动物房代购' : '自行购买'
+  })
+
+  const checkSpecialFeedingRequirements = computed(() => {
+    return state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL ? '有' : '无'
+  })
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  const approveStatusList = [
+    {
+      name: '待提交',
+      id: 10,
+    },
+    {
+      name: '审批中',
+      id: 20,
+    },
+    {
+      name: '通过',
+      id: 30,
+    },
+    {
+      name: '撤回',
+      id: 35,
+    },
+    {
+      name: '拒绝',
+      id: 40,
+    },
+  ]
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const state = reactive({
+    form: {
+      userName: '',
+      number: 0,
+      approveStatus: '',
+      categoryName: '',
+      variety: '',
+      projectGroupName: '',
+      createdTime: '',
+      maleNumber: 0,
+      famaleNumber: 0,
+      weight: 0,
+      age: 0,
+      feedingDay: 0,
+      buyFrom: '',
+      comeTime: '',
+      comeFromUnit: '',
+      hasFeedingSpecial: '',
+      feedingSpecialDesc: '',
+      licenseNumberFile: '',
+      animalTestDateFile: '',
+      envTestDateFile: '',
+      cageAppointFile: '',
+      ethicsCheckFile: '',
+      ethicsAdviceFile: '',
+    },
+    disabled: false,
+  })
+
+  // 解析附件字段,返回 { name, url }
+  const parseFileInfo = (file: any): { name: string; url: string } => {
+    const info = { name: '', url: '' }
+    if (!file) return info
+    try {
+      if (typeof file === 'string') {
+        const trimmed = file.trim()
+        if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
+          const data = JSON.parse(trimmed)
+          const pick = (d: any) => ({
+            name: d?.name || d?.fileName || d?.originalName || d?.filename || '',
+            url: d?.url || d?.fileUrl || d?.path || d?.filePath || '',
+          })
+          if (Array.isArray(data)) {
+            const first = data[0] || {}
+            return pick(first)
+          }
+          return pick(data)
+        }
+        // 普通字符串:可能是 URL 或文件名
+        if (/^https?:\/\//.test(trimmed) || trimmed.includes('/') || trimmed.includes('\\')) {
+          const segs = trimmed.split(/[\\\/]/)
+          info.name = segs[segs.length - 1] || trimmed
+          info.url = trimmed
+        } else {
+          info.name = trimmed
+        }
+        return info
+      }
+      if (typeof file === 'object') {
+        info.name = file?.name || file?.fileName || file?.originalName || file?.filename || ''
+        info.url = file?.url || file?.fileUrl || file?.path || file?.filePath || ''
+        return info
+      }
+    } catch (e) {
+      const s = String(file)
+      const segs = s.split(/[\\\/]/)
+      info.name = segs[segs.length - 1] || s
+      info.url = s
+    }
+    return info
+  }
+
+  // 打开弹窗
+  const initForm = async (code: string) => {
+    const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getEntityById({ id: parseInt(code) }))
+    if (err) return
+    await nextTick()
+    state.form = {
+      ...res?.data,
+      approveStatus: approveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
+      createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
+    }
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+  // 暴露变量
+  defineExpose({
+    initForm,
+  })
+</script>
+<style lang="scss" scoped>
+  .file-link {
+    color: #1989fa;
+    text-decoration: underline;
+  }
+</style>

+ 121 - 0
src/view/todo/component/plat_cage_release_applications.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="facilities-dialog-container">
+    <van-cell-group>
+      <van-cell
+        title="申请人姓名"
+        :value="state.form.userName"
+      />
+      <van-cell
+        title="课题名称"
+        :value="state.form.projectGroupName"
+      />
+      <van-cell
+        title="申请时间"
+        :value="state.form.createdTime"
+      />
+      <van-cell
+        title="申请状态"
+        :value="state.form.approveStatus"
+      />
+      <van-cell
+        title="退还笼位(个)"
+        :value="state.form.number"
+      />
+      <van-cell
+        title="动物类型"
+        :value="state.form.categoryName"
+      />
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import to from 'await-to-js'
+  import { nextTick, reactive, ref, watch } from 'vue'
+  import dayjs from 'dayjs'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  const approveStatusList = [
+    {
+      name: '待提交',
+      id: 10,
+    },
+    {
+      name: '审批中',
+      id: 20,
+    },
+    {
+      name: '通过',
+      id: 30,
+    },
+    {
+      name: '撤回',
+      id: 35,
+    },
+    {
+      name: '拒绝',
+      id: 40,
+    },
+  ]
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const state = reactive({
+    form: {
+      userName: '',
+      number: 0,
+      approveStatus: '',
+      categoryName: '',
+      projectGroupName: '',
+      createdTime: '',
+    },
+    disabled: false,
+  })
+
+  // 打开弹窗
+  const initForm = async (code: string) => {
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getApplicationReleaseDetail({ id: parseInt(code) }),
+    )
+    if (err) return
+    await nextTick()
+    state.form = {
+      ...res?.data,
+      approveStatus: approveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
+      createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
+    }
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+  // 暴露变量
+  defineExpose({
+    initForm,
+  })
+</script>
+<style lang="scss" scoped>
+  :deep(.disUoloadSty .van-uploader__upload) {
+    display: none;
+    /* 上传按钮隐藏 */
+  }
+
+  .form-row {
+    display: flex;
+    flex-wrap: wrap;
+    margin: 0 -10px;
+  }
+</style>

+ 241 - 0
src/view/todo/component/plat_platform_appoint.vue

@@ -0,0 +1,241 @@
+<template>
+  <div class="facilities-dialog-container">
+    <van-cell-group>
+      <h4 class="mb8 mt8">申请人信息</h4>
+      <van-cell
+        title="申请人姓名"
+        :value="state.form.memberName"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="申请人员类型"
+        :value="displayMemberType"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="申请人手机号"
+        :value="state.form.memberPhone"
+        title-class="cell-title"
+      />
+
+      <h4 class="mb8 mt8">申请人课题组信息</h4>
+      <van-cell
+        title="课题组"
+        :value="state.form.pgName"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="课题组负责人"
+        :value="state.form.mentorName"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="课题组负责人科室"
+        :value="state.form.mentorDeptName"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="课题组负责人电话"
+        :value="state.form.mentorPhone"
+        title-class="cell-title"
+      />
+      <h4 class="mb8 mt8">申请入室平台</h4>
+      <div
+        v-for="itemCell in formatPlatformAppointCell(
+          state.form.platPlatformAppointCellRes,
+          state.form.platPlatformAppointMolecularRes,
+        ).filter((item) => item.platformType === '10' || item.platformType === '20')"
+        :key="itemCell"
+      >
+        <van-cell-group>
+          <van-cell
+            title="申请平台"
+            :value="itemCell.platformName"
+          />
+          <van-cell
+            title="费用"
+            :value="`${itemCell.price}元`"
+          />
+          <van-cell
+            title="申请时长"
+            :value="`${itemCell.platformTime}个月`"
+          />
+
+          <template v-if="itemCell.platformType == '10'">
+            <van-cell
+              title="拟培养细胞种类"
+              :value="itemCell.cellType"
+            />
+            <van-cell
+              title="细胞房预约需求"
+              :value="itemCell.cellSourceType == '10' ? '普通' : '层流'"
+            />
+          </template>
+          <template v-else-if="itemCell.platformType == '20'">
+            <van-cell
+              title="其他需求"
+              :value="itemCell.platOtherNeed"
+            />
+          </template>
+        </van-cell-group>
+        <br />
+      </div>
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import to from 'await-to-js'
+  import { nextTick, reactive, ref, watch, computed } from 'vue'
+  import { usePlatformAppointApi } from '/@/api/platform/appoint'
+  import { useSystemApi } from '/@/api/platform/system'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  const activeNames = ref(['1', '2', '3'])
+
+  // 定义变量内容
+  const Api = usePlatformAppointApi()
+  const systemApi = useSystemApi()
+  const deptRef = ref()
+
+  const rules = {
+    pubId: { required: true, message: '请选择投稿刊物', trigger: 'blur' },
+    unitRankFirst: { required: true, message: '请输入单位排名(第一)', trigger: 'blur' },
+    unitRankSecond: { required: false, message: '请输入单位排名(第二)', trigger: 'blur' },
+    projectName: { required: false },
+  }
+  const state = reactive({
+    form: {
+      id: 0,
+      deptId: null,
+      deptName: '',
+      isTemporary: '10',
+      memberId: 0,
+      memberName: '',
+      memberPhone: '',
+      memberType: '',
+      mentorObj: null,
+      pgId: null,
+      pgName: '',
+      mentorId: null,
+      mentorName: '',
+      mentorDeptName: '',
+      mentorPhone: '',
+      platformId: null,
+      platformName: '',
+      platformTime: null,
+      platformType: '',
+      platformList: [],
+      isCellChecked: '20',
+      cellType: '',
+      cellSourceType: '',
+      isMolecularChecked: '20',
+      molecularTime: null,
+      platOtherNeed: '',
+      platPlatformAppointCellRes: [],
+      platPlatformAppointMolecularRes: [],
+      appointStartDate: '',
+      appointEndDate: '',
+      startDate: '',
+      endDate: '',
+      appointDate: [] as any[],
+      dateRange: [] as any[],
+      applyPg: { id: null, pgName: '' } as any,
+    },
+    isShowDialog: false,
+    dialog: {
+      isShowDialog: false,
+      approveDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+    disabled: false,
+  })
+
+  const belongOrgOption = ref<DeptTreeType[]>([])
+  const userList = ref<RowUserType[]>([])
+  const userTypeList = ref<RowDicDataType[]>([])
+  const userSexList = ref<RowDicDataType[]>([])
+  const pjtList = ref()
+
+  const displayMemberType = computed(() => {
+    const match = userTypeList.value.find((item) => item.dictValue === state.form.memberType)
+    return match?.dictLabel || state.form.memberType || '-'
+  })
+  const getDicts = () => {
+    Promise.all([
+      systemApi.getDeptTree(),
+      systemApi.getUserList({ noPage: true }),
+      systemApi.getDictDataByType('sys_user_type'),
+      systemApi.getDictDataByType('sys_com_sex'),
+      systemApi.getProjectGroupList(),
+    ]).then(([dept, user, type, sex, pjt]) => {
+      belongOrgOption.value = dept?.data || []
+      userList.value = user?.data?.list || []
+      userTypeList.value = type.data.values || []
+      userSexList.value = sex.data.values || []
+      pjtList.value = pjt.data.list || []
+    })
+  }
+  // 打开弹窗
+  const initForm = async (code: string) => {
+    getDicts()
+    state.dialog.title = '技术平台-入室申请'
+    state.disabled = true
+    const [err, res]: ToResponse = await to(Api.getDetail({ id: parseInt(code) }))
+    if (err) return
+    state.disabled = true
+    state.dialog.isShowDialog = true
+    await nextTick()
+    state.form = res?.data
+    state.form.appointDate = [state.form.appointStartDate, state.form.appointEndDate]
+    state.form.dateRange = [state.form.startDate, state.form.endDate]
+    state.form.applyPg = {
+      id: state.form.pgId,
+      pgName: state.form.pgName,
+    }
+  }
+
+  const formatPlatformAppointCell = (cellRes: any[], molecularRes: any[]) => {
+    return [...(cellRes || []), ...(molecularRes || [])]
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+  // 暴露变量
+  defineExpose({
+    initForm,
+  })
+</script>
+<style lang="scss" scoped>
+  /* Vant 自定义样式 */
+  .cell-title {
+    font-weight: 500;
+    width: 140px;
+  }
+
+  /* 确保移动端适配 */
+  @media (max-width: 768px) {
+    .cell-title {
+      width: 120px;
+    }
+  }
+
+  /* 调整单元格间距 */
+  :deep(.van-cell) {
+    padding: 12px 16px;
+  }
+</style>

+ 260 - 0
src/view/todo/component/plat_platform_out_room_appoint.vue

@@ -0,0 +1,260 @@
+<template>
+  <div class="facilities-dialog-container">
+    <van-cell-group>
+      <h4 class="mb8 mt8">申请人信息</h4>
+      <van-cell title="申请人姓名" :value="state.form.memberName" title-class="cell-title" />
+      <van-cell title="申请人员类型" :value="displayMemberType" title-class="cell-title" />
+      <van-cell title="申请人手机号" :value="state.form.memberPhone" title-class="cell-title" />
+      
+      <h4 class="mb8 mt8">申请人课题组信息</h4>
+      <van-cell title="课题组" :value="state.form.pgName" title-class="cell-title" />
+      <van-cell title="课题组负责人" :value="state.form.mentorName" title-class="cell-title" />
+      <van-cell title="课题组负责人科室" :value="state.form.mentorDeptName" title-class="cell-title" />
+      <van-cell title="课题组负责人电话" :value="state.form.mentorPhone" title-class="cell-title" />
+      
+      <h4 class="mb8 mt8">申请出室信息</h4>
+      <van-cell title="出室时间" :value="state.form.outApplyTime" title-class="cell-title" />
+      <!-- <el-row :gutter="35" v-if="state.form.platformTime">
+				<el-col :span="12" class="mb20">
+					<el-form-item label="申请平台" prop="isCellChecked">
+						<el-checkbox v-model="state.form.isCellChecked" true-label="10" false-label="20">细胞</el-checkbox>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="申请时长(月)" prop="platformTime">
+						<el-input-number v-model="state.form.platformTime" placeholder="请输入申请时长" :step="1" step-strictly class="w100"></el-input-number>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="拟培养细胞种类" prop="cellType">
+						<el-input v-model="state.form.cellType" placeholder="请输入拟培养细胞种类"></el-input>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="细胞房预约需求" prop="cellSourceType">
+						<el-radio-group v-model="state.form.cellSourceType">
+							<el-radio label="10">普通</el-radio>
+							<el-radio label="20">层流</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="35" v-else>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="申请平台" prop="isMolecularChecked">
+						<el-checkbox v-model="state.form.isMolecularChecked" true-label="10" false-label="20">分子生物平台</el-checkbox>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="申请时长(月)" prop="molecularTime">
+						<el-input-number v-model="state.form.molecularTime" placeholder="请输入申请时长" :step="1" step-strictly class="w100"></el-input-number>
+					</el-form-item>
+				</el-col>
+				<el-col :span="12" class="mb20">
+					<el-form-item label="其他需求" prop="platOtherNeed">
+						<el-input v-model="state.form.platOtherNeed" placeholder="请输入其他需求"></el-input>
+					</el-form-item>
+				</el-col>
+			</el-row> -->
+      <!-- <el-descriptions
+				border
+				:column="2"
+				class="mb20"
+				direction="vertical"
+				v-for="itemCell in formatPlatformAppointCell(state.form.platPlatformAppointCellRes, state.form.platPlatformAppointMolecularRes)"
+				:key="itemCell"
+			>
+
+				<el-descriptions-item label-class-name="cell-item" label="申请平台">
+					<tempalte>
+						<div>
+							{{ itemCell.platformName }}
+						</div>
+					</tempalte>
+				</el-descriptions-item>
+				<el-descriptions-item label-class-name="cell-item" label="费用">
+					<tempalte>
+						<div>{{ itemCell.price }}元</div>
+					</tempalte>
+				</el-descriptions-item>
+				<el-descriptions-item label-class-name="cell-item" label="申请时长">
+					<tempalte>
+						<div>{{ itemCell.platformTime }}个月</div>
+					</tempalte>
+				</el-descriptions-item>
+
+        <tempalte v-if="itemCell.platformType == '10'">
+
+          <el-descriptions-item label-class-name="cell-item" label="拟培养细胞种类">{{ itemCell.cellType }}</el-descriptions-item>
+          <el-descriptions-item label-class-name="cell-item" label="细胞房预约需求">
+            {{ itemCell.cellSourceType == '10' ? '普通' : '层流' }}
+          </el-descriptions-item>
+        </tempalte>
+        <template v-else-if="itemCell.platformType == '20'">
+
+          <el-descriptions-item label-class-name="cell-item" label="其他需求">
+            {{ itemCell.platOtherNeed }}
+          </el-descriptions-item>
+        </template>
+			</el-descriptions> -->
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+import to from 'await-to-js';
+import { nextTick, reactive, ref, watch, computed } from 'vue';
+import { usePlatformAppointApi } from '/@/api/platform/appoint';
+import { useSystemApi } from '/@/api/platform/system';
+
+// 定义子组件向父组件传值/事件
+const props = defineProps({
+  code: { type: String, default: '' },
+});
+
+const activeNames = ref(['1', '2', '3']);
+
+// 定义变量内容
+const Api = usePlatformAppointApi();
+const systemApi = useSystemApi();
+const deptRef = ref();
+
+const rules = {
+  pubId: { required: true, message: '请选择投稿刊物', trigger: 'blur' },
+  unitRankFirst: { required: true, message: '请输入单位排名(第一)', trigger: 'blur' },
+  unitRankSecond: { required: false, message: '请输入单位排名(第二)', trigger: 'blur' },
+  projectName: { required: false },
+};
+const state = reactive({
+  form: {
+    id: 0,
+    deptId: null,
+    deptName: '',
+    isTemporary: '10',
+    memberId: 0,
+    memberName: '',
+    memberPhone: '',
+    memberType: '',
+    mentorObj: null,
+    pgId: null,
+    pgName: '',
+    mentorId: null,
+    mentorName: '',
+    mentorDeptName: '',
+    mentorPhone: '',
+    platformId: null,
+    platformName: '',
+    platformTime: null,
+    platformType: '',
+    platformList: [],
+    isCellChecked: '20',
+    cellType: '',
+    cellSourceType: '',
+    isMolecularChecked: '20',
+    molecularTime: null,
+    platOtherNeed: '',
+    platPlatformAppointCellRes: [],
+    platPlatformAppointMolecularRes: [],
+    appointStartDate: '',
+    appointEndDate: '',
+    startDate: '',
+    endDate: '',
+    appointDate: [] as any[],
+    dateRange: [] as any[],
+    applyPg: { id: null, pgName: '' } as any,
+    outApplyTime: '',
+  },
+  isShowDialog: false,
+  dialog: {
+    isShowDialog: false,
+    approveDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  disabled: false,
+});
+
+const belongOrgOption = ref<DeptTreeType[]>([]);
+const userList = ref<RowUserType[]>([]);
+const userTypeList = ref<RowDicDataType[]>([]);
+const userSexList = ref<RowDicDataType[]>([]);
+const pjtList = ref();
+
+const displayMemberType = computed(() => {
+  const match = userTypeList.value.find((item) => item.dictValue === state.form.memberType);
+  return match?.dictLabel || state.form.memberType || '-';
+});
+const getDicts = () => {
+  Promise.all([
+    systemApi.getDeptTree(),
+    systemApi.getUserList({ noPage: true }),
+    systemApi.getDictDataByType('sys_user_type'),
+    systemApi.getDictDataByType('sys_com_sex'),
+    systemApi.getProjectGroupList(),
+  ]).then(([dept, user, type, sex, pjt]) => {
+    belongOrgOption.value = dept?.data || [];
+    userList.value = user?.data?.list || [];
+    userTypeList.value = type.data.values || [];
+    userSexList.value = sex.data.values || [];
+    pjtList.value = pjt.data.list || [];
+  });
+};
+// 打开弹窗
+const initForm = async (code: string) => {
+  getDicts();
+  state.dialog.title = '技术平台-入室申请';
+  state.disabled = true;
+  const [err, res]: ToResponse = await to(Api.getDetail({ id: parseInt(code) }));
+  if (err) return;
+  state.disabled = true;
+  state.dialog.isShowDialog = true;
+  await nextTick();
+  state.form = res?.data;
+  state.form.appointDate = [state.form.appointStartDate, state.form.appointEndDate];
+  state.form.dateRange = [state.form.startDate, state.form.endDate];
+  state.form.applyPg = {
+    id: state.form.pgId,
+    pgName: state.form.pgName,
+  };
+};
+
+const formatPlatformAppointCell = (cellRes: any[], molecularRes: any[]) => {
+  return [...(cellRes || []), ...(molecularRes || [])];
+};
+
+watch(
+  () => props.code,
+  (val) => {
+    initForm(val);
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+);
+// 暴露变量
+defineExpose({
+  initForm,
+});
+</script>
+<style lang="scss" scoped>
+/* Vant 自定义样式 */
+.cell-title {
+  font-size: 14px;
+  font-weight: 500;
+  width: 140px;
+}
+
+/* 确保移动端适配 */
+@media (max-width: 768px) {
+  .cell-title {
+    width: 120px;
+  }
+}
+
+/* 调整单元格间距 */
+:deep(.van-cell) {
+  padding: 12px 16px;
+}
+</style>

+ 184 - 0
src/view/todo/component/plat_platform_renewal.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="facilities-dialog-container">
+    <h4 class="mb8 mt8">原申请信息</h4>
+    <van-cell-group>
+      <van-cell
+        title="入室申请名称"
+        :value="`${state.oldForm.userName}的${state.oldForm.platformName}的入室申请`"
+      />
+      <van-cell
+        title="申请平台"
+        :value="state.oldForm.platformName"
+      />
+      <van-cell
+        title="入室周期"
+        :value="`${state.oldForm?.startTime ? formatDate(new Date(state.oldForm?.startTime), 'YYYY-mm-dd') : ''} ~ ${
+          state.oldForm?.endTime ? formatDate(new Date(state.oldForm?.endTime), 'YYYY-mm-dd') : ''
+        }`"
+      />
+    </van-cell-group>
+    <br />
+    <van-cell-group>
+      <h4 class="mb8 mt8">续期信息</h4>
+      <van-cell
+        title="续期时长(个月)"
+        :value="state.form.platformTime"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="续期后周期"
+        :value="formatDate(new Date(state.oldForm.startTime), 'YYYY-mm-dd') + ' ~ ' + state.form.endTime"
+        title-class="cell-title"
+      />
+      <van-cell
+        title="其他说明"
+        :value="state.form.remark || '无'"
+        title-class="cell-title"
+      />
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import to from 'await-to-js'
+  import { nextTick, reactive, watch } from 'vue'
+  import { formatDate } from '/@/utils/formatTime'
+  import { usePlatformRenewalApi } from '/@/api/platform/home/renewal'
+  import { useCellAssignApi } from '/@/api/platform/home/assign'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  // 定义变量内容
+  const Api = usePlatformRenewalApi()
+  const assignApi = useCellAssignApi()
+  const rules = {
+    memberName: { required: true, message: '不能为空', trigger: 'blur' },
+    memberType: { required: true, message: '不能为空', trigger: 'change' },
+    pgName: { required: true, message: '不能为空', trigger: 'blur' },
+  }
+  const state = reactive({
+    oldForm: {
+      id: 0,
+      userId: null,
+      userName: '',
+      platformId: 0,
+      platformName: '',
+      assignStatus: '',
+      startTime: null,
+      endTime: null,
+      resId: null,
+      resName: '',
+      resLocation: '',
+      platformTime: null,
+    },
+    form: {
+      id: 0,
+      assignId: 0,
+      platformId: 0,
+      platformName: '',
+      resId: null,
+      resName: '',
+      userId: null,
+      userName: '',
+      platformTime: 1,
+      endTime: '',
+      remark: '',
+    },
+    safePromise: false,
+    safeRead: false,
+    isRead: false,
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+  })
+  // 打开弹窗
+  const initForm = async (row: any) => {
+    state.dialog.title = '续期申请'
+    const [e, resp]: ToResponse = await to(Api.getDetail({ id: parseInt(row) }))
+    if (e) return
+    state.form = resp?.data || {}
+
+    const [err, res]: ToResponse = await to(assignApi.getDetail({ id: resp?.data?.assignId }))
+    if (err) return
+    state.oldForm = res?.data || {}
+
+    dateChange()
+    state.dialog.submitTxt = '提 交'
+    state.dialog.isShowDialog = true
+    // 清空表单,此项需加表单验证才能使用
+    await nextTick()
+  }
+
+  const dateChange = () => {
+    // 在endTime上添加月份
+    state.form.endTime = addMonths(state.oldForm.endTime, state.form.platformTime)
+  }
+
+  const addMonths = (dateStr, x) => {
+    let date = new Date(dateStr) // 解析日期字符串
+    let year = date.getFullYear() // 获取年份
+    let month = date.getMonth() + 1 // 获取月份 (从 0 开始,所以加 1)
+    let day = date.getDate() // 获取日期
+
+    // 计算新日期
+    let newMonth = month + x
+    let newYear = year + Math.floor((newMonth - 1) / 12)
+    let finalMonth = ((newMonth - 1) % 12) + 1
+
+    // 创建新的日期对象,并处理月份天数不足的情况
+    let finalDate = new Date(newYear, finalMonth, 0) // 获取该月最后一天
+    let finalDay = day > finalDate.getDate() ? finalDate.getDate() : day // 如果超过天数,则取最后一天
+
+    return formatDate(new Date(newYear, finalMonth - 1, finalDay), 'YYYY-mm-dd') // 返回结果日期
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+  // 暴露变量
+  defineExpose({
+    initForm,
+  })
+</script>
+<style lang="scss" scoped>
+  /* Vant 自定义样式 */
+  .cell-title {
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 18px;
+    color: #646566;
+    width: 140px;
+  }
+
+  /* 确保移动端适配 */
+  @media (max-width: 768px) {
+    .cell-title {
+      width: 120px;
+    }
+  }
+
+  /* 调整单元格间距 */
+  :deep(.van-cell) {
+    padding: 12px 16px;
+  }
+
+  :deep(.van-cell__value) {
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 18px;
+    color: #323233;
+  }
+</style>

+ 803 - 0
src/view/todo/component/plat_service_commission.vue

@@ -0,0 +1,803 @@
+<template>
+  <div class="facilities-dialog-container">
+    <div class="container">
+      <h4 class="tit">委托基本信息</h4>
+      <van-cell-group>
+        <van-cell
+          class="cell-item"
+          title="委托人(甲方)"
+          :value="state.form?.commitName"
+        />
+        <van-cell
+          class="cell-item"
+          title="委托人电话"
+          :value="state.form?.commitContact"
+        />
+        <van-cell
+          class="cell-item"
+          title="导师(经费人)"
+          :value="state.form?.teacherName"
+        />
+        <van-cell
+          class="cell-item"
+          title="导师电话"
+          :value="state.form?.teacherContact"
+        />
+        <van-cell
+          class="cell-item"
+          title="单位/科室"
+          :value="state.form?.deptName"
+        />
+        <van-cell
+          class="cell-item"
+          title="委托人邮箱"
+          :value="state.form?.commitMail"
+        />
+        <van-cell
+          class="cell-item"
+          title="课题组"
+          :value="state.form?.projName"
+        />
+        <van-cell
+          class="cell-item"
+          title="证件号"
+          :value="state.form?.studentNo"
+        />
+      </van-cell-group>
+      <h4 class="tit">委托实验信息</h4>
+      <div class="plat-wrap">
+        <div
+          v-for="(plat, platIdx) in platformDataReduce(state.form.service)"
+          :key="platIdx"
+        >
+          <div class="plat-items">
+            <h3 class="mb20">{{ `${platIdx + 1}、${plat.platName}` }}</h3>
+            <div class="mb14">
+              <!-- <span class="label">选择课题:</span>
+								<el-select v-model="plat.service.projectId" placeholder="Select" style="width: 400px">
+									<el-option-group v-for="group in projectGroupProjectList" :key="group.groupName" :label="group.groupName">
+										<el-option v-for="item in group.projects" :key="item.id" :label="item.projectName" :value="item.id" />
+									</el-option-group>
+								</el-select> -->
+            </div>
+            <van-empty
+              v-if="!plat.service.length"
+              description="暂无数据"
+              class="mt20 mb20"
+            />
+            <ul
+              v-else
+              class="service-list"
+            >
+              <li
+                v-for="(service, serviceIdx) in plat.service"
+                :key="serviceIdx"
+                class="service-card"
+              >
+                <div class="service-header">
+                  <div class="service-title">{{ service.serviceName }}</div>
+                  <span
+                    v-if="service.detail?.projectSource"
+                    class="tag"
+                  >
+                    {{ service.detail.projectSource }}
+                  </span>
+                </div>
+
+                <div class="kv-row">
+                  <span class="kv-label">实验类型</span>
+                  <span class="kv-value">{{ service.serviceName }}</span>
+                </div>
+
+                <div class="kv-row">
+                  <span class="kv-label">实验项目</span>
+                  <div class="chip-list">
+                    <span
+                      v-for="(itemRow, itemIdx) in filterItems(service.item)"
+                      :key="itemIdx"
+                      class="chip"
+                    >
+                      {{ itemRow.itemName }} · {{ itemRow.itemCount }} 次
+                    </span>
+                    <span
+                      v-if="!filterItems(service.item).length"
+                      class="text-muted"
+                    >
+                      暂无
+                    </span>
+                  </div>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.commissionContent"
+                >
+                  <span class="kv-label">委托内容</span>
+                  <span class="kv-value">{{ service.detail.commissionContent }}</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.experimentalPeriod"
+                >
+                  <span class="kv-label">实验周期</span>
+                  <span class="kv-value">{{ service.detail.experimentalPeriod }} 个月</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.animalType"
+                >
+                  <span class="kv-label">动物种类</span>
+                  <span class="kv-value">{{ service.detail.animalType }}</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.animalCount"
+                >
+                  <span class="kv-label">动物数量</span>
+                  <span class="kv-value">{{ service.detail.animalCount }} 只</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.estimateCost"
+                >
+                  <span class="kv-label">预估费用</span>
+                  <span class="kv-value">{{ service.detail.estimateCost }} 元</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.settlementCost"
+                >
+                  <span class="kv-label">结算费用</span>
+                  <span class="kv-value">{{ service.detail.settlementCost }} 元</span>
+                </div>
+
+                <div
+                  class="kv-row"
+                  v-if="service.detail.other"
+                >
+                  <span class="kv-label">其它</span>
+                  <span class="kv-value">{{ service.detail.other }}</span>
+                </div>
+
+                <div class="kv-row attach-row">
+                  <span class="kv-label">附件</span>
+                  <a
+                    v-if="service?.file?.[0]?.fileUrl"
+                    :href="service?.file[0]?.fileUrl"
+                    target="_blank"
+                    class="file-link"
+                  >
+                    {{ service?.file[0]?.fileName }}
+                  </a>
+                  <span
+                    v-else
+                    class="text-muted"
+                  >
+                    暂无
+                  </span>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+      <h4 class="tit">费用及支付方式</h4>
+      <p class="mb10">本项技术服务项目费用构成如下:</p>
+      <p class="mb10 desc">
+        说明:以下费用是根据标准情况得出的预估费用,实际实验过程中根据实验流程不同,可能存在项目和耗材的增加或减少,最终费用请以实验完成后的结算单为准。
+      </p>
+      <div class="bill-list">
+        <div
+          v-for="(row, index) in flattenedData(state.form.bill)"
+          :key="index"
+          class="bill-row"
+        >
+          <div class="bill-header">
+            <span class="bill-index">{{ index + 1 }}</span>
+            <span class="bill-title">{{ `${row.serviceName}-${row.itemName}` }}</span>
+          </div>
+          <div class="bill-line">
+            <span class="label">费用类型</span>
+            <span class="value">{{ row.chargeName }}</span>
+          </div>
+          <div class="bill-line">
+            <span class="label">单价</span>
+            <span class="value">{{ formatAmount(row.amount) }} 元</span>
+          </div>
+          <div class="bill-line">
+            <span class="label">数量</span>
+            <span class="value">{{ row.itemCount }} 次</span>
+          </div>
+          <div class="bill-line total">
+            <span class="label">小计</span>
+            <span class="value highlight">{{ formatAmount(row.itemCount * row.amount) }} 元</span>
+          </div>
+        </div>
+        <div class="bill-summary">
+          <span>合计次数:{{ totalBillCount }} 次</span>
+          <span class="summary-amount">合计金额:{{ totalBillAmount }} 元</span>
+        </div>
+      </div>
+      <van-cell-group class="amount-info mt20 mb20">
+        <van-cell title="本次技术服务费总金额">
+          <template #right-icon>
+            <van-button
+              type="warning"
+              size="small"
+              link
+            >
+              {{ state.form.amount }}元
+            </van-button>
+          </template>
+        </van-cell>
+        <van-cell title="大写金额">
+          <template #right-icon>
+            <van-button
+              type="warning"
+              size="small"
+              link
+            >
+              {{ numberToChinese(state.form.amount) }}
+            </van-button>
+          </template>
+        </van-cell>
+        <van-cell
+          title="其他事项"
+          :value="state.form.otherInfo"
+        />
+      </van-cell-group>
+      <van-cell-group
+        class="mt20 mb20"
+        v-if="state.form.finallyAmount"
+      >
+        <van-cell title="本次技术服务费最终总金额">
+          <template #right-icon>
+            <van-button
+              type="warning"
+              size="small"
+              link
+            >
+              {{ state.form.finallyAmount / 100 }}元
+            </van-button>
+          </template>
+        </van-cell>
+        <van-cell title="大写金额">
+          <template #right-icon>
+            <van-button
+              type="warning"
+              size="small"
+              link
+            >
+              {{ numberToChinese(state.form.finallyAmount / 100) }}
+            </van-button>
+          </template>
+        </van-cell>
+        <van-cell
+          title="最终费用备注"
+          :value="state.form.finallyAmountRemark"
+        />
+      </van-cell-group>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import to from 'await-to-js'
+  import { nextTick, reactive, watch, computed } from 'vue'
+  import { usePlatCommissionApi } from '/@/api/platform/techService/commission'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  // 定义变量内容
+  const platCommissionApi = usePlatCommissionApi()
+  const state = reactive({
+    form: {
+      id: 0,
+      serviceName: '',
+      serviceImg: '',
+      serviceDesc: '',
+      serviceContract: '',
+      serviceNotice: '',
+      serviceContent: '',
+      serviceResult: '',
+      service: [],
+      bill: [],
+    } as any,
+    dialog: {
+      approveDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+    disabled: false,
+  })
+
+  // 打开弹窗
+  const initForm = async (code: string) => {
+    state.dialog.title = '委托信息'
+    state.disabled = true
+    const [err, res]: ToResponse = await to(platCommissionApi.getEntityById({ id: parseInt(code) }))
+    if (err) return
+    const data = res?.data
+
+    // 如果是加急委托,添加加急费用
+    if (data && data.isUrgent === 1) {
+      // 计算现有项目费用总和(单位:分)
+      const existingTotal = data.bill.reduce((total: number, item: any) => {
+        return (
+          total +
+          item.charge.reduce((chargeTotal: number, charge: any) => {
+            return chargeTotal + item.itemCount * charge.amount
+          }, 0)
+        )
+      }, 0)
+
+      // 计算加急费用(总费用减去现有费用,单位:分)
+      const urgentFee = data.amount - existingTotal
+
+      // 如果加急费用大于0,添加加急费用项目
+      if (urgentFee > 0) {
+        const urgentItem = {
+          id: 'urgent_' + data.id,
+          platformId: data.platformId,
+          platformName: data.platformName,
+          serviceId: 'urgent',
+          serviceName: '加急服务',
+          commissionId: data.id,
+          commissionName: data.commissionName,
+          itemId: 'urgent_fee',
+          itemName: '加急费用',
+          itemDesc: '加急处理费用',
+          itemCount: 1,
+          remark: '加急服务费用',
+          charge: [
+            {
+              chargeName: '加急费用',
+              amount: urgentFee,
+              itemId: 'urgent_fee',
+            },
+          ],
+        }
+
+        // 将加急费用添加到 bill 数组的开头
+        data.bill.push(urgentItem)
+      }
+    }
+
+    data.service = data.service.map((item: any) => ({
+      ...item,
+      detail: JSON.parse(item.detail || '{}'),
+    }))
+
+    state.form = data
+    await nextTick()
+    state.form = data
+  }
+  const platformDataReduce = (data: any) => {
+    if (!data) return
+    const groupedData = data.reduce((acc: any, current: any) => {
+      // 查找是否已存在对应platformId的项
+      const existingPlatform = acc.find((platform: any) => platform.platid === current.platformId)
+
+      if (existingPlatform) {
+        // 如果存在,则将当前服务添加到services数组中
+        existingPlatform.service.push(current)
+      } else {
+        // 如果不存在,则创建新的对象并添加到acc数组中
+        acc.push({ platid: current.platformId, platName: current.platformName, service: [current] })
+      }
+
+      return acc
+    }, [])
+    console.log(groupedData)
+    return groupedData
+  }
+  // 拍平bill数据
+  const flattenedData = (data: any) => {
+    if (!data) return
+    return data.flatMap((item: any) =>
+      item.charge.map((charge: any) => ({
+        ...item,
+        chargeName: charge.chargeName,
+        amount: charge.amount,
+        // 移除原始的 charge 数组
+      })),
+    )
+  }
+  // 计算总金额
+  const getTotalAmount = () => {
+    const data = flattenedData(state.form.bill)
+    if (!data) return 0
+    const total = data.reduce((prev: number, curr: any) => {
+      const itemCount = Number(curr.itemCount)
+      const amount = Number(curr.amount)
+      if (!isNaN(itemCount) && !isNaN(amount)) {
+        return prev + itemCount * amount
+      } else {
+        return prev
+      }
+    }, 0)
+    state.form.amount = total / 100 // 更新总金额
+    return total
+  }
+
+  const totalBillCount = computed(() => {
+    const data = flattenedData(state.form.bill)
+    if (!data) return 0
+    return data.reduce((sum: number, row: any) => sum + Number(row.itemCount || 0), 0)
+  })
+
+  const totalBillAmount = computed(() => formatAmount(getTotalAmount()))
+
+  const filterItems = (items: any[]) => {
+    if (!items) return []
+    return items.filter((it: any) => Number(it?.itemCount) > 0)
+  }
+
+  // 格式化金额为千分位
+  const formatAmount = (amount: number | string) => {
+    // 确保输入是数字
+    let amountInFen = typeof amount === 'string' ? parseInt(amount, 10) : amount
+
+    // 将分转换为元(保留两位小数)
+    let amountInYuan = amountInFen / 100
+
+    // 使用 Intl.NumberFormat 进行千分位格式化,并保留两位小数
+    let formattedAmount = new Intl.NumberFormat('en-US', {
+      minimumFractionDigits: 2, // 最小小数位数
+      maximumFractionDigits: 2, // 最大小数位数
+    }).format(amountInYuan)
+
+    return formattedAmount
+  }
+
+  // 将数字转换为中文大写金额
+  const numberToChinese = (num: number) => {
+    console.log('num', num)
+    if (!num) return
+    if (num == 0) return '零元整'
+    const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
+    const units = ['', '拾', '佰', '仟']
+    const bigUnits = ['', '万', '亿', '兆']
+    const decimalUnits = ['角', '分']
+    const integerPartName = '元'
+    const zeroChar = '零'
+
+    let result = ''
+    let integerPart = Math.floor(num) // 整数部分
+    let decimalPart = Math.round((num - integerPart) * 100) // 小数部分,保留两位
+
+    // 处理整数部分
+    if (integerPart === 0) {
+      result += zeroChar + integerPartName
+    } else {
+      let integerStr = integerPart.toString()
+      let length = integerStr.length
+
+      for (let i = 0; i < length; i++) {
+        let digit = parseInt(integerStr[i])
+        let unitIndex = (length - i - 1) % 4
+        let bigUnitIndex = Math.floor((length - i - 1) / 4)
+
+        if (digit === 0) {
+          // 如果当前位是零,并且不是最后一位,避免连续多个零
+          if (result.slice(-1) !== zeroChar && result.slice(-1) !== integerPartName) {
+            result += zeroChar
+          }
+        } else {
+          result += digits[digit] + units[unitIndex]
+        }
+
+        // 添加大单位(万、亿等)
+        if (unitIndex === 0 && result.slice(-1) !== zeroChar) {
+          result += bigUnits[bigUnitIndex]
+        }
+      }
+
+      result += integerPartName
+    }
+
+    // 处理小数部分
+    if (decimalPart > 0) {
+      let decimalStr = decimalPart.toString().padStart(2, '0') // 补齐两位小数
+      let jiao = parseInt(decimalStr[0])
+      let fen = parseInt(decimalStr[1])
+
+      if (jiao > 0) {
+        result += digits[jiao] + decimalUnits[0]
+      }
+
+      if (fen > 0) {
+        result += digits[fen] + decimalUnits[1]
+      } else if (jiao > 0) {
+        result += '整' // 如果有角但没有分,则加“整”
+      }
+    } else {
+      result += '整' // 没有小数部分时加“整”
+    }
+
+    return result
+  }
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+  // 暴露变量
+  defineExpose({
+    initForm,
+  })
+</script>
+<style lang="scss" scoped>
+  .tit {
+    margin-bottom: 20px;
+    margin-top: 20px;
+    color: #1c9bfd;
+  }
+
+  /* 描述列表布局样式 */
+  .descriptions-row {
+    display: flex;
+    flex-wrap: wrap;
+    margin: -8px 0;
+
+    .cell-item {
+      flex: 0 0 50%;
+      padding: 8px 0;
+    }
+  }
+
+  .base-info {
+    margin-top: 20px;
+    margin-bottom: 20px;
+
+    :deep(.van-cell) {
+      border-bottom: 1px solid #ebedf0;
+    }
+  }
+
+  .amount-info {
+    margin-top: 20px;
+    margin-bottom: 20px;
+
+    :deep(.van-cell) {
+      border-bottom: 1px solid #ebedf0;
+    }
+  }
+
+  .plat-wrap {
+    .plat-items {
+      width: 100%;
+      border-radius: 4px;
+      background: #fff;
+      padding: 20px;
+      padding-right: 0;
+      box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.06);
+      margin-bottom: 20px;
+      box-sizing: border-box;
+      ul {
+        height: 100%;
+        overflow: auto;
+        padding-right: 20px;
+        li {
+          margin-bottom: 20px;
+        }
+      }
+      .charge {
+        border-bottom: 2px dashed rgba(112, 112, 112, 0.18);
+        .charge-name {
+          width: 200px;
+          margin: 0 10px;
+        }
+      }
+    }
+  }
+
+  ul,
+  li {
+    list-style: none;
+  }
+  .label {
+    display: inline-block;
+    width: 100px;
+  }
+  .dashed-border {
+    border-bottom: 2px dashed rgba(112, 112, 112, 0.18);
+  }
+  .desc {
+    color: #f59a23;
+  }
+
+  /* 自定义表格样式 */
+  .bill-list {
+    margin-top: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .bill-row {
+    padding: 12px;
+    border: 1px solid #ebedf0;
+    border-radius: 8px;
+    background: #fff;
+    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.04);
+  }
+
+  .bill-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 6px;
+  }
+
+  .bill-index {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 20px;
+    height: 20px;
+    border-radius: 10px;
+    background: #1c9bfd;
+    color: #fff;
+    font-size: 12px;
+  }
+
+  .bill-title {
+    font-weight: 600;
+    color: #323233;
+    font-size: 14px;
+  }
+
+  .bill-line {
+    display: flex;
+    justify-content: space-between;
+    font-size: 13px;
+    color: #646566;
+    padding: 4px 0;
+  }
+
+  .bill-line .value {
+    color: #323233;
+    font-weight: 500;
+  }
+
+  .bill-line.total {
+    border-top: 1px dashed #ebedf0;
+    margin-top: 4px;
+    padding-top: 8px;
+  }
+
+  .highlight {
+    color: #f59a23;
+    font-weight: 600;
+  }
+
+  .bill-summary {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    padding: 12px;
+    border-radius: 8px;
+    background: #f7f8fa;
+    color: #323233;
+    font-size: 14px;
+  }
+
+  .summary-amount {
+    color: #f59a23;
+    font-weight: 700;
+  }
+
+  .file-link {
+    color: #1989fa;
+    text-decoration: underline;
+  }
+
+  .text-muted {
+    color: #969799;
+  }
+
+  .service-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .service-card {
+    background: #fff;
+    border: 1px solid #ebedf0;
+    border-radius: 10px;
+    padding: 14px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
+  }
+
+  .service-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 8px;
+    margin-bottom: 10px;
+  }
+
+  .service-title {
+    font-weight: 700;
+    font-size: 15px;
+    color: #323233;
+    flex: 1;
+  }
+
+  .tag {
+    display: inline-flex;
+    align-items: center;
+    padding: 4px 8px;
+    border-radius: 999px;
+    background: #e8f4ff;
+    color: #1c9bfd;
+    font-size: 12px;
+    white-space: nowrap;
+  }
+
+  .kv-row {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 10px;
+    padding: 6px 0;
+    font-size: 13px;
+    color: #646566;
+    border-bottom: 1px dashed #f0f1f5;
+  }
+
+  .kv-row:last-child {
+    border-bottom: none;
+  }
+
+  .kv-label {
+    flex: 0 0 80px;
+    color: #969799;
+  }
+
+  .kv-value {
+    flex: 1;
+    color: #323233;
+    text-align: right;
+  }
+
+  .attach-row .kv-value,
+  .attach-row .file-link,
+  .attach-row .text-muted {
+    text-align: right;
+  }
+
+  .chip-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+    justify-content: flex-end;
+  }
+
+  .chip {
+    display: inline-flex;
+    align-items: center;
+    padding: 4px 8px;
+    border-radius: 999px;
+    background: #f5f7fa;
+    color: #323233;
+    font-size: 12px;
+    border: 1px solid #ebedf0;
+  }
+</style>

+ 219 - 0
src/view/todo/component/platform_animal_appointment_ethics.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="facilities-dialog-container">
+    <h4 class="mb8 mt8">基本信息</h4>
+    <van-cell-group>
+      <van-cell
+        title="关联课题"
+        :value="projectGroupNameDisplay"
+      />
+      <van-cell
+        title="项目来源"
+        :value="state.form.experimentName"
+      />
+      <van-cell
+        title="实验目的"
+        :value="state.form.experimentTarget"
+      />
+      <van-cell
+        title="拟实验动物种类"
+        :value="state.form.experimentAnimalCate"
+      />
+      <van-cell
+        title="拟实验动物数量"
+        :value="state.form.experimentAnimalNum"
+      />
+    </van-cell-group>
+
+    <h4 class="mb8 mt20">伦理信息</h4>
+    <van-cell-group>
+      <van-cell
+        title="审查委员会"
+        :value="state.form.ethicsCommittee"
+      />
+      <van-cell
+        title="审查时间"
+        :value="state.form.checkTime"
+      />
+      <van-cell
+        title="审查结果"
+        :value="state.form.checkResult"
+      />
+      <van-cell
+        title="有效时间"
+        :value="`${state.form.validTimeRange[0] || ''} - ${state.form.validTimeRange[1] || ''}`"
+      />
+    </van-cell-group>
+
+    <h4 class="mb8 mt20">文件信息</h4>
+    <van-cell-group>
+      <van-cell title="实验动物福利伦理审查申请表">
+        <template #value>
+          <a
+            v-if="state.form.applyFiles?.length"
+            :href="state.form.applyFiles[0].url"
+            target="_blank"
+            class="file-link"
+          >
+            {{ state.form.applyFiles[0].name }}
+          </a>
+          <span v-else>暂无</span>
+        </template>
+      </van-cell>
+      <van-cell title="实验动物福利伦理审查意见表">
+        <template #value>
+          <a
+            v-if="state.form.ethicsFiles?.length"
+            :href="state.form.ethicsFiles[0].url"
+            target="_blank"
+            class="file-link"
+          >
+            {{ state.form.ethicsFiles[0].name }}
+          </a>
+          <span v-else>暂无</span>
+        </template>
+      </van-cell>
+      <van-cell title="其他材料">
+        <template #value>
+          <div
+            v-if="state.form.otherFiles?.length"
+            class="file-list"
+          >
+            <a
+              v-for="item in state.form.otherFiles"
+              :key="item.url"
+              :href="item.url"
+              target="_blank"
+              class="file-link"
+            >
+              {{ item.name }}
+            </a>
+          </div>
+          <span v-else>暂无</span>
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+  import { reactive, ref, computed, watch } from 'vue'
+  import to from 'await-to-js'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+
+  // 定义子组件向父组件传值/事件
+  const props = defineProps({
+    code: { type: String, default: '' },
+  })
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const projectSourceList = ref<{ id: number; projectCode: string; projectName: string }[]>([])
+
+  const projectGroupNameDisplay = computed(() => {
+    const match = projectSourceList.value.find((item) => item.id === state.form.projectGroupId)
+    return match?.projectName || state.form.projectGroupId || ''
+  })
+
+  const state = reactive<any>({
+    form: {
+      experimentName: '',
+      projectGroupId: '',
+      experimentTarget: '',
+      experimentAnimalCate: '',
+      experimentAnimalNum: '',
+      checkTime: '',
+      ethicsCommittee: '',
+      validStartTime: '',
+      validEndTime: '',
+      checkResult: '',
+      applyFiles: [],
+      ethicsFiles: [],
+      otherFiles: [],
+      validTimeRange: [],
+    },
+    dialog: {
+      isShowDialog: false,
+      title: '',
+      type: '',
+    },
+  })
+
+  const initForm = async (code: string) => {
+    handleGetProjectSourceList()
+    const [err, res]: ToResponse = await to((platAnimalCageApplicationApi as any).getEthicsById({ id: parseInt(code) }))
+    if (err) return
+
+    const data = res?.data
+
+    state.form = {
+      ...data,
+      validTimeRange: [data.validStartTime, data.validEndTime],
+      applyFiles: data.applyFiles ? JSON.parse(data.applyFiles) : [],
+      ethicsFiles: data.ethicsFiles ? JSON.parse(data.ethicsFiles) : [],
+      otherFiles: data.otherFiles ? JSON.parse(data.otherFiles) : [],
+    }
+  }
+
+  state.dialog.isShowDialog = true
+
+  const handleGetProjectSourceList = async () => {
+    const [err, res]: ToResponse = await to((platAnimalCageApplicationApi as any).getProjectSourceListThirdParty())
+    if (err) return
+
+    projectSourceList.value = res?.data
+  }
+
+  watch(
+    () => props.code,
+    (val) => {
+      initForm(val)
+    },
+    {
+      deep: true,
+      immediate: true,
+    },
+  )
+</script>
+<style lang="scss" scoped>
+  // 表单布局样式
+  .form-row {
+    display: flex;
+    flex-wrap: wrap;
+    margin: 0 -10px;
+
+    .form-col {
+      flex: 1;
+      min-width: 0;
+      padding: 0 10px;
+    }
+
+    .form-col-full {
+      width: 100%;
+      padding: 0 10px;
+    }
+  }
+
+  // 上传组件样式
+  .upload-container {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #969799;
+    margin-top: 8px;
+  }
+
+  // 自定义字段样式
+  :deep(.van-field__label) {
+    font-weight: 500;
+  }
+
+  // 组件容器样式
+  .facilities-dialog-container {
+    padding: 10px;
+  }
+</style>

+ 160 - 146
src/view/todo/detail.vue

@@ -20,28 +20,29 @@
       <van-cell title="申请人" title-class="cell-title" :value="state.form.startUserName" />
       <van-cell title="申请时间" title-class="cell-title" :value="state.form.startTime" />
     </van-cell-group>
-    <h4>附件信息</h4>
+    <h4>单据信息</h4>
     <InstrumentAppointment v-if="state.form.defCode === 'instrument_appointment'" :code="state.form.businessCode" />
-
+    <PlatPlatformAppoint v-if="state.form.defCode === 'plat_platform_appoint'" :code="state.form.businessCode" />
+    <platServiceCommission v-if="state.form.defCode === 'commission_create'" :code="state.form.businessCode" />
+    <PlatCageApplications v-if="state.form.defCode === 'plat_cage_applications'" :code="state.form.businessCode" />
+    <PlatCageReleaseApplications v-if="state.form.defCode === 'plat_cage_release_applications'"
+      :code="state.form.businessCode" />
+    <PlatPlatformRenewal v-if="state.form.defCode === 'plat_platform_renewal'" :code="state.form.businessCode" />
+    <PlatAnimalTakewayApplications v-if="state.form.defCode === 'plat_animal_takeway_applications'"
+      :code="state.form.businessCode"></PlatAnimalTakewayApplications>
+    <PlatPlatformOutRoomAppoint v-if="state.form.defCode === 'plat_platform_out_room_appoint'"
+      :code="state.form.businessCode" />
+    <PlatformAnimalAppointmentEthics v-if="state.form.defCode === 'platform_animal_appointment_ethics'"
+      :code="state.form.businessCode" />
     <h4>审批记录</h4>
     <FlowTable :id="state.form.id" :businessCode="state.form.businessCode" :defCode="state.form.defCode" />
     <h4 v-if="state.type === 'approval'">审批意见</h4>
     <van-form v-if="state.type === 'approval'" class="mb20" ref="formRef" required="auto">
       <van-cell-group>
-        <van-field
-          v-model="state.approvalForm.approveOpinion"
-          label="审批意见"
-          placeholder="请输入审批意见"
-          :rules="[{ required: true, message: '请输入审批意见' }]"
-          rows="3"
-          type="textarea"
-        />
-        <van-field
-          v-model="state.approvalForm.approveResult"
-          label="审批结果"
-          placeholder="请选择审批结果"
-          :rules="[{ required: true, message: '请选择审批结果' }]"
-        >
+        <van-field v-model="state.approvalForm.approveOpinion" label="审批意见" placeholder="请输入审批意见"
+          :rules="[{ required: true, message: '请输入审批意见' }]" rows="3" type="textarea" />
+        <van-field v-model="state.approvalForm.approveResult" label="审批结果" placeholder="请选择审批结果"
+          :rules="[{ required: true, message: '请选择审批结果' }]">
           <template #input>
             <van-radio-group v-model="state.approvalForm.approveResult" direction="horizontal">
               <van-radio name="pass">通过</van-radio>
@@ -60,146 +61,159 @@
 </template>
 
 <script name="home" lang="ts" setup>
-  import to from 'await-to-js'
-  import { formatDate } from '/@/utils/formatTime'
-  import { defineAsyncComponent, onMounted, reactive, ref } from 'vue'
-  import { useRouter, useRoute } from 'vue-router'
-  import { useExecutionApi } from '/@/api/execution'
-  import { useFlowApi } from '/@/api/execution/flow'
+import to from 'await-to-js'
+import { formatDate } from '/@/utils/formatTime'
+import { defineAsyncComponent, onMounted, reactive, ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useExecutionApi } from '/@/api/execution'
+import { useFlowApi } from '/@/api/execution/flow'
 import { showNotify } from 'vant'
-  const FlowTable = defineAsyncComponent(() => import('/@/components/FlowTable.vue'))
-  const InstrumentAppointment = defineAsyncComponent(() => import('/@/view/todo/component/instrument_appointment.vue'))
-  const executionApi = useExecutionApi()
-  const flowApi = useFlowApi()
-  const router = useRouter()
-  const route = useRoute()
-  const apprList = ref<any[]>([])
-  const formRef = ref()
-  const state = reactive({
-    type: 'approval',
-    form: {
-      id: 0,
-      defId: 0,
-      defCode: '',
-      defName: '',
-      instTitle: '',
-      nodeId: '',
-      candidate: '',
-      taskId: 0,
-      startTime: '',
-      endTime: '',
-      duration: 0,
-      startUserId: 0,
-      startUserName: '',
-      isFinish: '',
-      platformId: 0,
-      remark: '',
-      createdBy: 0,
-      createdName: '',
-      createdTime: '',
-      updatedBy: 0,
-      updatedName: '',
-      updatedTime: '',
-      formConfig: {},
-      formData: {},
-      processTask: [],
-      businessCode: ''
-    },
-    approvalForm: {
-      approveOpinion: '',
-      approveResult: 'pass'
-    }
-  })
-  const onClickRight = () => {
-    router.go(-1)
-  }
-  // 审核方式翻译
-  const getNodeModel = (type: string, model: string) => {
-    if (type === 'start') return ''
-    if (model === '10') {
-      return '会签'
-    } else if (model === '20') {
-      return '或签'
-    }
-  }
-  // 详情工作流列表
-  const getFlowInstance = async () => {
-    const [err, res]: ToResponse = await to(flowApi.getFlowInstance({ id: state.form.id, businessCode: state.form.businessCode, defCode: state.form.defCode }))
-    if (err) return
-    const arr = res?.data?.nodes || []
-    apprList.value = arr
+const FlowTable = defineAsyncComponent(() => import('/@/components/FlowTable.vue'))
+const InstrumentAppointment = defineAsyncComponent(() => import('/@/view/todo/component/instrument_appointment.vue'))
+const PlatPlatformAppoint = defineAsyncComponent(() => import('/@/view/todo/component/plat_platform_appoint.vue'))
+const platServiceCommission = defineAsyncComponent(() => import('/@/view/todo/component/plat_service_commission.vue'))
+const PlatCageApplications = defineAsyncComponent(() => import('/@/view/todo/component/plat_cage_applications.vue'))
+const PlatCageReleaseApplications = defineAsyncComponent(() => import('/@/view/todo/component/PlatCageReleaseApplications.vue'))
+const PlatPlatformRenewal = defineAsyncComponent(() => import('/@/view/todo/component/plat_cage_release_applications.vue'))
+const PlatAnimalTakewayApplications = defineAsyncComponent(() => import('/@/view/todo/component/plat_animal_takeway_applications.vue'))
+const PlatPlatformOutRoomAppoint = defineAsyncComponent(() => import('/@/view/todo/component/plat_platform_out_room_appoint.vue'))
+const PlatformAnimalAppointmentEthics = defineAsyncComponent(() => import('/@/view/todo/component/platform_animal_appointment_ethics.vue'))
+const executionApi = useExecutionApi()
+const flowApi = useFlowApi()
+const router = useRouter()
+const route = useRoute()
+const apprList = ref<any[]>([])
+const formRef = ref()
+const state = reactive({
+  type: 'approval',
+  form: {
+    id: 0,
+    defId: 0,
+    defCode: '',
+    defName: '',
+    instTitle: '',
+    nodeId: '',
+    candidate: '',
+    taskId: 0,
+    startTime: '',
+    endTime: '',
+    duration: 0,
+    startUserId: 0,
+    startUserName: '',
+    isFinish: '',
+    platformId: 0,
+    remark: '',
+    createdBy: 0,
+    createdName: '',
+    createdTime: '',
+    updatedBy: 0,
+    updatedName: '',
+    updatedTime: '',
+    formConfig: {},
+    formData: {},
+    processTask: [],
+    businessCode: ''
+  },
+  approvalForm: {
+    approveOpinion: '',
+    approveResult: 'pass'
   }
-  const init = async (id: number) => {
-    const [err, res]: ToResponse = await to(executionApi.getInstanceById({ id }))
-    if (err) return
-    if (res?.data) {
-      state.form = res.data
-    }
+})
+const onClickRight = () => {
+  router.go(-1)
+}
+// 审核方式翻译
+const getNodeModel = (type: string, model: string) => {
+  if (type === 'start') return ''
+  if (model === '10') {
+    return '会签'
+  } else if (model === '20') {
+    return '或签'
   }
-  const onRouterPush = (val: string, params?: any) => {
-    router.push({
-      path: val,
-      query: { ...params }
-    })
+}
+// 详情工作流列表
+const getFlowInstance = async () => {
+  const [err, res]: ToResponse = await to(flowApi.getFlowInstance({ id: state.form.id, businessCode: state.form.businessCode, defCode: state.form.defCode }))
+  if (err) return
+  const arr = res?.data?.nodes || []
+  apprList.value = arr
+}
+const init = async (id: number) => {
+  const [err, res]: ToResponse = await to(executionApi.getInstanceById({ id }))
+  if (err) return
+  if (res?.data) {
+    state.form = res.data
   }
-  const onSave = async () => {
-    const [errValid] = await to(formRef.value.validate())
-    if (errValid) return
-    const params = {
-      taskId: state.form.taskId,
-      result: state.approvalForm.approveResult,
-      opinion: state.approvalForm.approveOpinion
-    }
-    const [err]: ToResponse = await to(executionApi.approve(params))
-    if (err) return
-    showNotify({
-      type: 'success',
-      message: '审批成功'
-    })
-    router.push({
-      path: '/todo'
-    })
+}
+const onRouterPush = (val: string, params?: any) => {
+  router.push({
+    path: val,
+    query: { ...params }
+  })
+}
+const onSave = async () => {
+  const [errValid] = await to(formRef.value.validate())
+  if (errValid) return
+  const params = {
+    taskId: state.form.taskId,
+    result: state.approvalForm.approveResult,
+    opinion: state.approvalForm.approveOpinion
   }
-  onMounted(() => {
-    const id = route.query.id ? +route.query.id : 0
-    state.type = route.query.type.toString()
-    init(id)
+  const [err]: ToResponse = await to(executionApi.approve(params))
+  if (err) return
+  showNotify({
+    type: 'success',
+    message: '审批成功'
   })
+  router.push({
+    path: '/todo'
+  })
+}
+onMounted(() => {
+  const id = route.query.id ? +route.query.id : 0
+  state.type = route.query.type.toString()
+  init(id)
+})
 </script>
 
 <style lang="scss" scoped>
-  .app-container {
-    header {
-      padding: 14px;
-      background-color: #f9ffff;
-      border-radius: 4px;
-      margin-top: 10px;
-    }
-    > h4 {
-      margin: 10px 0;
+.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;
-      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;
-    }
-    :deep(.cell-title) {
-      flex: 0 0 80px;
-    }
-    .van-action-bar {
-      z-index: 2;
+      margin-right: 4px;
+      vertical-align: top;
     }
   }
+
+  :deep(.flow-cell) {
+    margin-left: 22px;
+    display: flex;
+    justify-content: space-between;
+    color: #333;
+  }
+
+  :deep(.cell-title) {
+    flex: 0 0 80px;
+  }
+
+  .van-action-bar {
+    z-index: 2;
+  }
+}
 </style>

+ 21 - 16
src/view/todo/index.vue

@@ -83,38 +83,43 @@ const state = reactive({
 })
 const checkedList = ref([])
 
-const onClickRight = () => {
-  router.go(-1)
-}
-const changeType = (str: string) => {
+const changeType = () => {
   state.list = []
   checkedList.value = []
   state.queryParams.pageNum = 1
+  state.finished = false  // 重点
+  state.loading = false   // 重点:必须重置
   onLoad()
 }
+
 const onLoad = async () => {
-  let [err, res]: ToResponse = [null, undefined]
-  const tabs = state.status
-  if (tabs == 'start') {
-    ;[err, res] = await to(executionApi.getOwStartList(state.queryParams))
-  } else if (tabs == 'approval') {
-    ;[err, res] = await to(executionApi.getOwApproveList(state.queryParams))
-  } else if (tabs == 'history') {
-    // 审批历史
-    ;[err, res] = await to(executionApi.getOwnApprovedList(state.queryParams))
+  state.loading = true  // load 开始时设为 true
+
+  let [err, res] = [null, undefined]
+  if (state.status == 'start') {
+    [err, res] = await to(executionApi.getOwStartList(state.queryParams))
+  } else if (state.status == 'approval') {
+    [err, res] = await to(executionApi.getOwApproveList(state.queryParams))
+  } else {
+    [err, res] = await to(executionApi.getOwnApprovedList(state.queryParams))
   }
+
   if (err) return
+
   const list = res?.data?.list || []
-  for (const item of list) {
-    state.list.push(item)
-  }
+  state.list.push(...list)
+
   state.loading = false
   state.queryParams.pageNum++
+
   if (list.length < state.queryParams.pageSize) {
     state.finished = true
   }
+
+  await nextTick()
 }
 
+
 const batchApprovalSuccessCallback = () => {
   state.list = []
   checkedList.value = []