Procházet zdrojové kódy

fix:入室样式调整

liuzhenlin před 1 měsícem
rodič
revize
772ef1266f

+ 16 - 14
src/view/animal/applicationRemoval/components/addEdit.vue

@@ -18,33 +18,33 @@
 
             <h4 class="mb20 mt20">转出情况</h4>
             <van-cell-group>
-              <van-field v-model="state.form.categoryName" label="动物类别" placeholder="请选择" readonly is-link required
-                @click="showCategoryPicker = true" :rules="rules.categoryId" />
-              <van-field v-model="state.form.variety" label="品种品系" placeholder="请输入品种品系" required
+              <van-field v-model="state.form.categoryName" label="动物类别" placeholder="请选择" readonly :is-link="!isReadOnly" required
+                @click="!isReadOnly && (showCategoryPicker = true)" :rules="rules.categoryId" />
+              <van-field v-model="state.form.variety" label="品种品系" placeholder="请输入品种品系" :readonly="isReadOnly" required
                 :rules="rules.variety" />
-              <van-field v-model="state.form.takeawayDate" label="转出日期" placeholder="请选择时间" readonly is-link required
-                @click="showDatePicker = true" :rules="rules.takeawayDate" />
-              <van-field v-model="state.form.takeawayAddress" label="送往地点" placeholder="请输入" required
+              <van-field v-model="state.form.takeawayDate" label="转出日期" placeholder="请选择时间" readonly :is-link="!isReadOnly" required
+                @click="!isReadOnly && (showDatePicker = true)" :rules="rules.takeawayDate" />
+              <van-field v-model="state.form.takeawayAddress" label="送往地点" placeholder="请输入" :readonly="isReadOnly" required
                 :rules="rules.takeawayAddress" />
-              <van-field v-model="state.form.takeawayReason" label="带出原因" placeholder="请输入" required
+              <van-field v-model="state.form.takeawayReason" label="带出原因" placeholder="请输入" :readonly="isReadOnly" required
                 :rules="rules.takeawayReason" />
-              <van-field v-model="state.form.takeawayTransport" label="运输方式" placeholder="请输入" required
+              <van-field v-model="state.form.takeawayTransport" label="运输方式" placeholder="请输入" :readonly="isReadOnly" required
                 :rules="rules.takeawayTransport" />
-              <van-field v-model="state.form.accessCardNumber" label="门禁卡序列号" placeholder="请输入" />
-              <van-field v-model="state.form.takeawayMaleNumber" label="雄性" placeholder="雄性数量" type="digit">
+              <van-field v-model="state.form.accessCardNumber" label="门禁卡序列号" placeholder="请输入" :readonly="isReadOnly" />
+              <van-field v-model="state.form.takeawayMaleNumber" label="雄性" placeholder="雄性数量" type="digit" :readonly="isReadOnly">
                 <template #button>
-                  <van-stepper v-model="state.form.takeawayMaleNumber" :min="0" integer />
+                  <van-stepper v-model="state.form.takeawayMaleNumber" :min="0" integer :disabled="isReadOnly" />
                 </template>
               </van-field>
-              <van-field v-model="state.form.takewayFemaleNumber" label="雌性" placeholder="雌性数量" type="digit">
+              <van-field v-model="state.form.takewayFemaleNumber" label="雌性" placeholder="雌性数量" type="digit" :readonly="isReadOnly">
                 <template #button>
-                  <van-stepper v-model="state.form.takewayFemaleNumber" :min="0" integer />
+                  <van-stepper v-model="state.form.takewayFemaleNumber" :min="0" integer :disabled="isReadOnly" />
                 </template>
               </van-field>
             </van-cell-group>
 
             <div class="mt30 mb30 checkbox-wrapper">
-              <van-checkbox v-model="safePromiseStatus">
+              <van-checkbox v-model="safePromiseStatus" :disabled="isReadOnly">
                 本人已阅读并同意
                 <span class="link-text" @click.stop="handleReadNotice">
                   《实验动物带离须知》
@@ -125,6 +125,8 @@ const showCategoryPicker = ref<boolean>(false)
 const showDatePicker = ref<boolean>(false)
 const selectedDate = ref<Date | null>(null)
 
+const isReadOnly = computed(() => state.dialog.type === 'detail')
+
 const rules = {
   categoryId: [{ required: true, message: '动物类别不能为空' }],
   variety: [{ required: true, message: '品种品系不能为空' }],

+ 160 - 185
src/view/animal/applicationRemoval/components/dieModal.vue

@@ -1,210 +1,185 @@
 <template>
-  <div class="application-dialog-container">
-    <el-dialog
-      :title="state.dialog.title"
-      @close="onCancel"
-      :close-on-click-modal="false"
-      v-model="state.dialog.isShowDialog"
-      width="90%"
-    >
-      <el-form
-        ref="expertDialogFormRef"
-        :model="state.form"
-        :rules="rules"
-        size="default"
-        label-width="140px"
-        label-position="top"
-      >
-        <el-row class="mb20">
-          <el-col :span="24">
-            <el-form-item
-              label="淘汰日期"
-              prop="dieTime"
-            >
-              <el-date-picker
-                v-model="state.form.dieTime"
-                type="date"
-                placeholder="请选择时间"
-                clearable
-                value-format="YYYY-MM-DD"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row>
-          <el-col :span="24">
-            <el-form-item
-              label="淘汰原因"
-              prop="dieReason"
-            >
-              <el-input
-                v-model="state.form.dieReason"
-                placeholder="请输入"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row>
-          <el-col :span="24">
-            <el-form-item
-              label="雄性"
-              prop="returnMaleNumber"
-            >
-              <el-input-number
-                style="width: 100%"
-                placeholder="雄性数量"
-                v-model="state.form.returnMaleNumber"
-                :min="0"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row>
-          <el-col :span="24">
-            <el-form-item
-              label="雌性"
-              prop="returnFemaleNumber"
-            >
-              <el-input-number
-                style="width: 100%"
-                placeholder="雌性数量"
-                v-model="state.form.returnFemaleNumber"
-                :min="0"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button
-            type="info"
-            @click="onCancel"
-            size="default"
-          >
-            取 消
-          </el-button>
-          <el-button
-            color="#2c78ff"
-            @click="onSubmit()"
-            size="default"
-          >
+  <div class="drawer-dialog-container">
+    <van-popup v-model:show="state.dialog.isShowDialog" position="right"
+      :style="{ width: '100vw', maxWidth: '460px', height: '100vh' }" :closeable="true" @close="onCancel"
+      :close-on-click-overlay="false">
+      <div class="drawer-wrapper">
+        <header class="drawer-header">
+          <h3>{{ state.dialog.title }}</h3>
+        </header>
+        <div class="drawer-body">
+          <van-form ref="expertDialogFormRef">
+            <van-cell-group inset>
+              <van-field v-model="state.form.dieTime" label="淘汰日期" placeholder="请选择时间" readonly is-link
+                @click="showDieDatePicker = true" />
+
+              <van-field v-model="state.form.dieReason" label="淘汰原因" placeholder="请输入" type="textarea" rows="3" />
+
+              <van-field label="雄性">
+                <template #input>
+                  <van-stepper v-model="state.form.returnMaleNumber" integer :min="0" />
+                </template>
+              </van-field>
+
+              <van-field label="雌性">
+                <template #input>
+                  <van-stepper v-model="state.form.returnFemaleNumber" integer :min="0" />
+                </template>
+              </van-field>
+            </van-cell-group>
+          </van-form>
+        </div>
+        <footer class="drawer-footer">
+          <van-button block type="primary" @click="onSubmit">
             提交
-          </el-button>
-        </span>
-      </template>
-    </el-dialog>
+          </van-button>
+        </footer>
+      </div>
+    </van-popup>
+
+    <van-popup v-model:show="showDieDatePicker" position="bottom" :style="{ height: '80vh' }" round>
+      <van-calendar v-model:show="showDieDatePicker" @confirm="onDieDateConfirm" />
+    </van-popup>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { reactive, ref } from 'vue'
-  import to from 'await-to-js'
-  import { ElMessage } from 'element-plus'
-  import type { FormRules, FormInstance } from 'element-plus'
-
-  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
-  import { TakeawayList, ApplyLeaveDiePayload } from '/@/constants/pageConstants'
-  import { filterFields } from '/@/utils/func'
-
-  const emit = defineEmits(['refresh'])
-
-  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
-
-  const expertDialogFormRef = ref<FormInstance>()
-
-  const rules = reactive<FormRules<ApplyLeaveDiePayload>>({
-    dieTime: [{ required: true, message: '请选择日期', trigger: 'blur' }],
-    returnMaleNumber: [{ required: true, message: '请输入雄性数量', trigger: 'blur' }],
-    returnFemaleNumber: [{ required: true, message: '请输入雌性数量', trigger: 'blur' }],
-    dieReason: [{ required: true, message: '请输入死亡原因', trigger: 'blur' }],
-  })
-
-  const defaultFormFields: ApplyLeaveDiePayload = {
-    takeawayId: 0,
-    returnFemaleNumber: 0,
-    returnMaleNumber: 0,
-    takeawayType: '20',
-    dieTime: '',
-    dieReason: '',
+import { reactive, ref } from 'vue'
+import to from 'await-to-js'
+import { showNotify, showToast } from 'vant'
+import type { FormInstance } from 'vant/es'
+import dayjs from 'dayjs'
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import { TakeawayList, ApplyLeaveDiePayload } from '/@/constants/pageConstants'
+import { filterFields } from '/@/utils/func'
+
+const emit = defineEmits(['refresh'])
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+const expertDialogFormRef = ref<FormInstance>()
+const showDieDatePicker = ref(false)
+
+const createDefaultForm = (): ApplyLeaveDiePayload => ({
+  takeawayId: 0,
+  returnFemaleNumber: 0,
+  returnMaleNumber: 0,
+  takeawayType: '20',
+  dieTime: '',
+  dieReason: '',
+})
+
+const state = reactive<{
+  form: ApplyLeaveDiePayload
+  dialog: { isShowDialog: boolean; title: string; submitTxt: string }
+}>({
+  form: createDefaultForm(),
+  dialog: {
+    isShowDialog: false,
+    title: '',
+    submitTxt: '',
+  },
+})
+
+const validateForm = () => {
+  if (!state.form.dieTime) {
+    showNotify({ type: 'warning', message: '请选择淘汰日期' })
+    return false
   }
 
-  const state = reactive<{
-    form: ApplyLeaveDiePayload
-    safePromise: boolean
-    safeRead: boolean
-    dialog: { isShowDialog: boolean; title: string; submitTxt: string }
-  }>({
-    form: defaultFormFields,
-    safePromise: false,
-    safeRead: false,
-    dialog: {
-      isShowDialog: false,
-      title: '',
-      submitTxt: '',
-    },
-  })
-
-  const openDialog = async (sourceData: TakeawayList) => {
-    state.form.takeawayId = sourceData.id
-    state.dialog.title = '淘汰动物记录上报'
-    state.dialog.isShowDialog = true
+  if (!state.form.dieReason) {
+    showNotify({ type: 'warning', message: '请输入淘汰原因' })
+    return false
   }
 
-  const closeDialog = () => {
-    expertDialogFormRef.value.resetFields()
-    state.form = defaultFormFields
-    state.dialog.isShowDialog = false
+  if (!state.form.returnMaleNumber && !state.form.returnFemaleNumber) {
+    showNotify({ type: 'warning', message: '请至少填写一项淘汰数量' })
+    return false
   }
 
-  const onCancel = () => {
-    closeDialog()
+  return true
+}
+
+const openDialog = async (sourceData: TakeawayList) => {
+  state.form = {
+    ...createDefaultForm(),
+    takeawayId: sourceData.id,
   }
+  state.dialog.title = '淘汰动物记录上报'
+  state.dialog.isShowDialog = true
+}
+
+const closeDialog = () => {
+  expertDialogFormRef.value?.resetValidation?.()
+  state.form = createDefaultForm()
+  state.dialog.isShowDialog = false
+  showDieDatePicker.value = false
+}
+
+const onCancel = () => {
+  closeDialog()
+}
+
+const onSubmit = async () => {
+  if (!validateForm()) return
+
+  const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
+  const payload = filterFields({
+    ...state.form,
+    dieTime: dayjs(state.form.dieTime).format('YYYY-MM-DD'),
+    returnMaleNumber: Number(state.form.returnMaleNumber) || 0,
+    returnFemaleNumber: Number(state.form.returnFemaleNumber) || 0,
+  })
 
-  const onSubmit = async () => {
-    expertDialogFormRef.value.validate(async (valid: boolean) => {
-      if (!valid) return
+  const [err]: ToResponse = await to(post(payload))
 
-      const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
-      const [err]: ToResponse = await to(post(filterFields(state.form)))
+  if (err) return
+  showToast({ type: 'success', message: '操作成功' })
+  closeDialog()
+  emit('refresh')
+}
 
-      if (err) return
-      ElMessage.success('操作成功')
-      closeDialog()
-      emit('refresh')
-    })
-  }
+const onDieDateConfirm = (date: Date | Date[]) => {
+  const selectedDate = Array.isArray(date) ? date[0] : date
+  state.form.dieTime = dayjs(selectedDate).format('YYYY-MM-DD')
+  showDieDatePicker.value = false
+}
 
-  defineExpose({
-    openDialog,
-  })
+defineExpose({
+  openDialog,
+})
 </script>
 <style lang="scss" scoped>
-  .application-dialog-container {
-    .el-select {
-      width: 100%;
-    }
-  }
-  h4 {
-    font-size: 18px;
+.drawer-dialog-container {
+  .drawer-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
   }
-  ul {
-    padding-left: 20px;
-  }
-  .text {
-    p {
-      text-indent: 2em;
+
+  .drawer-header {
+    padding: 16px;
+    border-bottom: 1px solid #f0f0f0;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      text-align: center;
     }
   }
-  .el-upload + .el-button {
-    vertical-align: top;
+
+  .drawer-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 16px 12px 80px;
   }
-  :deep(.el-checkbox) {
-    white-space: pre-wrap;
+
+  .drawer-footer {
+    padding: 16px;
+    border-top: 1px solid #f0f0f0;
+    background-color: #fff;
   }
+}
 </style>

+ 198 - 288
src/view/animal/applicationRemoval/components/turnBack.vue

@@ -1,313 +1,223 @@
 <template>
-  <div class="application-dialog-container">
-    <el-dialog
-      :title="state.dialog.title"
-      @close="onCancel"
-      :close-on-click-modal="false"
-      v-model="state.dialog.isShowDialog"
-      width="90%"
-    >
-      <el-form
-        ref="expertDialogFormRef"
-        :model="state.form"
-        :rules="rules"
-        size="default"
-        label-width="140px"
-        label-position="top"
-      >
-        <el-row
-          class="mb20"
-          :gutter="20"
-        >
-          <el-col :span="24">
-            <el-form-item label="是否转回">
-              <el-radio-group
-                :disabled="state.dialog.type === 'view'"
-                v-model="isReturn"
-              >
-                <el-radio
-                  :label="1"
-                  size="large"
-                >
-                  是
-                </el-radio>
-                <el-radio
-                  :label="0"
-                  size="large"
-                >
-                  否
-                </el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <!-- <el-row class="mb20">
-          <el-col :span="24">
-            <el-form-item
-              label="品种品系"
-              prop="categoryId"
-            >
-              <el-select
-                v-model="state.form.categoryId"
-                placeholder="请选择"
-              >
-                <el-option
-                  v-for="item in animalTypeList"
-                  :key="item.id"
-                  :label="item.name"
-                  :value="item.id"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row> -->
-
-        <el-row v-if="isReturn === 1">
-          <el-col :span="24">
-            <el-form-item
-              label="转回日期"
-              prop="returnDate"
-            >
-              <el-date-picker
-                v-model="state.form.returnDate"
-                type="date"
-                placeholder="请选择时间"
-                clearable
-                value-format="YYYY-MM-DD"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row
-          class="mb20"
-          v-if="isReturn === 0"
-        >
-          <el-col :span="24">
-            <el-form-item
-              label="未返回动物情况说明"
-              prop="notReturnReason"
-            >
-              <el-input
-                v-model="state.form.notReturnReason"
-                placeholder="请输入"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row v-if="isReturn === 1">
-          <el-col :span="24">
-            <el-form-item
-              label="运输方式"
-              prop="returnTransport"
-            >
-              <el-input
-                v-model="state.form.returnTransport"
-                placeholder="请输入"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row
-          class="mb20"
-          v-if="isReturn === 1"
-        >
-          <el-col :span="24">
-            <el-form-item
-              label="雄性"
-              prop="returnMaleNumber"
-            >
-              <el-input-number
-                style="width: 100%"
-                placeholder="雄性数量"
-                v-model="state.form.returnMaleNumber"
-                :min="0"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row v-if="isReturn === 1">
-          <el-col :span="24">
-            <el-form-item
-              label="雌性"
-              prop="returnFemaleNumber"
-            >
-              <el-input-number
-                style="width: 100%"
-                placeholder="雌性数量"
-                v-model="state.form.returnFemaleNumber"
-                :min="0"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row
-          class="mb20"
-          :gutter="20"
-        >
-          <el-col :span="12">
-            <el-form-item
-              label="门禁卡是否归还"
-              prop="accessCardReturn"
-            >
-              <el-radio-group v-model="state.form.accessCardReturn">
-                <el-radio :label="1">是</el-radio>
-                <el-radio :label="0">否</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button
-            type="info"
-            @click="onCancel"
-            size="default"
-          >
-            取 消
-          </el-button>
-          <el-button
-            color="#2c78ff"
-            @click="onSubmit()"
-            size="default"
-          >
+  <div class="drawer-dialog-container">
+    <van-popup v-model:show="state.dialog.isShowDialog" position="right" :style="{ width: '85vw', maxWidth: '460px', height: '100vh' }"
+      :closeable="true" round @close="onCancel" :close-on-click-overlay="false">
+      <div class="drawer-wrapper">
+        <header class="drawer-header">
+          <h3>{{ state.dialog.title }}</h3>
+        </header>
+        <div class="drawer-body">
+          <van-form ref="expertDialogFormRef" @submit="onSubmit">
+            <van-cell-group inset>
+              <van-field label="是否转回" readonly>
+                <template #input>
+                  <van-radio-group v-model="isReturn" direction="horizontal">
+                    <van-radio :name="1">是</van-radio>
+                    <van-radio :name="0">否</van-radio>
+                  </van-radio-group>
+                </template>
+              </van-field>
+
+              <van-field v-if="isReturn === 1" v-model="state.form.returnDate" label="转回日期" placeholder="请选择时间" readonly
+                is-link @click="showReturnDatePicker = true" />
+
+              <van-field v-if="isReturn === 1" v-model="state.form.returnTransport" label="运输方式" placeholder="请输入" />
+
+              <van-field v-if="isReturn === 1" label="雄性">
+                <template #input>
+                  <van-stepper v-model="state.form.returnMaleNumber" integer :min="0" />
+                </template>
+              </van-field>
+
+              <van-field v-if="isReturn === 1" label="雌性">
+                <template #input>
+                  <van-stepper v-model="state.form.returnFemaleNumber" integer :min="0" />
+                </template>
+              </van-field>
+
+              <van-field v-if="isReturn === 0" v-model="state.form.notReturnReason" label="未返回动物情况说明"
+                placeholder="请输入" type="textarea" rows="3" />
+
+              <van-field label="门禁卡是否归还">
+                <template #input>
+                  <van-radio-group v-model="state.form.accessCardReturn" direction="horizontal">
+                    <van-radio :name="1">是</van-radio>
+                    <van-radio :name="0">否</van-radio>
+                  </van-radio-group>
+                </template>
+              </van-field>
+            </van-cell-group>
+          </van-form>
+        </div>
+        <footer class="drawer-footer">
+          <van-button block type="primary" @click="onSubmit">
             提交
-          </el-button>
-        </span>
-      </template>
-    </el-dialog>
+          </van-button>
+        </footer>
+      </div>
+    </van-popup>
+
+    <van-popup v-model:show="showReturnDatePicker" position="bottom" :style="{ height: '80vh' }" round>
+      <van-calendar v-model:show="showReturnDatePicker" @confirm="onReturnDateConfirm" />
+    </van-popup>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { reactive, ref } from 'vue'
-  import to from 'await-to-js'
-  import { ElMessage } from 'element-plus'
-  import type { FormRules, FormInstance } from 'element-plus'
-
-  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
-  import { TurnBackPayload, TakeawayList } from '/@/constants/pageConstants'
-  import { filterFields } from '/@/utils/func'
-
-  const emit = defineEmits(['refresh'])
-
-  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
-
-  const expertDialogFormRef = ref<FormInstance>()
-  const animalTypeList = ref<{ id: string; name: string }[]>([])
-  const isReturn = ref<number>(1)
-
-  const rules = reactive<FormRules<TurnBackPayload>>({
-    // categoryId: [{ required: true, message: '请选择动物种类', trigger: 'blur' }],
-    returnDate: [{ required: true, message: '请选择转出日期', trigger: 'blur' }],
-
-    notReturnReason: [{ required: true, message: '请输入未返回动物情况说明', trigger: 'blur' }],
-  })
-
-  const defaultFormFields: TurnBackPayload = {
-    takeawayId: 0,
-    // categoryId: null,
-    returnDate: '',
-    returnTransport: '',
-    accessCardReturn: 1,
-    returnFemaleNumber: 0,
-    returnMaleNumber: 0,
-    notReturnReason: '',
-    takeawayType: '10',
-  }
-
-  const state = reactive<{
-    form: TurnBackPayload
-    dialog: { isShowDialog: boolean; type: string; title: string; submitTxt: string }
-  }>({
-    form: defaultFormFields,
-    dialog: {
-      isShowDialog: false,
-      type: '',
-      title: '',
-      submitTxt: '',
-    },
-  })
+import { reactive, ref } from 'vue'
+import to from 'await-to-js'
+import { showNotify, showToast } from 'vant'
+import type { FormInstance } from 'vant/es'
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import { TurnBackPayload, TakeawayList } from '/@/constants/pageConstants'
+import { filterFields } from '/@/utils/func'
+import dayjs from 'dayjs'
+
+const emit = defineEmits(['refresh'])
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+const expertDialogFormRef = ref<FormInstance>()
+const isReturn = ref<number>(1)
+const showReturnDatePicker = ref(false)
+
+const createDefaultForm = (): TurnBackPayload => ({
+  takeawayId: 0,
+  returnDate: '',
+  returnTransport: '',
+  accessCardReturn: 1,
+  returnFemaleNumber: 0,
+  returnMaleNumber: 0,
+  notReturnReason: '',
+  takeawayType: '10',
+})
+
+const state = reactive<{
+  form: TurnBackPayload
+  dialog: { isShowDialog: boolean; type: string; title: string; submitTxt: string }
+}>({
+  form: createDefaultForm(),
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+})
+
+const validateForm = () => {
+  if (isReturn.value === 1) {
+    if (!state.form.returnDate) {
+      showNotify({ type: 'warning', message: '请选择转回日期' })
+      return false
+    }
 
-  const getDicts = async () => {
-    const [_, res]: ToResponse = await to(platAnimalCageApplicationApi.getAnimalTypeList({}))
+    if (!state.form.returnTransport) {
+      showNotify({ type: 'warning', message: '请输入运输方式' })
+      return false
+    }
 
-    if (res) {
-      animalTypeList.value = res.data
+    if (!state.form.returnMaleNumber && !state.form.returnFemaleNumber) {
+      showNotify({ type: 'warning', message: '请添加雄性或雌性数量' })
+      return false
+    }
+  } else {
+    if (!state.form.notReturnReason) {
+      showNotify({ type: 'warning', message: '请输入未返回动物情况说明' })
+      return false
     }
+    state.form.returnDate = ''
+    state.form.returnTransport = ''
+    state.form.returnMaleNumber = 0
+    state.form.returnFemaleNumber = 0
   }
 
-  const openDialog = async (sourceData: TakeawayList) => {
-    await getDicts()
-    state.dialog.title = '登记转回'
-    state.form.takeawayId = sourceData.id
-    // state.form.categoryId = animalTypeList.value[0].id
-    state.dialog.isShowDialog = true
-  }
+  return true
+}
 
-  const closeDialog = () => {
-    expertDialogFormRef.value.resetFields()
-    state.form = defaultFormFields
-    state.dialog.isShowDialog = false
+const openDialog = async (sourceData: TakeawayList) => {
+  state.dialog.title = '登记转回'
+  state.form = {
+    ...createDefaultForm(),
+    takeawayId: sourceData.id,
   }
+  isReturn.value = 1
+  state.dialog.isShowDialog = true
+}
+
+const closeDialog = () => {
+  expertDialogFormRef.value?.resetValidation?.()
+  state.form = createDefaultForm()
+  state.dialog.isShowDialog = false
+  isReturn.value = 1
+  showReturnDatePicker.value = false
+}
+
+const onCancel = () => {
+  closeDialog()
+}
+
+const onSubmit = async () => {
+  if (!validateForm()) return
+
+  const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
+  const payload = filterFields({
+    ...state.form,
+    returnDate: state.form.returnDate ? dayjs(state.form.returnDate).format('YYYY-MM-DD') : '',
+    returnMaleNumber: Number(state.form.returnMaleNumber) || 0,
+    returnFemaleNumber: Number(state.form.returnFemaleNumber) || 0,
+    takeawayType: isReturn.value === 1 ? '10' : '11',
+  })
 
-  const onCancel = () => {
-    closeDialog()
-  }
-
-  const onSubmit = async () => {
-    expertDialogFormRef.value.validate(async (valid: boolean) => {
-      if (!valid) return
-
-      if (isReturn.value === 1) {
-        if (!state.form.returnMaleNumber && !state.form.returnFemaleNumber) {
-          ElMessage.error('请添加雄性或雌性数量!')
-          return
-        }
-      }
+  const [err]: ToResponse = await to(post(payload))
 
-      const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
-      const [err]: ToResponse = await to(post(filterFields(state.form)))
+  if (err) return
+  showToast({ type: 'success', message: '操作成功' })
+  closeDialog()
+  emit('refresh')
+}
 
-      if (err) return
-      ElMessage.success('操作成功')
-      closeDialog()
-      emit('refresh')
-    })
-  }
+const onReturnDateConfirm = (date: Date | Date[]) => {
+  const selectedDate = Array.isArray(date) ? date[0] : date
+  state.form.returnDate = dayjs(selectedDate).format('YYYY-MM-DD')
+  showReturnDatePicker.value = false
+}
 
-  defineExpose({
-    openDialog,
-  })
+defineExpose({
+  openDialog,
+})
 </script>
 <style lang="scss" scoped>
-  .application-dialog-container {
-    .el-select {
-      width: 100%;
-    }
-  }
-  h4 {
-    font-size: 18px;
-  }
-  ul {
-    padding-left: 20px;
+.drawer-dialog-container {
+  .drawer-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
   }
-  .text {
-    p {
-      text-indent: 2em;
+
+  .drawer-header {
+    padding: 16px;
+    border-bottom: 1px solid #f0f0f0;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      text-align: center;
     }
   }
-  .el-upload + .el-button {
-    vertical-align: top;
+
+  .drawer-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 16px 12px 80px;
   }
-  :deep(.el-checkbox) {
-    white-space: pre-wrap;
+
+  .drawer-footer {
+    padding: 16px;
+    border-top: 1px solid #f0f0f0;
+    background-color: #fff;
   }
+}
 </style>

+ 736 - 764
src/view/entry/components/allocate.vue

@@ -1,56 +1,21 @@
 <template>
   <div class="allocate-popup-container">
-    <van-popup
-      class="allocate-popup"
-      v-model:show="state.dialog.isShowDialog"
-      position="right"
-      :style="{ width: '100%' }"
-      closeable
-      :close-on-popstate="false"
-      @close="onCancel"
-    >
+    <van-popup class="allocate-popup" v-model:show="state.dialog.isShowDialog" position="right"
+      :style="{ width: '100%' }" closeable :close-on-popstate="false" @close="onCancel">
       <header class="popup-header">
         <div class="popup-title">资源分配</div>
         <div class="popup-controls">
-          <div
-            class="group-selector"
-            @click="groupPickerVisible = true"
-          >
+          <div class="group-selector" @click="groupPickerVisible = true">
             <span class="label">资源组</span>
-            <van-field
-              :model-value="groupLabel"
-              readonly
-              is-link
-              input-align="right"
-              placeholder="全部资源组"
-              border
-            />
+            <van-field :model-value="groupLabel" readonly is-link input-align="right" placeholder="全部资源组" border />
           </div>
-          <van-popup
-            v-model:show="groupPickerVisible"
-            round
-            position="bottom"
-          >
-            <van-picker
-              title="选择资源组"
-              show-toolbar
-              :columns="groupOptions"
-              @cancel="groupPickerVisible = false"
-              @confirm="onGroupConfirm"
-            />
+          <van-popup v-model:show="groupPickerVisible" round position="bottom">
+            <van-picker title="选择资源组" show-toolbar :columns="groupOptions" @cancel="groupPickerVisible = false"
+              @confirm="onGroupConfirm" />
           </van-popup>
-          <van-radio-group
-            v-model="resClass"
-            direction="horizontal"
-            class="type-radio-group"
-            @change="filterOutList"
-          >
+          <van-radio-group v-model="resClass" direction="horizontal" class="type-radio-group" @change="filterOutList">
             <van-radio name="">全部</van-radio>
-            <van-radio
-              v-for="item in cellType"
-              :key="item.dictValue"
-              :name="item.dictValue"
-            >
+            <van-radio v-for="item in cellType" :key="item.dictValue" :name="item.dictValue">
               {{ item.dictLabel }}
             </van-radio>
           </van-radio-group>
@@ -59,18 +24,10 @@
       <div class="popup-body">
         <section class="card confirm-card">
           <div class="info-banner warning">待分配队列(已确认{{ comfirmTotal }}人)</div>
-          <van-empty
-            v-if="!state.confirmList.length"
-            image="default"
-            description="暂无队列数据"
-          />
+          <van-empty v-if="!state.confirmList.length" image="default" description="暂无队列数据" />
           <ul v-else>
-            <li
-              v-for="(item, index) in state.confirmList"
-              :key="index"
-              :class="{ active: index === state.active }"
-              @click="state.active = index"
-            >
+            <li v-for="(item, index) in state.confirmList" :key="index" :class="{ active: index === state.active }"
+              @click="state.active = index">
               <div class="text">
                 <p>
                   分配房间:
@@ -89,19 +46,10 @@
                 </p>
               </div>
               <div class="btns">
-                <button
-                  v-if="item.resId"
-                  class="link-btn danger"
-                  type="button"
-                  @click.stop="cancelAllocate(item)"
-                >
+                <button v-if="item.resId" class="link-btn danger" type="button" @click.stop="cancelAllocate(item)">
                   取消
                 </button>
-                <button
-                  class="link-btn"
-                  type="button"
-                  @click.stop="onDetail(item)"
-                >
+                <button class="link-btn" type="button" @click.stop="onDetail(item)">
                   查看
                 </button>
               </div>
@@ -110,28 +58,13 @@
         </section>
         <section class="card resource-card">
           <div class="info-banner warning">资源列表(共{{ state.resourceList.length }}个)</div>
-          <van-empty
-            v-if="!state.resourceList.length"
-            image="search"
-            description="暂无资源数据"
-          />
-          <div
-            v-else
-            class="resource-grid"
-          >
-            <div
-              v-for="item in state.resourceList"
-              :key="item.id"
-              class="resource-item"
-              :class="{ active: curSelectedResourceId === item.id }"
-              @click="onSelectResource(item)"
-            >
+          <van-empty v-if="!state.resourceList.length" image="search" description="暂无资源数据" />
+          <div v-else class="resource-grid">
+            <div v-for="item in state.resourceList" :key="item.id" class="resource-item"
+              :class="{ active: curSelectedResourceId === item.id }" @click="onSelectResource(item)">
               <div class="resource-header">
                 <span class="resource-name">{{ item.resName }}</span>
-                <van-tag
-                  type="primary"
-                  plain
-                >
+                <van-tag type="primary" plain>
                   {{ getDictLabel(cellType, item.resClass) }}
                 </van-tag>
               </div>
@@ -139,11 +72,7 @@
               <div class="resource-footer">
                 <span class="resource-usage">{{ item.usingSource || 0 }}/{{ item.maxNum }}</span>
                 <ul class="usage-dots">
-                  <li
-                    v-for="(p, index) in item.usedList"
-                    :key="index"
-                    :class="getUsedClass(p)"
-                  ></li>
+                  <li v-for="(p, index) in item.usedList" :key="index" :class="getUsedClass(p)"></li>
                 </ul>
               </div>
             </div>
@@ -153,33 +82,19 @@
           <div class="info-banner warning">房间使用情况</div>
           <div class="usage-list">
             <ul class="position-choose">
-              <li
-                v-for="(p, index) in allocationSituationList"
-                :key="index"
-              >
+              <li v-for="(p, index) in allocationSituationList" :key="index">
                 <i :class="getUsedClass(p)"></i>
                 <div class="txt-flex">
-                  <span
-                    v-if="p.userObj"
-                    :title="p.userObj.memberName"
-                  >
+                  <span v-if="p.userObj" :title="p.userObj.memberName">
                     {{ p.userObj.memberName }}
                   </span>
-                  <span
-                    v-else-if="p.userName"
-                    :title="p.userName"
-                  >
+                  <span v-else-if="p.userName" :title="p.userName">
                     {{ p.userName }}
                   </span>
                   <span v-else>空闲</span>
                 </div>
-                <van-button
-                  v-if="!p.userObj && (p.assignStatus == '10' || p.assignStatus == '40')"
-                  type="primary"
-                  size="mini"
-                  plain
-                  @click.stop="onAllocate(p)"
-                >
+                <van-button v-if="!p.userObj && (p.assignStatus == '10' || p.assignStatus == '40')" type="primary"
+                  size="mini" plain @click.stop="onAllocate(p)">
                   分配
                 </van-button>
               </li>
@@ -188,19 +103,10 @@
         </section>
       </div>
       <div class="popup-footer">
-        <van-button
-          type="primary"
-          plain
-          block
-          @click="onCancel"
-        >
+        <van-button type="primary" plain block @click="onCancel">
           取 消
         </van-button>
-        <van-button
-          type="primary"
-          block
-          @click="onSubmit('20')"
-        >
+        <van-button type="primary" block @click="onSubmit('20')">
           确 定
         </van-button>
       </div>
@@ -210,722 +116,788 @@
 </template>
 
 <script setup lang="ts" name="entryAllocate">
-  import to from 'await-to-js'
-  import { reactive, ref, defineAsyncComponent, computed } from 'vue'
-  import { showToast, showConfirmDialog } from 'vant'
-  import { getDictLabel } from '/@/utils/other'
-  import { usePlatformApi } from '/@/api/platform/home'
-  import { useSystemApi } from '/@/api/platform/system'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { storeToRefs } from 'pinia'
-  import { useDictApi } from '/@/api/base/system/dict'
-  import { useCellAssignApi } from '/@/api/platform/home/assign'
-  import { formatDate } from '/@/utils/formatTime'
-
-  const DetailsDialog = defineAsyncComponent(() => import('/@/view/entry/components/details.vue'))
-  const stores = useUserInfo()
-  const { userInfos } = storeToRefs(stores)
-
-  // 定义子组件向父组件传值/事件
-  const emit = defineEmits(['refresh'])
-
-  // 定义变量内容
-  const systemApi = useSystemApi()
-  const platformApi = usePlatformApi()
-  const dictApi = useDictApi()
-  const userTypeList = ref<RowDicDataType[]>([])
-  const projectGroupList = ref<any[]>([])
-  const cellType = ref<RowDicDataType[]>([])
-  const originalResourceList = ref<any[]>([])
-  const resourceList = ref<any[]>([])
-  const molecularGroupList = ref<any[]>([])
-  const cellAssignApi = useCellAssignApi()
-
-  const state = reactive({
-    form: {
-      id: 0,
-      memberId: 0,
-      memberName: '',
-      memberPhone: '',
-      memberSex: '',
-      memberNo: '',
-      memberIden: '',
-      startDate: '',
-      endDate: '',
-      memberType: '',
-      deptId: 0,
-      deptName: '',
-      workPlace: '',
-      mentorId: 0,
-      mentorName: '',
-      mentorPhone: '',
-      mentorDeptId: 0,
-      mentorDeptName: '',
-      platformId: 0,
-      platformName: '',
-      platformType: '',
-      platformTime: 0,
-      other: '',
-      isTemporary: '',
-    },
-    confirmList: [] as any[],
-    resourceList: [] as any[],
-    active: -1,
-    dialog: {
-      isShowDialog: false,
-      type: '',
-      title: '',
-      submitTxt: '',
-    },
-  })
-  const comfirmTotal = computed(() => {
-    return state.confirmList.reduce((pre: number, cur: any) => {
-      if (cur.resId) return pre + 1
-      return pre
-    }, 0)
-  })
-  const originalConfirmList = ref<any[]>([])
-  const confirmList = ref<any[]>([])
-  const belongOrgOption = ref<any[]>([])
-  const userList = ref<any[]>([])
-  const platformList = ref()
-  const resClass = ref('')
-  const groupId = ref('') // 资源组ID
-  const groupPickerVisible = ref(false)
-  const detailsDialog = ref()
-  const curSelectedResourceId = ref(0) // 当前选中的资源ID
-  const allocationSituationList = ref<any[]>([]) // 资源分配情况列表
-  const groupOptions = computed(() => {
-    const options = molecularGroupList.value.map((item: any) => ({
-      text: item.groupName,
-      value: String(item.id),
-    }))
-    return [{ text: '全部资源组', value: '' }, ...options]
-  })
-  const groupLabel = computed(() => {
-    const option = groupOptions.value.find((item) => item.value === groupId.value)
-    return option ? option.text : '全部资源组'
-  })
-  const onGroupConfirm = (value: any, detail?: any) => {
-    let option: any = null
-    if (value && typeof value === 'object' && 'selectedOptions' in value) {
-      option = value.selectedOptions?.[0]
-    } else if (detail && typeof detail === 'object' && 'selectedOptions' in detail) {
-      option = detail.selectedOptions?.[0]
-    } else if (Array.isArray(value)) {
-      const target = value[0]
-      option = groupOptions.value.find((item) => item.value === target)
-    } else if (value && typeof value === 'object') {
-      option = value
-    }
-    groupId.value = option?.value ?? ''
-    groupPickerVisible.value = false
-    filterOutListByGroupId(groupId.value)
-  }
-  const getDicts = () => {
-    Promise.all([
-      systemApi.getDeptTree(),
-      systemApi.getUserList({ noPage: true }),
-      platformApi.getAllPlatformList({ noPage: true }),
-      dictApi.getDictDataByType('sys_user_type'),
-      systemApi.getProjectGroupListForApp({ noPage: true }),
-      userInfos.value.platformId != 0 ? platformApi.getResourceTypeDict({ id: userInfos.value.platformId }) : '',
-    ]).then(([dept, user, plat, type, pjt, cell]) => {
-      belongOrgOption.value = dept?.data || []
-      userList.value = user?.data?.list || []
-      platformList.value = plat?.data?.list || []
-      userTypeList.value = type?.data?.values || []
-      projectGroupList.value = pjt?.data?.list || []
-      cellType.value = cell?.data || []
-    })
-  }
-  const getMolecularGroupList = async () => {
-    const [err, res]: ToResponse = await to(platformApi.getMolecularGroupList({ id: userInfos.value.platformId }))
-    if (err) return
-    molecularGroupList.value = res?.data || []
-  }
-  const getResourceList = async () => {
-    const [err, res]: ToResponse = await to(
-      platformApi.getResourceList({ platformId: userInfos.value.platformId, resStatus: '10' }),
-    )
-    if (err) return
-    originalResourceList.value = JSON.parse(JSON.stringify(res?.data?.list || []))
-    resourceList.value = JSON.parse(JSON.stringify(originalResourceList.value))
-    state.resourceList = JSON.parse(JSON.stringify(resourceList.value))
-  }
-  // 待分配列表
-  const getConfirmList = async () => {
-    const [err, res]: ToResponse = await to(cellAssignApi.assignQueue({ platformId: userInfos.value.platformId }))
-    if (err) return
-    originalConfirmList.value = JSON.parse(JSON.stringify(res?.data || []))
-    confirmList.value = JSON.parse(JSON.stringify(originalConfirmList.value))
-    state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
-  }
-  // 打开弹窗
-  const openDialog = async (type: string, row: number) => {
-    getDicts()
-    getResourceList()
-    getConfirmList()
-    getMolecularGroupList()
-    state.dialog.type = type
-    state.dialog.title = '入室资源分配'
-    state.dialog.isShowDialog = true
-  }
-  // 前端筛选类型
-  const filterOutList = (val: string) => {
-    if (val) {
-      state.resourceList = resourceList.value.filter((item: any) => item.resClass == val)
-      state.confirmList = confirmList.value.filter((item: any) => item.cellSourceType == val)
-    } else {
-      state.resourceList = [...resourceList.value]
-      state.confirmList = [...confirmList.value]
-    }
-  }
-  // 前端筛选资源组
-  const filterOutListByGroupId = (val: string) => {
-    if (val) {
-      state.resourceList = resourceList.value.filter((item: any) => String(item.groupId) === val)
-    } else {
-      state.resourceList = [...resourceList.value]
-    }
-  }
-  // 选择资源房间
-  const onSelectResource = (row: any) => {
-    curSelectedResourceId.value = row.id
-    allocationSituationList.value = row.usedList
-  }
-  // 确认
-  const onConfirm = (row: any) => {
-    // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
-  }
-  // 关闭弹窗
-  const resetDialogState = () => {
-    confirmList.value = JSON.parse(JSON.stringify(originalConfirmList.value))
-    state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
-    resourceList.value = JSON.parse(JSON.stringify(originalResourceList.value))
-    state.resourceList = JSON.parse(JSON.stringify(resourceList.value))
-    allocationSituationList.value = []
-    curSelectedResourceId.value = 0
-    resClass.value = ''
-    groupId.value = ''
-    state.active = -1
+import to from 'await-to-js'
+import { reactive, ref, defineAsyncComponent, computed } from 'vue'
+import { showToast, showConfirmDialog } from 'vant'
+import { getDictLabel } from '/@/utils/other'
+import { usePlatformApi } from '/@/api/platform/home'
+import { useSystemApi } from '/@/api/platform/system'
+import { useUserInfo } from '/@/stores/userInfo'
+import { storeToRefs } from 'pinia'
+import { useDictApi } from '/@/api/base/system/dict'
+import { useCellAssignApi } from '/@/api/platform/home/assign'
+import { formatDate } from '/@/utils/formatTime'
+
+const DetailsDialog = defineAsyncComponent(() => import('/@/view/entry/components/details.vue'))
+const stores = useUserInfo()
+const { userInfos } = storeToRefs(stores)
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh'])
+
+// 定义变量内容
+const systemApi = useSystemApi()
+const platformApi = usePlatformApi()
+const dictApi = useDictApi()
+const userTypeList = ref<RowDicDataType[]>([])
+const projectGroupList = ref<any[]>([])
+const cellType = ref<RowDicDataType[]>([])
+const originalResourceList = ref<any[]>([])
+const resourceList = ref<any[]>([])
+const molecularGroupList = ref<any[]>([])
+const cellAssignApi = useCellAssignApi()
+
+const state = reactive({
+  form: {
+    id: 0,
+    memberId: 0,
+    memberName: '',
+    memberPhone: '',
+    memberSex: '',
+    memberNo: '',
+    memberIden: '',
+    startDate: '',
+    endDate: '',
+    memberType: '',
+    deptId: 0,
+    deptName: '',
+    workPlace: '',
+    mentorId: 0,
+    mentorName: '',
+    mentorPhone: '',
+    mentorDeptId: 0,
+    mentorDeptName: '',
+    platformId: 0,
+    platformName: '',
+    platformType: '',
+    platformTime: 0,
+    other: '',
+    isTemporary: '',
+  },
+  confirmList: [] as any[],
+  resourceList: [] as any[],
+  active: -1,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+})
+const comfirmTotal = computed(() => {
+  return state.confirmList.reduce((pre: number, cur: any) => {
+    if (cur.resId) return pre + 1
+    return pre
+  }, 0)
+})
+const originalConfirmList = ref<any[]>([])
+const confirmList = ref<any[]>([])
+const belongOrgOption = ref<any[]>([])
+const userList = ref<any[]>([])
+const platformList = ref()
+const resClass = ref('')
+const groupId = ref('') // 资源组ID
+const groupPickerVisible = ref(false)
+const detailsDialog = ref()
+const curSelectedResourceId = ref(0) // 当前选中的资源ID
+const allocationSituationList = ref<any[]>([]) // 资源分配情况列表
+const groupOptions = computed(() => {
+  const options = molecularGroupList.value.map((item: any) => ({
+    text: item.groupName,
+    value: String(item.id),
+  }))
+  return [{ text: '全部资源组', value: '' }, ...options]
+})
+const groupLabel = computed(() => {
+  const option = groupOptions.value.find((item) => item.value === groupId.value)
+  return option ? option.text : '全部资源组'
+})
+const onGroupConfirm = (value: any, detail?: any) => {
+  let option: any = null
+  if (value && typeof value === 'object' && 'selectedOptions' in value) {
+    option = value.selectedOptions?.[0]
+  } else if (detail && typeof detail === 'object' && 'selectedOptions' in detail) {
+    option = detail.selectedOptions?.[0]
+  } else if (Array.isArray(value)) {
+    const target = value[0]
+    option = groupOptions.value.find((item) => item.value === target)
+  } else if (value && typeof value === 'object') {
+    option = value
   }
-  const closeDialog = () => {
-    resetDialogState()
-    state.dialog.isShowDialog = false
-    emit('refresh')
+  groupId.value = option?.value ?? ''
+  groupPickerVisible.value = false
+  filterOutListByGroupId(groupId.value)
+}
+const getDicts = () => {
+  Promise.all([
+    systemApi.getDeptTree(),
+    systemApi.getUserList({ noPage: true }),
+    platformApi.getAllPlatformList({ noPage: true }),
+    dictApi.getDictDataByType('sys_user_type'),
+    systemApi.getProjectGroupListForApp({ noPage: true }),
+    userInfos.value.platformId != 0 ? platformApi.getResourceTypeDict({ id: userInfos.value.platformId }) : '',
+  ]).then(([dept, user, plat, type, pjt, cell]) => {
+    belongOrgOption.value = dept?.data || []
+    userList.value = user?.data?.list || []
+    platformList.value = plat?.data?.list || []
+    userTypeList.value = type?.data?.values || []
+    projectGroupList.value = pjt?.data?.list || []
+    cellType.value = cell?.data || []
+  })
+}
+const getMolecularGroupList = async () => {
+  const [err, res]: ToResponse = await to(platformApi.getMolecularGroupList({ id: userInfos.value.platformId }))
+  if (err) return
+  molecularGroupList.value = res?.data || []
+}
+const getResourceList = async () => {
+  const [err, res]: ToResponse = await to(
+    platformApi.getResourceList({ platformId: userInfos.value.platformId, resStatus: '10' }),
+  )
+  if (err) return
+  originalResourceList.value = JSON.parse(JSON.stringify(res?.data?.list || []))
+  resourceList.value = JSON.parse(JSON.stringify(originalResourceList.value))
+  state.resourceList = JSON.parse(JSON.stringify(resourceList.value))
+}
+// 待分配列表
+const getConfirmList = async () => {
+  const [err, res]: ToResponse = await to(cellAssignApi.assignQueue({ platformId: userInfos.value.platformId }))
+  if (err) return
+  originalConfirmList.value = JSON.parse(JSON.stringify(res?.data || []))
+  confirmList.value = JSON.parse(JSON.stringify(originalConfirmList.value))
+  state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
+}
+// 打开弹窗
+const openDialog = async (type: string, row: number) => {
+  getDicts()
+  getResourceList()
+  getConfirmList()
+  getMolecularGroupList()
+  state.dialog.type = type
+  state.dialog.title = '入室资源分配'
+  state.dialog.isShowDialog = true
+}
+// 前端筛选类型
+const filterOutList = (val: string) => {
+  if (val) {
+    state.resourceList = resourceList.value.filter((item: any) => item.resClass == val)
+    state.confirmList = confirmList.value.filter((item: any) => item.cellSourceType == val)
+  } else {
+    state.resourceList = [...resourceList.value]
+    state.confirmList = [...confirmList.value]
   }
-  // 取消
-  const onCancel = () => {
-    closeDialog()
+}
+// 前端筛选资源组
+const filterOutListByGroupId = (val: string) => {
+  if (val) {
+    state.resourceList = resourceList.value.filter((item: any) => String(item.groupId) === val)
+  } else {
+    state.resourceList = [...resourceList.value]
   }
+}
+// 选择资源房间
+const onSelectResource = (row: any) => {
+  curSelectedResourceId.value = row.id
+  allocationSituationList.value = row.usedList
+}
+// 确认
+const onConfirm = (row: any) => {
+  // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
+}
+// 关闭弹窗
+const resetDialogState = () => {
+  confirmList.value = JSON.parse(JSON.stringify(originalConfirmList.value))
+  state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
+  resourceList.value = JSON.parse(JSON.stringify(originalResourceList.value))
+  state.resourceList = JSON.parse(JSON.stringify(resourceList.value))
+  allocationSituationList.value = []
+  curSelectedResourceId.value = 0
+  resClass.value = ''
+  groupId.value = ''
+  state.active = -1
+}
+const closeDialog = () => {
+  resetDialogState()
+  state.dialog.isShowDialog = false
+}
+// 取消
+const onCancel = () => {
+  closeDialog()
+}
 
-  const handelBatchAssign = async () => {
-    const checkedUser = state.confirmList.filter((item: any) => item.checked)
-    console.log(checkedUser)
-    if (!checkedUser.length) {
-      showToast({ message: '请选择待分配队列人员', type: 'text' })
-      return
-    }
-    showConfirmDialog({
-      title: '提示',
-      message: '是否将选中队列人员一键分配资源?',
-      confirmButtonText: '确认',
-      cancelButtonText: '取消',
+const handelBatchAssign = async () => {
+  const checkedUser = state.confirmList.filter((item: any) => item.checked)
+  console.log(checkedUser)
+  if (!checkedUser.length) {
+    showToast({ message: '请选择待分配队列人员', type: 'text' })
+    return
+  }
+  showConfirmDialog({
+    title: '提示',
+    message: '是否将选中队列人员一键分配资源?',
+    confirmButtonText: '确认',
+    cancelButtonText: '取消',
+  })
+    .then(async () => {
+      const [err]: ToResponse = await to(
+        platformApi.batchAssign({ appointIds: checkedUser.map((item: any) => item.id) }),
+      )
+      if (err) return
+      showToast({ message: '一键分配成功', type: 'success' })
+      getResourceList()
+      getConfirmList()
     })
-      .then(async () => {
-        const [err]: ToResponse = await to(
-          platformApi.batchAssign({ appointIds: checkedUser.map((item: any) => item.id) }),
-        )
-        if (err) return
-        showToast({ message: '一键分配成功', type: 'success' })
-        getResourceList()
-        getConfirmList()
-      })
-      .catch(() => {})
+    .catch(() => { })
+}
+// 提交
+const onSubmit = async (type: string) => {
+  const arr = confirmList.value.filter((item: any) => item.resId)
+  if (!arr.length) {
+    showToast({ message: '请分配资源', type: 'text' })
+    return
   }
-  // 提交
-  const onSubmit = async (type: string) => {
-    const arr = confirmList.value.filter((item: any) => item.resId)
-    if (!arr.length) {
-      showToast({ message: '请分配资源', type: 'text' })
-      return
-    }
 
-    const [err]: ToResponse = await to(
-      cellAssignApi.create({
-        assignList: arr.map((item: any) => {
-          return {
-            appointId: item.id,
-            resId: item.resId,
-            location: item.location,
-            replaceId: item.replaceId,
-            mainId: item.id || 0,
-            platformType: item.platformType || '10',
-          }
-        }),
+  const [err]: ToResponse = await to(
+    cellAssignApi.create({
+      assignList: arr.map((item: any) => {
+        return {
+          appointId: item.id,
+          resId: item.resId,
+          location: item.location,
+          replaceId: item.replaceId,
+          mainId: item.id || 0,
+          platformType: item.platformType || '10',
+        }
       }),
-    )
-    if (err) return
-    showToast({ message: '操作成功', type: 'success' })
-    emit('refresh')
-    closeDialog()
+    }),
+  )
+  if (err) return
+  showToast({ message: '操作成功', type: 'success' })
+  emit('refresh')
+  closeDialog()
+}
+const onDetail = (val: number) => {
+  detailsDialog.value.openDialog('get', val)
+}
+// 分配
+const onAllocate = (pos: any) => {
+  if (state.active < 0) {
+    showToast({ message: '请先选择一个待分配人员', type: 'text' })
+    return
   }
-  const onDetail = (val: number) => {
-    detailsDialog.value.openDialog('get', val)
+  const activeObj = state.confirmList[state.active]
+  if (activeObj.resId) {
+    showToast({ message: '该人员已分配房间,如需调整请先取消当前分配', type: 'text' })
+    return
   }
-  // 分配
-  const onAllocate = (pos: any) => {
-    if (state.active < 0) {
-      showToast({ message: '请先选择一个待分配人员', type: 'text' })
-      return
-    }
-    const activeObj = state.confirmList[state.active]
-    if (activeObj.resId) {
-      showToast({ message: '该人员已分配房间,如需调整请先取消当前分配', type: 'text' })
-      return
-    }
-    const room = state.resourceList.find((item: any) => item.id == curSelectedResourceId.value)
-    if (!room) {
-      showToast({ message: '请选择资源房间', type: 'text' })
-      return
-    }
-    if (room.resClass != activeObj.cellSourceType) {
-      showToast({ message: '请选择正确的资源', type: 'text' })
-      return
-    }
-    activeObj.resId = room.id
-    activeObj.resName = room.resName
-    activeObj.location = pos.location
-    activeObj.replaceId = pos.id
-    const user = confirmList.value.find((item: any) => item.id == activeObj.id)
-    user.resId = room.id
-    user.resName = room.resName
-    user.location = pos.location
-    user.replaceId = pos.id
-    pos.userObj = { ...activeObj }
-    const resource = resourceList.value.find((item: any) => item.id == room.id)
-    const used = resource.usedList.find((item: any) => item.location == pos.location)
-    used.userObj = { ...activeObj }
+  const room = state.resourceList.find((item: any) => item.id == curSelectedResourceId.value)
+  if (!room) {
+    showToast({ message: '请选择资源房间', type: 'text' })
+    return
   }
-  // 取消分配
-  const cancelAllocate = (obj: any) => {
-    if (!obj?.resId || !obj?.location) return
-    const { resId, location } = obj
-
-    const resetRoomUsage = (list: any[]) => {
-      const room = list.find((item: any) => item.id === resId)
-      if (!room?.usedList) return
-      const used = room.usedList.find((i: any) => i.location === location)
-      if (used) used.userObj = null
-    }
-
-    resetRoomUsage(resourceList.value)
-    resetRoomUsage(state.resourceList)
+  if (room.resClass != activeObj.cellSourceType) {
+    showToast({ message: '请选择正确的资源', type: 'text' })
+    return
+  }
+  activeObj.resId = room.id
+  activeObj.resName = room.resName
+  activeObj.location = pos.location
+  activeObj.replaceId = pos.id
+  const user = confirmList.value.find((item: any) => item.id == activeObj.id)
+  user.resId = room.id
+  user.resName = room.resName
+  user.location = pos.location
+  user.replaceId = pos.id
+  pos.userObj = { ...activeObj }
+  const resource = resourceList.value.find((item: any) => item.id == room.id)
+  const used = resource.usedList.find((item: any) => item.location == pos.location)
+  used.userObj = { ...activeObj }
+}
+// 取消分配
+const cancelAllocate = (obj: any) => {
+  if (!obj?.resId || !obj?.location) return
+  const { resId, location } = obj
 
-    if (Array.isArray(allocationSituationList.value) && allocationSituationList.value.length) {
-      const usage = allocationSituationList.value.find((item: any) => item.location === location)
-      if (usage) usage.userObj = null
-    }
+  const resetRoomUsage = (list: any[]) => {
+    const room = list.find((item: any) => item.id === resId)
+    if (!room?.usedList) return
+    const used = room.usedList.find((i: any) => i.location === location)
+    if (used) used.userObj = null
+  }
 
-    const user = confirmList.value.find((item) => item.id === obj.id)
-    if (user) {
-      user.resId = null
-      user.resName = null
-      user.location = null
-      user.replaceId = null
-    }
+  resetRoomUsage(resourceList.value)
+  resetRoomUsage(state.resourceList)
 
-    obj.resId = null
-    obj.resName = null
-    obj.location = null
-    obj.replaceId = null
+  if (Array.isArray(allocationSituationList.value) && allocationSituationList.value.length) {
+    const usage = allocationSituationList.value.find((item: any) => item.location === location)
+    if (usage) usage.userObj = null
+  }
 
-    filterOutList(resClass.value)
+  const user = confirmList.value.find((item) => item.id === obj.id)
+  if (user) {
+    user.resId = null
+    user.resName = null
+    user.location = null
+    user.replaceId = null
   }
-  const getUsedClass = (row: any) => {
-    if (row.assignStatus == '10') {
-      // 空
-      if (row.userObj) return 'empty-allocate'
-      return 'empty'
-    } else if (row.assignStatus == '40') {
-      // 次月离室
-      if (row.userObj) return 'leave-allocate'
-      return 'leave'
-    } else {
-      // 占用20 已分配45
-      return 'used'
-    }
+
+  obj.resId = null
+  obj.resName = null
+  obj.location = null
+  obj.replaceId = null
+
+  filterOutList(resClass.value)
+}
+const getUsedClass = (row: any) => {
+  if (row.assignStatus == '10') {
+    // 空
+    if (row.userObj) return 'empty-allocate'
+    return 'empty'
+  } else if (row.assignStatus == '40') {
+    // 次月离室
+    if (row.userObj) return 'leave-allocate'
+    return 'leave'
+  } else {
+    // 占用20 已分配45
+    return 'used'
   }
-  // 暴露变量
-  defineExpose({
-    openDialog,
-  })
+}
+// 暴露变量
+defineExpose({
+  openDialog,
+})
 </script>
 <style lang="scss" scoped>
-  .allocate-popup-container {
-    .allocate-popup {
-      display: flex;
-      flex-direction: column;
-      height: 100%;
-      padding: 16px;
-      box-sizing: border-box;
+.allocate-popup-container {
+  .allocate-popup {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    padding: 16px;
+    box-sizing: border-box;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    flex-wrap: wrap;
+    gap: 16px;
+    margin-bottom: 16px;
+
+    .popup-title {
+      font-size: 20px;
+      font-weight: 600;
+      color: #111827;
     }
-    .popup-header {
+
+    .popup-controls {
       display: flex;
-      justify-content: space-between;
-      align-items: flex-start;
+      align-items: center;
+      gap: 12px;
       flex-wrap: wrap;
-      gap: 16px;
-      margin-bottom: 16px;
-      .popup-title {
-        font-size: 20px;
-        font-weight: 600;
-        color: #111827;
-      }
-      .popup-controls {
+
+      .group-selector {
         display: flex;
         align-items: center;
-        gap: 12px;
-        flex-wrap: wrap;
-        .group-selector {
-          display: flex;
-          align-items: center;
-          gap: 10px;
-          padding: 6px 12px;
-          background: #ffffff;
-          border-radius: 999px;
-          border: 1px solid #e5e7ef;
-          box-shadow: 0 2px 6px rgba(148, 163, 184, 0.15);
-          cursor: pointer;
-          flex: 1;
-          .label {
-            width: 100px;
-            font-size: 13px;
-            color: #4b5563;
-            font-weight: 500;
-          }
-          :deep(.van-field) {
-            padding: 0;
-            min-width: 120px;
-            .van-field__control {
-              font-size: 13px;
-              color: #1f2937;
-            }
-            .van-field__control--right {
-              text-align: right;
-            }
-            .van-field__right-icon {
-              color: #2563eb;
-            }
-          }
+        gap: 10px;
+        padding: 6px 12px;
+        background: #ffffff;
+        border-radius: 999px;
+        border: 1px solid #e5e7ef;
+        box-shadow: 0 2px 6px rgba(148, 163, 184, 0.15);
+        cursor: pointer;
+        flex: 1;
+
+        .label {
+          width: 100px;
+          font-size: 13px;
+          color: #4b5563;
+          font-weight: 500;
         }
-        .type-radio-group {
-          display: flex;
-          align-items: center;
-          gap: 8px;
-          padding: 6px 10px;
-          background: #eef2ff;
-          border-radius: 999px;
-          .van-radio__icon {
-            display: none;
-          }
-          .van-radio__label {
-            color: #374151;
+
+        :deep(.van-field) {
+          padding: 0;
+          min-width: 120px;
+
+          .van-field__control {
             font-size: 13px;
+            color: #1f2937;
           }
-          .van-radio--horizontal {
-            padding: 0 6px;
+
+          .van-field__control--right {
+            text-align: right;
           }
-          .van-radio--horizontal.van-radio--checked .van-radio__label {
+
+          .van-field__right-icon {
             color: #2563eb;
-            font-weight: 600;
           }
         }
       }
-    }
-    .popup-body {
-      flex: 1;
-      display: flex;
-      flex-direction: column;
-      gap: 16px;
-      overflow-y: auto;
-      padding-bottom: 16px;
-    }
-    .card {
-      background: #fff;
-      border-radius: 12px;
-      padding: 16px;
-      box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
-      display: flex;
-      flex-direction: column;
-      gap: 12px;
-      .info-banner {
-        padding: 10px 14px;
-        border-radius: 8px;
-        font-size: 15px;
-        font-weight: 600;
-        &.warning {
-          background: linear-gradient(90deg, rgba(252, 211, 77, 0.2), rgba(253, 230, 138, 0.5));
-          color: #b45309;
+
+      .type-radio-group {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        padding: 6px 10px;
+        background: #eef2ff;
+        border-radius: 999px;
+
+        .van-radio__icon {
+          display: none;
         }
-      }
-    }
-    .confirm-card {
-      ul {
-        max-height: 420px;
-        overflow-y: auto;
-        padding-right: 4px;
-        &::-webkit-scrollbar {
-          width: 6px;
+
+        .van-radio__label {
+          color: #374151;
+          font-size: 13px;
         }
-        &::-webkit-scrollbar-thumb {
-          background: rgba(156, 163, 175, 0.4);
-          border-radius: 999px;
+
+        .van-radio--horizontal {
+          padding: 0 6px;
         }
-        li {
-          display: flex;
-          align-items: flex-start;
-          justify-content: space-between;
-          gap: 12px;
-          padding: 12px;
-          border: 1px solid #e5e7eb;
-          border-radius: 10px;
-          transition: border-color 0.2s ease, box-shadow 0.2s ease;
-          cursor: pointer;
-          & + li {
-            margin-top: 10px;
-          }
-          &.active {
-            border-color: #93c5fd;
-            box-shadow: 0 10px 20px rgba(59, 130, 246, 0.08);
-          }
-          .text {
-            flex: 1;
-            display: flex;
-            flex-direction: column;
-            gap: 6px;
-            p {
-              margin: 0;
-              color: #374151;
-              font-size: 14px;
-              line-height: 1.4;
-              white-space: nowrap;
-              overflow: hidden;
-              text-overflow: ellipsis;
-            }
-            .assign-room {
-              color: #2563eb;
-              font-weight: 600;
-            }
-          }
-          .btns {
-            display: flex;
-            flex-wrap: wrap;
-            gap: 8px;
-            justify-content: flex-end;
-            .link-btn {
-              padding: 6px 12px;
-              border-radius: 999px;
-              border: 1px solid transparent;
-              background: #eff6ff;
-              font-size: 13px;
-              font-weight: 500;
-              color: #1d4ed8;
-              cursor: pointer;
-              transition: all 0.2s ease;
-              line-height: 1;
-              &:hover {
-                background: #dbeafe;
-                color: #1e40af;
-                box-shadow: 0 4px 12px rgba(29, 78, 216, 0.25);
-              }
-              &.danger {
-                background: #fee2e2;
-                color: #dc2626;
-                &:hover {
-                  background: #fecaca;
-                  color: #b91c1c;
-                  box-shadow: 0 4px 12px rgba(220, 38, 38, 0.25);
-                }
-              }
-            }
-          }
+
+        .van-radio--horizontal.van-radio--checked .van-radio__label {
+          color: #2563eb;
+          font-weight: 600;
         }
       }
     }
-    .resource-card {
-      .van-empty {
-        padding: 24px 0;
-        .van-empty__description {
-          color: #6b7280;
-          font-size: 14px;
-        }
+  }
+
+  .popup-body {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    overflow-y: auto;
+    padding-bottom: 16px;
+  }
+
+  .card {
+    background: #fff;
+    border-radius: 12px;
+    padding: 16px;
+    box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    .info-banner {
+      padding: 10px 14px;
+      border-radius: 8px;
+      font-size: 15px;
+      font-weight: 600;
+
+      &.warning {
+        background: linear-gradient(90deg, rgba(252, 211, 77, 0.2), rgba(253, 230, 138, 0.5));
+        color: #b45309;
       }
-      .resource-grid {
-        display: grid;
-        grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
-        gap: 12px;
+    }
+  }
+
+  .confirm-card {
+    ul {
+      max-height: 420px;
+      overflow-y: auto;
+      padding-right: 4px;
+
+      &::-webkit-scrollbar {
+        width: 6px;
       }
-      .resource-item {
-        background: #f9fafb;
-        border-radius: 10px;
-        padding: 16px;
-        border: 1px solid transparent;
+
+      &::-webkit-scrollbar-thumb {
+        background: rgba(156, 163, 175, 0.4);
+        border-radius: 999px;
+      }
+
+      li {
         display: flex;
-        flex-direction: column;
-        gap: 10px;
+        align-items: flex-start;
+        justify-content: space-between;
+        gap: 12px;
+        padding: 12px;
+        border: 1px solid #e5e7eb;
+        border-radius: 10px;
+        transition: border-color 0.2s ease, box-shadow 0.2s ease;
         cursor: pointer;
-        transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
-        &:hover {
-          transform: translateY(-2px);
-          box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
+
+        &+li {
+          margin-top: 10px;
         }
+
         &.active {
-          border-color: #60a5fa;
-          box-shadow: 0 8px 18px rgba(59, 130, 246, 0.16);
+          border-color: #93c5fd;
+          box-shadow: 0 10px 20px rgba(59, 130, 246, 0.08);
         }
-        .resource-header {
+
+        .text {
+          flex: 1;
           display: flex;
-          justify-content: space-between;
-          align-items: center;
-          gap: 8px;
-          .resource-name {
-            font-size: 16px;
+          flex-direction: column;
+          gap: 6px;
+
+          p {
+            margin: 0;
+            color: #374151;
+            font-size: 14px;
+            line-height: 1.4;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .assign-room {
+            color: #2563eb;
             font-weight: 600;
-            color: #111827;
           }
         }
-        .resource-location {
-          color: #6b7280;
-          font-size: 13px;
-        }
-        .resource-footer {
+
+        .btns {
           display: flex;
-          align-items: center;
-          justify-content: space-between;
-          .resource-usage {
-            font-size: 14px;
-            font-weight: 600;
-            color: #111827;
-          }
-          .usage-dots {
-            display: flex;
-            flex-wrap: wrap;
-            gap: 6px;
-            row-gap: 6px;
-            list-style: none;
-            padding: 0;
-            margin: 0;
-            max-width: 120px;
-            justify-content: flex-end;
-            li {
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background: #86efac;
-              &.used {
-                background: #f87171;
-              }
-              &.empty-allocate {
-                background: linear-gradient(90deg, #86efac 0%, #86efac 50%, #2563eb 50%, #2563eb 100%);
-              }
-              &.leave {
-                background: #fb923c;
-              }
-              &.leave-allocate {
-                background: linear-gradient(90deg, #fb923c 0%, #fb923c 50%, #2563eb 50%, #2563eb 100%);
+          flex-wrap: wrap;
+          gap: 8px;
+          justify-content: flex-end;
+
+          .link-btn {
+            padding: 6px 12px;
+            border-radius: 999px;
+            border: 1px solid transparent;
+            background: #eff6ff;
+            font-size: 13px;
+            font-weight: 500;
+            color: #1d4ed8;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            line-height: 1;
+
+            &:hover {
+              background: #dbeafe;
+              color: #1e40af;
+              box-shadow: 0 4px 12px rgba(29, 78, 216, 0.25);
+            }
+
+            &.danger {
+              background: #fee2e2;
+              color: #dc2626;
+
+              &:hover {
+                background: #fecaca;
+                color: #b91c1c;
+                box-shadow: 0 4px 12px rgba(220, 38, 38, 0.25);
               }
             }
           }
         }
       }
     }
-    .usage-card {
-      .usage-list {
-        max-height: 360px;
-        overflow-y: auto;
-        padding-right: 4px;
-        &::-webkit-scrollbar {
-          width: 6px;
+  }
+
+  .resource-card {
+    .van-empty {
+      padding: 24px 0;
+
+      .van-empty__description {
+        color: #6b7280;
+        font-size: 14px;
+      }
+    }
+
+    .resource-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+      gap: 12px;
+    }
+
+    .resource-item {
+      background: #f9fafb;
+      border-radius: 10px;
+      padding: 16px;
+      border: 1px solid transparent;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+      cursor: pointer;
+      transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
+
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
+      }
+
+      &.active {
+        border-color: #60a5fa;
+        box-shadow: 0 8px 18px rgba(59, 130, 246, 0.16);
+      }
+
+      .resource-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        gap: 8px;
+
+        .resource-name {
+          font-size: 16px;
+          font-weight: 600;
+          color: #111827;
         }
-        &::-webkit-scrollbar-thumb {
-          background: rgba(156, 163, 175, 0.4);
-          border-radius: 999px;
+      }
+
+      .resource-location {
+        color: #6b7280;
+        font-size: 13px;
+      }
+
+      .resource-footer {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .resource-usage {
+          font-size: 14px;
+          font-weight: 600;
+          color: #111827;
         }
-        .position-choose {
+
+        .usage-dots {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 6px;
+          row-gap: 6px;
           list-style: none;
           padding: 0;
           margin: 0;
-          display: flex;
-          flex-direction: column;
-          gap: 8px;
+          max-width: 120px;
+          justify-content: flex-end;
+
           li {
-            display: flex;
-            align-items: center;
-            gap: 10px;
-            padding: 8px 10px;
-            border: 1px solid #e5e7eb;
-            border-radius: 8px;
-            background: #f9fafb;
-            i {
-              width: 10px;
-              height: 10px;
-              border-radius: 50%;
-              background: #86efac;
-              &.used {
-                background: #f87171;
-              }
-              &.empty-allocate {
-                background: linear-gradient(90deg, #86efac 0%, #86efac 50%, #2563eb 50%, #2563eb 100%);
-              }
-              &.leave {
-                background: #fb923c;
-              }
-              &.leave-allocate {
-                background: linear-gradient(90deg, #fb923c 0%, #fb923c 50%, #2563eb 50%, #2563eb 100%);
-              }
+            width: 10px;
+            height: 10px;
+            border-radius: 50%;
+            background: #86efac;
+
+            &.used {
+              background: #f87171;
+            }
+
+            &.empty-allocate {
+              background: linear-gradient(90deg, #86efac 0%, #86efac 50%, #2563eb 50%, #2563eb 100%);
             }
-            .txt-flex {
-              flex: 1;
-              white-space: nowrap;
-              overflow: hidden;
-              text-overflow: ellipsis;
-              color: #374151;
-              font-size: 14px;
+
+            &.leave {
+              background: #fb923c;
             }
-            .van-button {
-              min-width: 64px;
+
+            &.leave-allocate {
+              background: linear-gradient(90deg, #fb923c 0%, #fb923c 50%, #2563eb 50%, #2563eb 100%);
             }
           }
         }
       }
     }
-    .popup-footer {
-      display: flex;
-      gap: 12px;
-      margin-top: 16px;
-      .van-button {
-        height: 44px;
-        font-size: 16px;
-        font-weight: 600;
+  }
+
+  .usage-card {
+    .usage-list {
+      max-height: 360px;
+      overflow-y: auto;
+      padding-right: 4px;
+
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background: rgba(156, 163, 175, 0.4);
+        border-radius: 999px;
+      }
+
+      .position-choose {
+        list-style: none;
+        padding: 0;
+        margin: 0;
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+
+        li {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          padding: 8px 10px;
+          border: 1px solid #e5e7eb;
+          border-radius: 8px;
+          background: #f9fafb;
+
+          i {
+            width: 10px;
+            height: 10px;
+            border-radius: 50%;
+            background: #86efac;
+
+            &.used {
+              background: #f87171;
+            }
+
+            &.empty-allocate {
+              background: linear-gradient(90deg, #86efac 0%, #86efac 50%, #2563eb 50%, #2563eb 100%);
+            }
+
+            &.leave {
+              background: #fb923c;
+            }
+
+            &.leave-allocate {
+              background: linear-gradient(90deg, #fb923c 0%, #fb923c 50%, #2563eb 50%, #2563eb 100%);
+            }
+          }
+
+          .txt-flex {
+            flex: 1;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            color: #374151;
+            font-size: 14px;
+          }
+
+          .van-button {
+            min-width: 64px;
+          }
+        }
       }
     }
   }
-  @media (min-width: 1024px) {
-    .allocate-popup-container {
-      .popup-body {
-        flex-direction: row;
-        .confirm-card {
-          flex: 0 0 300px;
-        }
-        .resource-card {
-          flex: 1;
-        }
-        .usage-card {
-          flex: 0 0 280px;
-        }
+
+  .popup-footer {
+    display: flex;
+    gap: 12px;
+    margin-top: 16px;
+
+    .van-button {
+      height: 44px;
+      font-size: 16px;
+      font-weight: 600;
+    }
+  }
+}
+
+@media (min-width: 1024px) {
+  .allocate-popup-container {
+    .popup-body {
+      flex-direction: row;
+
+      .confirm-card {
+        flex: 0 0 300px;
+      }
+
+      .resource-card {
+        flex: 1;
+      }
+
+      .usage-card {
+        flex: 0 0 280px;
       }
     }
   }
+}
 </style>

+ 432 - 459
src/view/entry/components/confirm.vue

@@ -8,39 +8,18 @@
 -->
 <template>
   <div class="facilities-dialog-container">
-    <van-popup
-      class="facilities-popup"
-      v-model:show="state.dialog.isShowDialog"
-      position="right"
-      :style="{ width: '100%' }"
-      closeable
-      :close-on-popstate="false"
-      @close="onCancel"
-    >
+    <van-popup class="facilities-popup" v-model:show="state.dialog.isShowDialog" position="right"
+      :style="{ width: '100%' }" closeable :close-on-popstate="false" @close="onCancel">
       <header class="popup-header">
         <span class="popup-title">{{ state.dialog.title || '入室资源确认' }}</span>
-        <div class="popup-controls">
-          <van-radio-group
-            v-if="cellType.length > 1"
-            v-model="resClass"
-            direction="horizontal"
-            @change="filterOutList"
-          >
+        <div class="popup-controls w100">
+          <van-radio-group v-if="cellType.length > 1" v-model="resClass" direction="horizontal" @change="filterOutList">
             <van-radio name="">全部</van-radio>
-            <van-radio
-              v-for="item in cellType"
-              :key="item.dictValue"
-              :name="item.dictValue"
-            >
+            <van-radio v-for="item in cellType" :key="item.dictValue" :name="item.dictValue">
               {{ item.dictLabel }}
             </van-radio>
           </van-radio-group>
-          <van-button
-            type="primary"
-            size="small"
-            :loading="autoFlag"
-            @click="autoConfirm"
-          >
+          <van-button type="primary" size="small" :loading="autoFlag" @click="autoConfirm">
             自动确认
           </van-button>
         </div>
@@ -51,20 +30,10 @@
             下个月出室人员(共{{ state.outList.length }}人)
           </div>
 
-          <van-empty
-            v-if="!state.outList.length"
-            image="search"
-            description="暂无出室人员"
-          />
-
-          <van-list
-            v-else
-            finished-text="没有更多了"
-          >
-            <van-cell
-              v-for="item in state.outList"
-              :key="item"
-            >
+          <van-empty v-if="!state.outList.length" image="search" description="暂无出室人员" />
+
+          <van-list v-else finished-text="没有更多了">
+            <van-cell v-for="item in state.outList" :key="item">
               <template #default>
                 <div class="list">
                   <p class="inst-title">
@@ -139,11 +108,7 @@
             待确认队列 (已确认{{ comfirmTotal }}人)
           </div>
 
-          <van-empty
-            v-if="!state.confirmList.length"
-            image="default"
-            description="暂无待确认队列"
-          />
+          <van-empty v-if="!state.confirmList.length" image="default" description="暂无待确认队列" />
           <ul v-else>
             <li v-for="(item, index) in state.confirmList" :key="index">
               <div class="text">
@@ -154,35 +119,15 @@
                 </p>
                 <p>{{ item.pgName }}-{{ item.mentorName }}</p>
               </div>
-              <van-icon
-                v-if="item.checked"
-                name="success"
-                class="checked-icon"
-              />
+              <van-icon v-if="item.checked" name="success" class="checked-icon" />
               <div class="btns">
-                <van-button
-                  v-if="!item.checked"
-                  type="primary"
-                  plain
-                  size="small"
-                  @click="onConfirm(item)"
-                >
+                <van-button v-if="!item.checked" type="primary" plain size="small" @click="onConfirm(item)">
                   确认
                 </van-button>
-                <van-button
-                  v-else
-                  plain
-                  size="small"
-                  @click="item.checked = false"
-                >
+                <van-button v-else plain size="small" @click="item.checked = false">
                   取消
                 </van-button>
-                <van-button
-                  type="primary"
-                  size="small"
-                  plain
-                  @click="onDetail(item)"
-                >
+                <van-button type="primary" size="small" plain @click="onDetail(item)">
                   查看
                 </van-button>
               </div>
@@ -191,19 +136,10 @@
         </div>
       </div>
       <div class="popup-footer">
-        <van-button
-          type="primary"
-          plain
-          block
-          @click="onCancel"
-        >
+        <van-button type="primary" plain block @click="onCancel">
           取 消
         </van-button>
-        <van-button
-          type="primary"
-          block
-          @click="onSubmit"
-        >
+        <van-button type="primary" block @click="onSubmit">
           确 定
         </van-button>
       </div>
@@ -213,427 +149,464 @@
 </template>
 
 <script setup lang="ts" name="systemProDialog">
-  import to from 'await-to-js'
-  import { defineAsyncComponent, reactive, ref, computed } from 'vue'
-  import { showToast, showConfirmDialog } from 'vant'
-  import { usePlatformAppointApi } from '/@/api/platform/appoint'
-  import { usePlatformApi } from '/@/api/platform/home'
-  import { useSystemApi } from '/@/api/platform/system'
-  import { getDictLabel } from '/@/utils/other'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { storeToRefs } from 'pinia'
-  import { useDictApi } from '/@/api/base/system/dict'
-  import { formatDate } from '/@/utils/formatTime'
-
-  const DetailsDialog = defineAsyncComponent(() => import('/@/view/entry/components/details.vue'))
-
-  const stores = useUserInfo()
-  const { userInfos } = storeToRefs(stores)
-
-  // 定义子组件向父组件传值/事件
-  const emit = defineEmits(['refresh'])
-
-  // 定义变量内容
-  const Api = usePlatformAppointApi()
-  const systemApi = useSystemApi()
-  const platformApi = usePlatformApi()
-  const dictApi = useDictApi()
-  const detailsDialog = ref()
-  const userTypeList = ref<RowDicDataType[]>([])
-  const projectGroupList = ref<any[]>([])
-  const cellType = ref<RowDicDataType[]>([])
-
-  const rules = {
-    memberName: { required: true, message: '不能为空', trigger: 'blur' },
-    memberType: { required: true, message: '不能为空', trigger: 'change' },
-    deptId: { required: true, message: '不能为空', trigger: 'change' },
-    platformName: { required: true, message: '不能为空', trigger: 'blur' },
-    platformTime: { required: true, message: '不能为空', trigger: 'blur' },
-    startDate: { required: true, message: '不能为空', trigger: 'change' },
-    endDate: { required: true, message: '不能为空', trigger: 'change' },
-    mentorName: { required: true, message: '不能为空', trigger: 'blur' }
+import to from 'await-to-js'
+import { defineAsyncComponent, reactive, ref, computed } from 'vue'
+import { showToast, showConfirmDialog } from 'vant'
+import { usePlatformAppointApi } from '/@/api/platform/appoint'
+import { usePlatformApi } from '/@/api/platform/home'
+import { useSystemApi } from '/@/api/platform/system'
+import { getDictLabel } from '/@/utils/other'
+import { useUserInfo } from '/@/stores/userInfo'
+import { storeToRefs } from 'pinia'
+import { useDictApi } from '/@/api/base/system/dict'
+import { formatDate } from '/@/utils/formatTime'
+
+const DetailsDialog = defineAsyncComponent(() => import('/@/view/entry/components/details.vue'))
+
+const stores = useUserInfo()
+const { userInfos } = storeToRefs(stores)
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh'])
+
+// 定义变量内容
+const Api = usePlatformAppointApi()
+const systemApi = useSystemApi()
+const platformApi = usePlatformApi()
+const dictApi = useDictApi()
+const detailsDialog = ref()
+const userTypeList = ref<RowDicDataType[]>([])
+const projectGroupList = ref<any[]>([])
+const cellType = ref<RowDicDataType[]>([])
+
+const rules = {
+  memberName: { required: true, message: '不能为空', trigger: 'blur' },
+  memberType: { required: true, message: '不能为空', trigger: 'change' },
+  deptId: { required: true, message: '不能为空', trigger: 'change' },
+  platformName: { required: true, message: '不能为空', trigger: 'blur' },
+  platformTime: { required: true, message: '不能为空', trigger: 'blur' },
+  startDate: { required: true, message: '不能为空', trigger: 'change' },
+  endDate: { required: true, message: '不能为空', trigger: 'change' },
+  mentorName: { required: true, message: '不能为空', trigger: 'blur' }
+}
+const state = reactive({
+  form: {
+    id: 0,
+    deptId: null,
+    deptName: '',
+    isTemporary: '10',
+    memberId: 0,
+    memberName: '',
+    memberPhone: '',
+    memberType: '',
+    memberDeptId: null,
+    memberDeptName: '',
+    mentorObj: null,
+    pgId: null,
+    pgName: '',
+    mentorId: null,
+    mentorName: '',
+    mentorDeptName: '',
+    mentorPhone: '',
+    platformId: null,
+    platformName: '',
+    platformTime: null,
+    platformType: '',
+    isCellChecked: '20',
+    cellType: '',
+    cellSourceType: '',
+    isMolecularChecked: '20',
+    molecularTime: null,
+    platOtherNeed: ''
+  },
+  outList: [] as any[],
+  confirmList: [] as any[],
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: ''
   }
-  const state = reactive({
-    form: {
-      id: 0,
-      deptId: null,
-      deptName: '',
-      isTemporary: '10',
-      memberId: 0,
-      memberName: '',
-      memberPhone: '',
-      memberType: '',
-      memberDeptId: null,
-      memberDeptName: '',
-      mentorObj: null,
-      pgId: null,
-      pgName: '',
-      mentorId: null,
-      mentorName: '',
-      mentorDeptName: '',
-      mentorPhone: '',
-      platformId: null,
-      platformName: '',
-      platformTime: null,
-      platformType: '',
-      isCellChecked: '20',
-      cellType: '',
-      cellSourceType: '',
-      isMolecularChecked: '20',
-      molecularTime: null,
-      platOtherNeed: ''
-    },
-    outList: [] as any[],
-    confirmList: [] as any[],
-    dialog: {
-      isShowDialog: false,
-      type: '',
-      title: '',
-      submitTxt: ''
-    }
-  })
-  const autoFlag = ref(false)
-  const comfirmTotal = computed(() => {
-    return state.confirmList.reduce((pre: number, cur: any) => {
-      if (cur.checked) return pre + 1
-      return pre
-    }, 0)
+})
+const autoFlag = ref(false)
+const comfirmTotal = computed(() => {
+  return state.confirmList.reduce((pre: number, cur: any) => {
+    if (cur.checked) return pre + 1
+    return pre
+  }, 0)
+})
+const outList = ref<any[]>([])
+const confirmList = ref<any[]>([])
+const belongOrgOption = ref<any[]>([])
+const userList = ref<any[]>([])
+const resClass = ref('')
+const platform = ref()
+const getDicts = async () => {
+  Promise.all([
+    systemApi.getDeptTree(),
+    systemApi.getUserList({ noPage: true }),
+    platformApi.getDetail({ id: userInfos.value.platformId }),
+    dictApi.getDictDataByType('sys_user_type'),
+    systemApi.getProjectGroupListForApp({ noPage: true }),
+    userInfos.value.platformId != 0 ? platformApi.getResourceTypeDict({ id: userInfos.value.platformId }) : ''
+  ]).then(([dept, user, plat, type, pjt, cell]) => {
+    belongOrgOption.value = dept?.data || []
+    userList.value = user?.data?.list || []
+    userTypeList.value = type?.data?.values || []
+    projectGroupList.value = pjt?.data?.list || []
+    cellType.value = cell?.data || []
+    platform.value = plat?.data || ''
+    getOutList()
+    getConfirmList()
   })
-  const outList = ref<any[]>([])
-  const confirmList = ref<any[]>([])
-  const belongOrgOption = ref<any[]>([])
-  const userList = ref<any[]>([])
-  const resClass = ref('')
-  const platform = ref()
-  const getDicts = async () => {
-    Promise.all([
-      systemApi.getDeptTree(),
-      systemApi.getUserList({ noPage: true }),
-      platformApi.getDetail({ id: userInfos.value.platformId }),
-      dictApi.getDictDataByType('sys_user_type'),
-      systemApi.getProjectGroupListForApp({ noPage: true }),
-      userInfos.value.platformId != 0 ? platformApi.getResourceTypeDict({ id: userInfos.value.platformId }) : ''
-    ]).then(([dept, user, plat, type, pjt, cell]) => {
-      belongOrgOption.value = dept?.data || []
-      userList.value = user?.data?.list || []
-      userTypeList.value = type?.data?.values || []
-      projectGroupList.value = pjt?.data?.list || []
-      cellType.value = cell?.data || []
-      platform.value = plat?.data || ''
-      getOutList()
-      getConfirmList()
-    })
+}
+// 打开弹窗
+const openDialog = async (type: string, row: number) => {
+  await getDicts()
+  state.dialog.type = type
+  state.dialog.title = '入室资源确认'
+  state.dialog.isShowDialog = true
+}
+const getOutList = async () => {
+  const [err, res]: ToResponse = await to(Api.nextMonthPerson({ id: userInfos.value.platformId, platformType: platform.value.platformType }))
+  if (err) return
+  outList.value = res?.data || []
+  state.outList = JSON.parse(JSON.stringify(outList.value))
+}
+const getConfirmList = async () => {
+  const [err, res]: ToResponse = await to(Api.getConfirmedQueue({ id: userInfos.value.platformId, platformType: platform.value.platformType }))
+  if (err) return
+  confirmList.value = res?.data || []
+  state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
+}
+// 前端筛选类型
+const filterOutList = (val?: string) => {
+  if (val) {
+    state.outList = outList.value.filter((item: any) => item.resClass == val)
+    state.confirmList = confirmList.value.filter((item: any) => item.cellSourceType == val)
+  } else {
+    state.outList = [...outList.value]
+    state.confirmList = [...confirmList.value]
   }
-  // 打开弹窗
-  const openDialog = async (type: string, row: number) => {
-    await getDicts()
-    state.dialog.type = type
-    state.dialog.title = '入室资源确认'
-    state.dialog.isShowDialog = true
-  }
-  const getOutList = async () => {
-    const [err, res]: ToResponse = await to(Api.nextMonthPerson({ id: userInfos.value.platformId, platformType: platform.value.platformType }))
-    if (err) return
-    outList.value = res?.data || []
-    state.outList = JSON.parse(JSON.stringify(outList.value))
-  }
-  const getConfirmList = async () => {
-    const [err, res]: ToResponse = await to(Api.getConfirmedQueue({ id: userInfos.value.platformId, platformType: platform.value.platformType }))
-    if (err) return
-    confirmList.value = res?.data || []
-    state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
-  }
-  // 前端筛选类型
-  const filterOutList = (val?: string) => {
-    if (val) {
-      state.outList = outList.value.filter((item: any) => item.resClass == val)
-      state.confirmList = confirmList.value.filter((item: any) => item.cellSourceType == val)
-    } else {
-      state.outList = [...outList.value]
-      state.confirmList = [...confirmList.value]
-    }
-  }
-  // 确认
-  const onConfirm = (row: any) => {
-    // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
-    const total = outList.value.filter((item: any) => item.resClass == row.cellSourceType).length
+}
+// 确认
+const onConfirm = (row: any) => {
+  // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
+  const total = outList.value.filter((item: any) => item.resClass == row.cellSourceType).length
 
-    if (total === confirmList.value.filter((item: any) => item.cellSourceType == row.cellSourceType && item.checked).length) {
-      showToast({ message: '该类型下已确认人数与出室人员人数一致,无法确认', type: 'text' })
-      return
-    }
-    row.checked = true
-    const obj = confirmList.value.find((item: any) => item.id == row.id)
-    if (obj) obj.checked = true
+  if (total === confirmList.value.filter((item: any) => item.cellSourceType == row.cellSourceType && item.checked).length) {
+    showToast({ message: '该类型下已确认人数与出室人员人数一致,无法确认', type: 'text' })
+    return
   }
-  // 自动确认
-  const autoConfirm = () => {
-    autoFlag.value = true
-    // 按待确认列表顺序依次确认
-    filterOutList()
-    state.confirmList.forEach((item: any) => {
-      item.checked = false
-    })
-    for (const item of cellType.value) {
-      const total = state.outList.filter((i: any) => i.resClass == item.dictValue).length
-      const confirm = state.confirmList.filter((i: any) => i.cellSourceType == item.dictValue)
-      for (let i = 0; i < total; i++) {
-        if (confirm[i]) confirm[i].checked = true
-      }
+  row.checked = true
+  const obj = confirmList.value.find((item: any) => item.id == row.id)
+  if (obj) obj.checked = true
+}
+// 自动确认
+const autoConfirm = () => {
+  autoFlag.value = true
+  // 按待确认列表顺序依次确认
+  filterOutList()
+  state.confirmList.forEach((item: any) => {
+    item.checked = false
+  })
+  for (const item of cellType.value) {
+    const total = state.outList.filter((i: any) => i.resClass == item.dictValue).length
+    const confirm = state.confirmList.filter((i: any) => i.cellSourceType == item.dictValue)
+    for (let i = 0; i < total; i++) {
+      if (confirm[i]) confirm[i].checked = true
     }
-    autoFlag.value = false
-  }
-  // 关闭弹窗
-  const closeDialog = () => {
-    state.outList = []
-    state.confirmList = []
-    outList.value = []
-    confirmList.value = []
-    state.dialog.isShowDialog = false
   }
-  // 取消
-  const onCancel = () => {
-    closeDialog()
-  }
-  // 提交
-  const onSubmit = async () => {
-    // 验证 所有下个月出室人员 是否全部确认 二次确认
-    const total = state.outList.length
-    const arr = state.confirmList.filter((item: any) => item.checked)
-    // 获取对应下月出室人员后端标记 不再显示在列表中
-    const outArr: number[] = []
-    for (const item of cellType.value) {
-      const confirm = arr.filter((i: any) => i.cellSourceType == item.dictValue)
-      const out = state.outList.filter((i: any) => i.resClass == item.dictValue)
-      for (let i = 0; i < confirm.length; i++) {
-        if (out[i]) outArr.push(out[i].id)
-      }
+  autoFlag.value = false
+}
+// 关闭弹窗
+const closeDialog = () => {
+  state.outList = []
+  state.confirmList = []
+  outList.value = []
+  confirmList.value = []
+  state.dialog.isShowDialog = false
+}
+// 取消
+const onCancel = () => {
+  closeDialog()
+}
+// 提交
+const onSubmit = async () => {
+  // 验证 所有下个月出室人员 是否全部确认 二次确认
+  const total = state.outList.length
+  const arr = state.confirmList.filter((item: any) => item.checked)
+  // 获取对应下月出室人员后端标记 不再显示在列表中
+  const outArr: number[] = []
+  for (const item of cellType.value) {
+    const confirm = arr.filter((i: any) => i.cellSourceType == item.dictValue)
+    const out = state.outList.filter((i: any) => i.resClass == item.dictValue)
+    for (let i = 0; i < confirm.length; i++) {
+      if (out[i]) outArr.push(out[i].id)
     }
-    if (arr.length < total) {
-      try {
-        await showConfirmDialog({
-          title: '提示',
-          message: '下个月出室人员未全部确认,是否继续提交?',
-          confirmButtonText: '确定',
-          cancelButtonText: '取消'
-        })
-      } catch (error) {
-        return
-      }
-    }
-    const [err]: ToResponse = await to(
-      Api.markNextMonthEntry({ ids: arr.map((item: any) => item.id), assignIds: outArr, platformType: platform.value.platformType })
-    )
-    if (err) return
-    showToast({ message: '操作成功', type: 'success' })
-    emit('refresh')
-    closeDialog()
   }
-  const onDetail = (val: number) => {
-    detailsDialog.value.openDialog('get', val)
+  if (arr.length < total) {
+    try {
+      await showConfirmDialog({
+        title: '提示',
+        message: '下个月出室人员未全部确认,是否继续提交?',
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      })
+    } catch (error) {
+      return
+    }
   }
-  // 暴露变量
-  defineExpose({
-    openDialog
-  })
+  const [err]: ToResponse = await to(
+    Api.markNextMonthEntry({ ids: arr.map((item: any) => item.id), assignIds: outArr, platformType: platform.value.platformType })
+  )
+  if (err) return
+  showToast({ message: '操作成功', type: 'success' })
+  emit('refresh')
+  closeDialog()
+}
+const onDetail = (val: number) => {
+  detailsDialog.value.openDialog('get', val)
+}
+// 暴露变量
+defineExpose({
+  openDialog
+})
 </script>
 <style lang="scss" scoped>
-  .facilities-dialog-container {
-    .facilities-popup {
-      display: flex;
-      flex-direction: column;
-      height: 100%;
-      padding: 16px;
-      box-sizing: border-box;
+.facilities-dialog-container {
+  .facilities-popup {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    padding: 16px;
+    box-sizing: border-box;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 16px;
+    gap: 16px;
+    flex-wrap: wrap;
+
+    .popup-title {
+      font-size: 18px;
+      font-weight: 600;
+      color: #1f2937;
     }
-    .popup-header {
+
+    .popup-controls {
       display: flex;
-      justify-content: space-between;
-      align-items: flex-start;
-      margin-bottom: 16px;
-      gap: 16px;
+      align-items: center;
+      gap: 12px;
       flex-wrap: wrap;
-      .popup-title {
-        font-size: 18px;
-        font-weight: 600;
-        color: #1f2937;
-      }
-      .popup-controls {
+
+      .van-radio-group {
         display: flex;
         align-items: center;
-        gap: 12px;
-        flex-wrap: wrap;
-        .van-radio-group {
-          display: flex;
-          align-items: center;
-          gap: 8px;
-          padding: 6px 10px;
-          background: #f5f7fb;
-          border-radius: 999px;
-          .van-radio__label {
-            color: #374151;
-            font-size: 13px;
-          }
+        gap: 8px;
+        padding: 6px 10px;
+        background: #f5f7fb;
+        border-radius: 999px;
+
+        .van-radio__label {
+          color: #374151;
+          font-size: 13px;
+        }
+      }
+
+      .van-button {
+        min-width: 88px;
+      }
+    }
+  }
+
+  .table-container {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .info-banner {
+      padding: 10px 14px;
+      border-radius: 6px;
+      font-size: 14px;
+      font-weight: 500;
+      margin-bottom: 12px;
+
+      &.warning {
+        background: linear-gradient(90deg, rgba(255, 207, 145, 0.2), rgba(255, 237, 204, 0.6));
+        color: #b45309;
+      }
+    }
+
+    .out-table,
+    .confirm-table {
+      background: #fff;
+      border-radius: 10px;
+      padding: 16px;
+      box-shadow: 0 4px 16px rgba(31, 41, 55, 0.08);
+
+      .van-empty {
+        padding: 24px 0;
+
+        .van-empty__image {
+          width: 120px;
         }
-        .van-button {
-          min-width: 88px;
+
+        .van-empty__description {
+          color: #6b7280;
+          font-size: 13px;
         }
       }
     }
-    .table-container {
-      flex: 1;
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-      gap: 16px;
-      .info-banner {
-        padding: 10px 14px;
-        border-radius: 6px;
-        font-size: 14px;
-        font-weight: 500;
-        margin-bottom: 12px;
-        &.warning {
-          background: linear-gradient(90deg, rgba(255, 207, 145, 0.2), rgba(255, 237, 204, 0.6));
-          color: #b45309;
+
+    .out-table {
+      .van-cell {
+        background: transparent;
+
+        &:not(:last-child)::after {
+          left: 16px;
+          right: 16px;
         }
       }
-      .out-table,
-      .confirm-table {
-        background: #fff;
-        border-radius: 10px;
-        padding: 16px;
-        box-shadow: 0 4px 16px rgba(31, 41, 55, 0.08);
-        .van-empty {
-          padding: 24px 0;
-          .van-empty__image {
-            width: 120px;
+
+      .list {
+        display: flex;
+        flex-direction: column;
+        gap: 6px;
+        padding: 6px 4px;
+
+        .inst-title {
+          display: flex;
+          justify-content: space-between;
+          color: #4b5563;
+          font-size: 13px;
+
+          span:first-child {
+            color: #9ca3af;
           }
-          .van-empty__description {
-            color: #6b7280;
-            font-size: 13px;
+
+          .title {
+            color: #111827;
+            font-weight: 500;
           }
         }
       }
-      .out-table {
-        .van-cell {
-          background: transparent;
-          &:not(:last-child)::after {
-            left: 16px;
-            right: 16px;
-          }
+    }
+
+    .confirm-table {
+      display: flex;
+      flex-direction: column;
+
+      ul {
+        flex: 1;
+        max-height: 440px;
+        overflow-y: auto;
+        padding-right: 4px;
+
+        &::-webkit-scrollbar {
+          width: 6px;
         }
-        .list {
-          display: flex;
-          flex-direction: column;
-          gap: 6px;
-          padding: 6px 4px;
-          .inst-title {
-            display: flex;
-            justify-content: space-between;
-            color: #4b5563;
-            font-size: 13px;
-            span:first-child {
-              color: #9ca3af;
-            }
-            .title {
-              color: #111827;
-              font-weight: 500;
-            }
-          }
+
+        &::-webkit-scrollbar-thumb {
+          background-color: rgba(156, 163, 175, 0.4);
+          border-radius: 999px;
         }
-      }
-      .confirm-table {
-        display: flex;
-        flex-direction: column;
-        ul {
-          flex: 1;
-          max-height: 440px;
-          overflow-y: auto;
-          padding-right: 4px;
-          &::-webkit-scrollbar {
-            width: 6px;
+
+        li {
+          display: flex;
+          align-items: flex-start;
+          gap: 12px;
+          border-radius: 8px;
+          border: 1px solid #e5e7eb;
+          padding: 12px;
+          transition: border-color 0.2s ease, box-shadow 0.2s ease;
+
+          &:hover {
+            border-color: #a3bffa;
+            box-shadow: 0 8px 16px rgba(79, 70, 229, 0.08);
           }
-          &::-webkit-scrollbar-thumb {
-            background-color: rgba(156, 163, 175, 0.4);
-            border-radius: 999px;
+
+          &+li {
+            margin-top: 10px;
           }
-          li {
+
+          .text {
+            flex: 1;
+            overflow: hidden;
             display: flex;
-            align-items: flex-start;
-            gap: 12px;
-            border-radius: 8px;
-            border: 1px solid #e5e7eb;
-            padding: 12px;
-            transition: border-color 0.2s ease, box-shadow 0.2s ease;
-            &:hover {
-              border-color: #a3bffa;
-              box-shadow: 0 8px 16px rgba(79, 70, 229, 0.08);
-            }
-            & + li {
-              margin-top: 10px;
-            }
-            .text {
-              flex: 1;
+            flex-direction: column;
+            gap: 4px;
+
+            p {
+              white-space: nowrap;
               overflow: hidden;
-              display: flex;
-              flex-direction: column;
-              gap: 4px;
-              p {
-                white-space: nowrap;
-                overflow: hidden;
-                text-overflow: ellipsis;
-                color: #374151;
-                font-size: 14px;
-              }
-              p:first-child {
-                font-weight: 600;
-              }
+              text-overflow: ellipsis;
+              color: #374151;
+              font-size: 14px;
             }
-            .checked-icon {
-              color: #10b981;
-              font-size: 20px;
-              flex-shrink: 0;
-              margin-top: 2px;
+
+            p:first-child {
+              font-weight: 600;
             }
-            .btns {
-              display: flex;
-              flex-direction: column;
-              gap: 8px;
-              min-width: 100px;
-              .van-button {
-                font-size: 12px;
-                padding: 0 12px;
-                height: 28px;
-              }
+          }
+
+          .checked-icon {
+            color: #10b981;
+            font-size: 20px;
+            flex-shrink: 0;
+            margin-top: 2px;
+          }
+
+          .btns {
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+            min-width: 100px;
+
+            .van-button {
+              font-size: 12px;
+              padding: 0 12px;
+              height: 28px;
             }
           }
         }
       }
     }
-    .popup-footer {
-      display: flex;
-      gap: 12px;
-      margin-top: auto;
-      .van-button {
-        height: 40px;
-        font-size: 15px;
-        font-weight: 600;
-      }
+  }
+
+  .popup-footer {
+    display: flex;
+    gap: 12px;
+    margin-top: auto;
+
+    .van-button {
+      height: 40px;
+      font-size: 15px;
+      font-weight: 600;
     }
   }
-  @media (min-width: 768px) {
-    .facilities-dialog-container {
-      .table-container {
-        flex-direction: row;
-        align-items: stretch;
-        .out-table,
-        .confirm-table {
-          flex: 1;
-        }
-        .confirm-table ul {
-          max-height: unset;
-        }
+}
+
+@media (min-width: 768px) {
+  .facilities-dialog-container {
+    .table-container {
+      flex-direction: row;
+      align-items: stretch;
+
+      .out-table,
+      .confirm-table {
+        flex: 1;
+      }
+
+      .confirm-table ul {
+        max-height: unset;
       }
     }
   }
+}
 </style>

+ 9 - 6
src/view/instr/detail.vue

@@ -160,7 +160,7 @@
       finished-text="没有更多了"
       @load="onLoad"
     >
-      <van-cell v-for="item in state.list">
+      <van-cell v-for="item in state.list" :key="item.id">
         <template #default>
           <div class="list">
             <header class="flex justify-between">
@@ -588,7 +588,8 @@
       }
     }
     > header {
-      height: 80px;
+      height: auto;
+      min-height: 80px;
       background-color: #fff;
       padding: 12px;
     }
@@ -598,7 +599,8 @@
     .i-right {
       flex: 1;
       font-size: 14px;
-      height: 80px;
+      height: auto;
+      min-height: 80px;
       .i-r-icon {
         width: 15px;
         height: 15px;
@@ -608,9 +610,10 @@
     .detailTxt {
       font-size: 12px;
       color: #333333;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
+      white-space: normal;
+      overflow: visible;
+      text-overflow: unset;
+      word-break: break-all;
       &.name {
         font-weight: bold;
         font-size: 16px;

+ 2 - 2
src/view/todo/component/instrument_appointment.vue

@@ -2,7 +2,7 @@
   <div class="cert-dialog-container">
     <van-cell-group>
       <van-cell title="仪器名称" title-class="cell-title" :value="state.form.instName" />
-      <van-cell title="所属平台" title-class="cell-title" :value="state.form.placeAddress" />
+      <van-cell title="所属平台" title-class="cell-title" :value="state.form.platformName" />
       <van-cell title="仪器类型" title-class="cell-title" :value="state.form.instClassDesc" />
       <van-cell title="计费方式" title-class="cell-title" :value="state.form.costType" />
       <van-cell title="预约人" title-class="cell-title" :value="state.form.userName" />
@@ -32,7 +32,7 @@
   const state = reactive({
     form: {
       instName: '',
-      placeAddress: '',
+      platformName: '',
       instClassDesc: '',
       costType: '',
       appointeeInstrumentName: '',

+ 138 - 124
src/view/todo/index.vue

@@ -33,7 +33,7 @@
                   </p>
 
                   <p>
-                    <van-text-ellipsis :content="item.candidate" />
+                    <van-text-ellipsis :content="item.candidate" class="left" />
                   </p>
                 </div>
 
@@ -53,145 +53,159 @@
 </template>
 
 <script name="home" lang="ts" setup>
-  import to from 'await-to-js'
-  import { showNotify } from 'vant'
-  import { onMounted, reactive, ref, defineAsyncComponent } from 'vue'
-  import { useRouter, useRoute } from 'vue-router'
-
-  import { formatDate } from '/@/utils/formatTime'
-  import { useExecutionApi } from '/@/api/execution'
-  import { useNewsApi } from '/@/api/system/news'
-
-  const ApprovalTasksForm = defineAsyncComponent(() => import('/@/view/todo/component/approveTasks.vue'))
-
-  const approvalTasksForm = ref()
-
-  const executionApi = useExecutionApi()
-  const newsApi = useNewsApi()
-  const router = useRouter()
-  const route = useRoute()
-  const state = reactive({
-    status: 'approval',
-    queryParams: {
-      platformId: 1000103,
-      pageNum: 1,
-      pageSize: 10
-    },
-    finished: false,
-    loading: true,
-    list: [] as any[]
-  })
-  const checkedList = ref([])
+import to from 'await-to-js'
+import { showNotify } from 'vant'
+import { onMounted, reactive, ref, defineAsyncComponent } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 
-  const onClickRight = () => {
-    router.go(-1)
-  }
-  const changeType = (str: string) => {
-    state.list = []
-    checkedList.value = []
-    state.queryParams.pageNum = 1
-    onLoad()
+import { formatDate } from '/@/utils/formatTime'
+import { useExecutionApi } from '/@/api/execution'
+import { useNewsApi } from '/@/api/system/news'
+
+const ApprovalTasksForm = defineAsyncComponent(() => import('/@/view/todo/component/approveTasks.vue'))
+
+const approvalTasksForm = ref()
+
+const executionApi = useExecutionApi()
+const newsApi = useNewsApi()
+const router = useRouter()
+const route = useRoute()
+const state = reactive({
+  status: 'approval',
+  queryParams: {
+    platformId: 1000103,
+    pageNum: 1,
+    pageSize: 10
+  },
+  finished: false,
+  loading: true,
+  list: [] as any[]
+})
+const checkedList = ref([])
+
+const onClickRight = () => {
+  router.go(-1)
+}
+const changeType = (str: string) => {
+  state.list = []
+  checkedList.value = []
+  state.queryParams.pageNum = 1
+  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))
   }
-  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))
-    }
-    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
-    }
+  if (err) return
+  const list = res?.data?.list || []
+  for (const item of list) {
+    state.list.push(item)
   }
-
-  const batchApprovalSuccessCallback = () => {
-    state.list = []
-    checkedList.value = []
-    state.queryParams.pageNum = 1
-    onLoad()
+  state.loading = false
+  state.queryParams.pageNum++
+  if (list.length < state.queryParams.pageSize) {
+    state.finished = true
   }
+}
 
-  const handleBatchApproval = () => {
-    let ids = checkedList.value.map((item) => ({
-      taskId: item.taskId,
-      relId: item.relId
-    }))
+const batchApprovalSuccessCallback = () => {
+  state.list = []
+  checkedList.value = []
+  state.queryParams.pageNum = 1
+  onLoad()
+}
 
-    if (ids.length == 0) {
-      showNotify({ type: 'warning', message: '请选择要审批的任务' })
-      return
-    }
+const handleBatchApproval = () => {
+  let ids = checkedList.value.map((item) => ({
+    taskId: item.taskId,
+    relId: item.relId
+  }))
 
-    approvalTasksForm.value.openDialog(ids)
+  if (ids.length == 0) {
+    showNotify({ type: 'warning', message: '请选择要审批的任务' })
+    return
   }
 
-  const toDetail = (id: number) => {
-    router.push({
-      path: '/todo/detail',
-      query: {
-        id,
-        type: state.status
-      }
-    })
-  }
-  onMounted(() => {
-    onLoad()
+  approvalTasksForm.value.openDialog(ids)
+}
+
+const toDetail = (id: number) => {
+  router.push({
+    path: '/todo/detail',
+    query: {
+      id,
+      type: state.status
+    }
   })
+}
+onMounted(() => {
+  onLoad()
+})
 </script>
 
 <style lang="scss" scoped>
-  .app-container {
-    padding: 0 !important;
-    .list-container {
-      padding: 0 10px;
+.app-container {
+  padding: 0 !important;
+
+  .list-container {
+    padding: 0 10px;
+  }
+
+  .van-row .van-button {
+    flex: 1;
+
+    &+.van-button {
+      margin-left: 10px;
     }
-    .van-row .van-button {
-      flex: 1;
-      & + .van-button {
-        margin-left: 10px;
+  }
+
+  .van-list {
+    height: calc(100% - 20px);
+    border-radius: 4px;
+
+    .van-cell {
+      background-color: #fff;
+      margin-top: 10px;
+
+      header {
+        color: #333;
+        font-size: 16px;
       }
-    }
-    .van-list {
-      height: calc(100% - 20px);
-      border-radius: 4px;
-      .van-cell {
-        background-color: #fff;
-        margin-top: 10px;
-        header {
-          color: #333;
-          font-size: 16px;
-        }
-        footer {
-          color: #969799;
-          margin-top: 4px;
-        }
-        .title {
-          font-weight: bold;
-        }
-        .inst-title {
-          color: #333;
-          text-align: left;
-          flex: 1;
-          overflow: hidden;
-          white-space: nowrap;
-          text-overflow: ellipsis;
-          margin-top: 4px;
-        }
-        .time {
-          color: #f69a4d;
-        }
+
+      footer {
+        color: #969799;
+        margin-top: 4px;
+      }
+
+      .title {
+        font-weight: bold;
+      }
+
+      .inst-title {
+        color: #333;
+        text-align: left;
+        flex: 1;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-top: 4px;
+      }
+
+      .time {
+        color: #f69a4d;
       }
     }
   }
+}
+
+.left {
+  text-align: left;
+}
 </style>

+ 1 - 1
src/view/training/index.vue

@@ -78,7 +78,7 @@ const onLoad = async () => {
   state.loading = false
 }
 const onEnroll = (row: any) => {
-  if (row.status === '40' && row.applyStatus === '10') {
+  if (row.status === '40' && props.isAll === '10') {
     showNotify({
       type: 'warning',
       message: '该培训已结束'