Browse Source

fix:入室问题修复

liuzhenlin 4 months ago
parent
commit
62842c0022

+ 4 - 0
src/types/index.d.ts

@@ -62,3 +62,7 @@ declare interface RowTraningApplyType {
   createdTime: string
   applyStatus: string
 }
+
+declare interface RowTraningApplyTypeItem extends RowTraningApplyType {
+  status: string
+}

+ 21 - 19
src/utils/url.ts

@@ -8,21 +8,23 @@
  * @returns 完整的图片URL
  */
 export function getImageUrl(url: string): string {
-  if (!url) return ''
-  
-  // 如果已经包含http或https协议,直接返回
-  if (url.startsWith('http://') || url.startsWith('https://')) {
-    return url
-  }
-  
-  // 获取环境变量中的图片基础URL
-  const baseUrl = import.meta.env.VITE_IMAGE_BASE_URL || ''
-  
-  // 确保baseUrl不以/结尾,url以/开头
-  const cleanBaseUrl = baseUrl.replace(/\/$/, '')
-  const cleanUrl = url.startsWith('/') ? url : `/${url}`
-  
-  return `${cleanBaseUrl}${cleanUrl}`
+  // 图片是相对路径不需要拼 
+  return url
+  // if (!url) return ''
+
+  // // 如果已经包含http或https协议,直接返回
+  // if (url.startsWith('http://') || url.startsWith('https://')) {
+  //   return url
+  // }
+
+  // // 获取环境变量中的图片基础URL
+  // const baseUrl = import.meta.env.VITE_IMAGE_BASE_URL || ''
+
+  // // 确保baseUrl不以/结尾,url以/开头
+  // const cleanBaseUrl = baseUrl.replace(/\/$/, '')
+  // const cleanUrl = url.startsWith('/') ? url : `/${url}`
+
+  // return `${cleanBaseUrl}${cleanUrl}`
 }
 
 /**
@@ -33,18 +35,18 @@ export function getImageUrl(url: string): string {
  */
 export function getResourceUrl(url: string, baseUrlKey: string = 'VITE_IMAGE_BASE_URL'): string {
   if (!url) return ''
-  
+
   // 如果已经包含http或https协议,直接返回
   if (url.startsWith('http://') || url.startsWith('https://')) {
     return url
   }
-  
+
   // 获取环境变量中的基础URL
   const baseUrl = (import.meta.env as any)[baseUrlKey] || ''
-  
+
   // 确保baseUrl不以/结尾,url以/开头
   const cleanBaseUrl = baseUrl.replace(/\/$/, '')
   const cleanUrl = url.startsWith('/') ? url : `/${url}`
-  
+
   return `${cleanBaseUrl}${cleanUrl}`
 }

+ 576 - 436
src/view/entry/components/allocate.vue

@@ -1,71 +1,69 @@
 <template>
-  <div class="facilities-dialog-container">
-    <el-dialog
-      :title="state.dialog.title"
+  <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"
-      :close-on-click-modal="false"
-      v-model="state.dialog.isShowDialog"
-      width="90%"
     >
-      <header class="mb8">
-        <span>资源分配</span>
-        <el-form
-          inline
-          size="mini"
-          label-width="0"
-        >
-          <el-form-item
-            label="资源组"
-            label-width="60px"
+      <header class="popup-header">
+        <div class="popup-title">资源分配</div>
+        <div class="popup-controls">
+          <div
+            class="group-selector"
+            @click="groupPickerVisible = true"
           >
-            <el-select
-              v-model="groupId"
+            <span class="label">资源组</span>
+            <van-field
+              :model-value="groupLabel"
+              readonly
+              is-link
+              input-align="right"
               placeholder="全部资源组"
-              clearable
-              style="width: 120px"
-              @change="filterOutListByGroupId"
-            >
-              <el-option
-                v-for="item in molecularGroupList"
-                :key="item.id"
-                :label="item.groupName"
-                :value="item.id"
-              ></el-option>
-            </el-select>
-          </el-form-item>
-          <el-form-item class="ml20">
-            <el-radio-group
-              v-model="resClass"
-              size="small"
-              @change="filterOutList"
+              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>
+          <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"
             >
-              <el-radio label="">全部</el-radio>
-              <el-radio
-                v-for="item in cellType"
-                :key="item.dictValue"
-                :label="item.dictValue"
-              >
-                {{ item.dictLabel }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <!-- <el-form-item class="ml12">
-						<el-button color="#2c78ff" @click="onSubmit('20')" size="default">分配</el-button>
-					</el-form-item> -->
-        </el-form>
+              {{ item.dictLabel }}
+            </van-radio>
+          </van-radio-group>
+        </div>
       </header>
-      <div class="table-container">
-        <div class="confirm-table">
-          <el-alert
-            :title="`待分配队列(已确认${comfirmTotal}人)`"
-            type="warning"
-            :closable="false"
-            class="mb8"
-          />
-          <el-empty
-            v-if="state.confirmList.length == 0"
+      <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="暂无队列数据"
-          ></el-empty>
+          />
           <ul v-else>
             <li
               v-for="(item, index) in state.confirmList"
@@ -73,13 +71,12 @@
               :class="{ active: index === state.active }"
               @click="state.active = index"
             >
-              <!-- <el-checkbox v-model="item.checked" class="mr8"></el-checkbox> -->
               <div class="text">
                 <p>
                   分配房间:
-                  <span class="assignRoom">{{ item.resName }}</span>
+                  <span class="assign-room">{{ item.resName || '未分配' }}</span>
                 </p>
-                <p class="mb4 mt4">
+                <p>
                   {{ item.memberName }}
                   【{{ formatDate(new Date(item.appointStartDate), 'YYYY-mm-dd') }}~{{
                     formatDate(new Date(item.appointEndDate), 'mm-dd')
@@ -91,175 +88,123 @@
                   }}
                 </p>
               </div>
-              <!-- <el-icon v-if="item.checked">
-								<ele-Select />
-							</el-icon> -->
               <div class="btns">
-                <el-button
+                <button
                   v-if="item.resId"
-                  link
-                  type="primary"
-                  @click="cancelAllocate(item)"
+                  class="link-btn danger"
+                  type="button"
+                  @click.stop="cancelAllocate(item)"
                 >
                   取消
-                </el-button>
-                <el-button
-                  link
-                  type="primary"
-                  @click="onDetail(item)"
+                </button>
+                <button
+                  class="link-btn"
+                  type="button"
+                  @click.stop="onDetail(item)"
                 >
                   查看
-                </el-button>
+                </button>
               </div>
             </li>
           </ul>
-        </div>
-        <div class="out-table mr12">
-          <el-alert
-            :title="`资源列表(共${state.resourceList.length}个)`"
-            type="warning"
-            :closable="false"
-            class="mb8"
+        </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 class="out-table-wrap">
-            <el-row
-              :gutter="20"
-              class="out-table-wrap-row"
+          <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)"
             >
-              <!-- 资源列表 -->
-              <el-col
-                :span="24"
-                class="resourceList-col"
-              >
-                <el-row
-                  :gutter="10"
-                  v-if="state.resourceList.length"
+              <div class="resource-header">
+                <span class="resource-name">{{ item.resName }}</span>
+                <van-tag
+                  type="primary"
+                  plain
                 >
-                  <el-col
-                    :span="8"
-                    v-for="item in state.resourceList"
-                    :key="item.id"
-                    class="mb8"
-                    @click="onSelectResource(item)"
-                  >
-                    <div
-                      class="items"
-                      :class="{ active: curSelectedResourceId === item.id }"
-                    >
-                      <header>
-                        <div class="title">
-                          <span class="mr8">{{ item.resName }}</span>
-                          <!-- <el-checkbox v-model="item.checked" class="mr8"></el-checkbox> -->
-                        </div>
-                        <div class="btns">
-                          <el-tag size="small">{{ getDictLabel(cellType, item.resClass) }}</el-tag>
-                        </div>
-                      </header>
-                      <p>{{ item.resLocation }}</p>
-                      <footer>
-                        <p>{{ item.usingSource || 0 }}/{{ item.maxNum }}</p>
-                        <ul>
-                          <li
-                            v-for="(p, index) in item.usedList"
-                            :key="index"
-                            :class="getUsedClass(p)"
-                          ></li>
-                        </ul>
-                      </footer>
-                    </div>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
+                  {{ getDictLabel(cellType, item.resClass) }}
+                </van-tag>
+              </div>
+              <p class="resource-location">{{ item.resLocation }}</p>
+              <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>
+                </ul>
+              </div>
+            </div>
           </div>
-        </div>
-        <div class="use-table out-table">
-          <el-alert
-            title="房间使用情况"
-            type="warning"
-            :closable="false"
-            class="mb8"
-          />
-          <div class="out-table-wrap">
-            <el-row
-              :gutter="20"
-              class="out-table-wrap-row"
-            >
-              <!-- 资源列表 -->
-              <el-col
-                :span="24"
-                class="allocation-situation-col"
+        </section>
+        <section class="card usage-card">
+          <div class="info-banner warning">房间使用情况</div>
+          <div class="usage-list">
+            <ul class="position-choose">
+              <li
+                v-for="(p, index) in allocationSituationList"
+                :key="index"
               >
-                <!-- 资源分配情况页面 -->
-                <div class="allocation-situation">
-                  <ul class="position-choose">
-                    <li
-                      v-for="(p, index) in allocationSituationList"
-                      :key="index"
-                      class="flex"
-                    >
-                      <i :class="getUsedClass(p)"></i>
-                      <!-- <span v-if="p.userObj"
-													>{{ p.userObj.memberName }}【{{ formatDate(new Date(new Date(p.userObj.appointStartDate)), 'YYYY-mm-dd') }}~{{
-														formatDate(new Date(new Date(p.userObj.appointEndDate)), 'mm-dd')
-													}}】</span
-												>
-												<span v-else-if="p.userName"
-													>{{ p.userName }}【{{ formatDate(new Date(p.startTime), 'YYYY-mm-dd') }}~{{
-														formatDate(new Date(p.endTime), 'mm-dd')
-													}}】</span
-												> -->
-                      <div class="txt-flex">
-                        <span
-                          v-if="p.userObj"
-                          :title="p.userObj.memberName"
-                        >
-                          {{ p.userObj.memberName }}
-                        </span>
-                        <span
-                          v-else-if="p.userName"
-                          :title="p.userName"
-                        >
-                          {{ p.userName }}
-                        </span>
-                        <span v-else>空闲</span>
-                      </div>
-                      <el-button
-                        v-if="!p.userObj && (p.assignStatus == '10' || p.assignStatus == '40')"
-                        link
-                        type="primary"
-                        @click="onAllocate(p)"
-                      >
-                        分配
-                      </el-button>
-                    </li>
-                  </ul>
+                <i :class="getUsedClass(p)"></i>
+                <div class="txt-flex">
+                  <span
+                    v-if="p.userObj"
+                    :title="p.userObj.memberName"
+                  >
+                    {{ p.userObj.memberName }}
+                  </span>
+                  <span
+                    v-else-if="p.userName"
+                    :title="p.userName"
+                  >
+                    {{ p.userName }}
+                  </span>
+                  <span v-else>空闲</span>
                 </div>
-              </el-col>
-            </el-row>
+                <van-button
+                  v-if="!p.userObj && (p.assignStatus == '10' || p.assignStatus == '40')"
+                  type="primary"
+                  size="mini"
+                  plain
+                  @click.stop="onAllocate(p)"
+                >
+                  分配
+                </van-button>
+              </li>
+            </ul>
           </div>
-        </div>
+        </section>
       </div>
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button
-            type="info"
-            @click="onCancel"
-            size="default"
-          >
-            取 消
-          </el-button>
-          <!-- <el-button type="success" @click="handelBatchAssign()" size="default">一键分配</el-button> -->
-          <el-button
-            color="#2c78ff"
-            @click="onSubmit('20')"
-            size="default"
-          >
-            确 定
-          </el-button>
-        </span>
-      </template>
-    </el-dialog>
+      <div class="popup-footer">
+        <van-button
+          type="primary"
+          plain
+          block
+          @click="onCancel"
+        >
+          取 消
+        </van-button>
+        <van-button
+          type="primary"
+          block
+          @click="onSubmit('20')"
+        >
+          确 定
+        </van-button>
+      </div>
+    </van-popup>
     <DetailsDialog ref="detailsDialog" />
   </div>
 </template>
@@ -267,8 +212,8 @@
 <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 { ElMessage, ElMessageBox } from 'element-plus'
   import { usePlatformApi } from '/@/api/platform/home'
   import { useSystemApi } from '/@/api/platform/system'
   import { useUserInfo } from '/@/stores/userInfo'
@@ -291,6 +236,7 @@
   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()
@@ -338,15 +284,44 @@
       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(null) // 资源组ID
+  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(),
@@ -370,19 +345,20 @@
     molecularGroupList.value = res?.data || []
   }
   const getResourceList = async () => {
-	console.log(userInfos.value, 'dsaddada')
     const [err, res]: ToResponse = await to(
       platformApi.getResourceList({ platformId: userInfos.value.platformId, resStatus: '10' }),
     )
     if (err) return
-    resourceList.value = res?.data?.list || []
+    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
-    confirmList.value = res?.data || []
+    originalConfirmList.value = JSON.parse(JSON.stringify(res?.data || []))
+    confirmList.value = JSON.parse(JSON.stringify(originalConfirmList.value))
     state.confirmList = JSON.parse(JSON.stringify(confirmList.value))
   }
   // 打开弹窗
@@ -408,7 +384,7 @@
   // 前端筛选资源组
   const filterOutListByGroupId = (val: string) => {
     if (val) {
-      state.resourceList = resourceList.value.filter((item: any) => item.groupId == val)
+      state.resourceList = resourceList.value.filter((item: any) => String(item.groupId) === val)
     } else {
       state.resourceList = [...resourceList.value]
     }
@@ -423,11 +399,19 @@
     // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
   }
   // 关闭弹窗
+  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 = () => {
-    state.resourceList = []
-    state.confirmList = []
-    resourceList.value = []
-    confirmList.value = []
+    resetDialogState()
     state.dialog.isShowDialog = false
   }
   // 取消
@@ -439,20 +423,21 @@
     const checkedUser = state.confirmList.filter((item: any) => item.checked)
     console.log(checkedUser)
     if (!checkedUser.length) {
-      ElMessage.warning('请选择待分配队列人员')
+      showToast({ message: '请选择待分配队列人员', type: 'text' })
       return
     }
-    ElMessageBox.confirm(`是否将选中队列人员一键分配资源?`, '提示', {
+    showConfirmDialog({
+      title: '提示',
+      message: '是否将选中队列人员一键分配资源?',
       confirmButtonText: '确认',
       cancelButtonText: '取消',
-      type: 'warning',
     })
       .then(async () => {
         const [err]: ToResponse = await to(
           platformApi.batchAssign({ appointIds: checkedUser.map((item: any) => item.id) }),
         )
         if (err) return
-        ElMessage.warning('一键分配成功')
+        showToast({ message: '一键分配成功', type: 'success' })
         getResourceList()
         getConfirmList()
       })
@@ -462,7 +447,7 @@
   const onSubmit = async (type: string) => {
     const arr = confirmList.value.filter((item: any) => item.resId)
     if (!arr.length) {
-      ElMessage.warning('请分配资源')
+      showToast({ message: '请分配资源', type: 'text' })
       return
     }
 
@@ -481,7 +466,7 @@
       }),
     )
     if (err) return
-    ElMessage.success('操作成功')
+    showToast({ message: '操作成功', type: 'success' })
     emit('refresh')
     closeDialog()
   }
@@ -491,13 +476,21 @@
   // 分配
   const onAllocate = (pos: any) => {
     if (state.active < 0) {
-      ElMessage.warning('请先选择一个待分配人员')
+      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) {
-      ElMessage.warning('请选择正确的资源')
+      showToast({ message: '请选择正确的资源', type: 'text' })
       return
     }
     activeObj.resId = room.id
@@ -516,19 +509,37 @@
   }
   // 取消分配
   const cancelAllocate = (obj: any) => {
-    for (const room of resourceList.value) {
-      if (room.id == obj.resId) {
-        const used = room.usedList.find((i: any) => {
-          return i.location == obj.location
-        })
-        used.userObj = null
-      }
+    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 (Array.isArray(allocationSituationList.value) && allocationSituationList.value.length) {
+      const usage = allocationSituationList.value.find((item: any) => item.location === location)
+      if (usage) usage.userObj = null
     }
-    const user = confirmList.value.find((item) => item.resId == obj.resId)
-    user.resId = null
-    user.resName = null
-    user.location = null
-    user.replaceId = 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
+    }
+
+    obj.resId = null
+    obj.resName = null
+    obj.location = null
+    obj.replaceId = null
+
     filterOutList(resClass.value)
   }
   const getUsedClass = (row: any) => {
@@ -551,238 +562,367 @@
   })
 </script>
 <style lang="scss" scoped>
-  h4 {
-    font-size: 18px;
-  }
-
-  header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 32px;
-    line-height: 32px;
-
-    .el-form.el-form--inline .el-form-item {
-      margin: 0;
+  .allocate-popup-container {
+    .allocate-popup {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      padding: 16px;
+      box-sizing: border-box;
     }
-  }
-
-  .table-container {
-    /* display: flex; */
-    .use-table {
-      /* width: 240px !important; */
+    .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-controls {
+        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;
+            }
+          }
+        }
+        .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;
+            font-size: 13px;
+          }
+          .van-radio--horizontal {
+            padding: 0 6px;
+          }
+          .van-radio--horizontal.van-radio--checked .van-radio__label {
+            color: #2563eb;
+            font-weight: 600;
+          }
+        }
+      }
+    }
+    .popup-body {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+      overflow-y: auto;
+      padding-bottom: 16px;
     }
-    .out-table {
-      /* width: 848px; */
-      .out-table-wrap {
-        height: 500px;
-        .out-table-wrap-row {
-          height: 100%;
+    .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;
         }
-        .resourceList-col {
-          height: 100%;
-          overflow: auto;
+      }
+    }
+    .confirm-card {
+      ul {
+        max-height: 420px;
+        overflow-y: auto;
+        padding-right: 4px;
+        &::-webkit-scrollbar {
+          width: 6px;
+        }
+        &::-webkit-scrollbar-thumb {
+          background: rgba(156, 163, 175, 0.4);
+          border-radius: 999px;
+        }
+        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);
+                }
+              }
+            }
+          }
         }
       }
-      .items {
-        height: 146px;
-        padding: 12px;
-        border-radius: 4px;
-        background-color: #f2f2f2;
+    }
+    .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;
-        justify-content: space-between;
-        overflow: hidden;
+        gap: 10px;
         cursor: pointer;
+        transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
         &:hover {
-          box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3);
+          transform: translateY(-2px);
+          box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
         }
         &.active {
-          background: #ebf3ff;
-          box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3);
+          border-color: #60a5fa;
+          box-shadow: 0 8px 18px rgba(59, 130, 246, 0.16);
         }
-        header {
+        .resource-header {
           display: flex;
-          align-items: center;
           justify-content: space-between;
-
-          .title {
-            display: flex;
-            align-items: center;
-
-            .el-switch {
-              line-height: 24px;
-            }
+          align-items: center;
+          gap: 8px;
+          .resource-name {
+            font-size: 16px;
+            font-weight: 600;
+            color: #111827;
           }
         }
-
-        footer {
+        .resource-location {
+          color: #6b7280;
+          font-size: 13px;
+        }
+        .resource-footer {
           display: flex;
           align-items: center;
-
-          ul {
-            flex: 1;
-            list-style: none;
+          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 {
-              flex: 0 0 20%;
-              display: flex;
-              justify-content: center;
-              margin-bottom: 2px;
-
-              &::before {
-                display: block;
-                content: '';
-                height: 10px;
-                width: 10px;
-                border-radius: 50%;
-                background-color: #70b603;
+              width: 10px;
+              height: 10px;
+              border-radius: 50%;
+              background: #86efac;
+              &.used {
+                background: #f87171;
               }
-
-              &.used::before {
-                background-color: #fe5d5a;
-              }
-              &.empty-allocate::before {
-                background: linear-gradient(to right, #70b603 0%, #70b603 50%, #2c78ff 50%, #2c78ff 100%);
+              &.empty-allocate {
+                background: linear-gradient(90deg, #86efac 0%, #86efac 50%, #2563eb 50%, #2563eb 100%);
               }
-              &.leave::before {
-                background-color: #f59a23;
+              &.leave {
+                background: #fb923c;
               }
-              &.leave-allocate::before {
-                background: linear-gradient(to right, #f59a23 0%, #f59a23 50%, #2c78ff 50%, #2c78ff 100%);
+              &.leave-allocate {
+                background: linear-gradient(90deg, #fb923c 0%, #fb923c 50%, #2563eb 50%, #2563eb 100%);
               }
             }
           }
         }
-
-        .el-icon {
-          font-size: 20px !important;
-        }
       }
     }
-  }
-
-  .confirm-table {
-    margin-right: 12px;
-    width: 300px;
-    display: flex;
-    flex-direction: column;
-    ul {
-      height: 500px;
-      overflow-y: auto;
-      li {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        border-radius: 4px;
-        border: 1px solid #eee;
-        padding: 8px;
-        cursor: pointer;
-        & + li {
-          margin-top: 8px;
+    .usage-card {
+      .usage-list {
+        max-height: 360px;
+        overflow-y: auto;
+        padding-right: 4px;
+        &::-webkit-scrollbar {
+          width: 6px;
         }
-        &.active {
-          background-color: #ebf3ff;
+        &::-webkit-scrollbar-thumb {
+          background: rgba(156, 163, 175, 0.4);
+          border-radius: 999px;
         }
-        .text {
-          flex: 1;
-        }
-        .assignRoom {
-          font-weight: bold;
-          color: #2c78ff;
-        }
-        .el-icon {
-          color: #fff;
-          background-color: #0dda83;
-          font-size: 20px;
-          padding: 4px;
-          border-radius: 50%;
-          margin: 0 12px;
-        }
-        .btns {
+        .position-choose {
+          list-style: none;
+          padding: 0;
+          margin: 0;
           display: flex;
           flex-direction: column;
-          .el-button {
-            margin-left: 0;
+          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;
+            }
           }
         }
       }
     }
-  }
-
-  :deep(.disUoloadSty .el-upload--picture-card) {
-    display: none;
-    /* 上传按钮隐藏 */
-  }
-
-  .avatar-uploader {
-    :deep(.el-upload) {
-      width: 100%;
-      height: 100%;
-      border: 1px dashed var(--el-border-color);
-      border-radius: 6px;
-      cursor: pointer;
-      position: relative;
-      overflow: hidden;
-      transition: var(--el-transition-duration-fast);
-
-      &:hover {
-        border-color: var(--el-color-primary);
+    .popup-footer {
+      display: flex;
+      gap: 12px;
+      margin-top: 16px;
+      .van-button {
+        height: 44px;
+        font-size: 16px;
+        font-weight: 600;
       }
     }
   }
-
-  .el-icon.avatar-uploader-icon {
-    font-size: 28px;
-    color: #8c939d;
-    width: 178px;
-    height: 178px;
-    text-align: center;
-  }
-  .allocation-situation-col {
-    height: 100%;
-    .allocation-situation {
-      height: 100%;
-      overflow: auto;
-      .position-choose {
-        list-style: none;
-        border: 1px solid #eee;
-        height: 100%;
-        overflow: auto;
-        li {
-          height: 32px;
-          line-height: 32px;
-          display: flex;
-          // justify-content: space-between;
-          align-items: center;
-          .txt-flex {
-            flex: 1;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-          }
-          i {
-            display: inline-block;
-            height: 10px;
-            width: 10px;
-            border-radius: 50%;
-            margin: 0 4px;
-            background-color: #70b603;
-            &.used {
-              background-color: #fe5d5a;
-            }
-            &.empty-allocate {
-              background: linear-gradient(to right, #70b603 0%, #70b603 50%, #2c78ff 50%, #2c78ff 100%);
-            }
-            &.leave {
-              background-color: #f59a23;
-            }
-            &.leave-allocate {
-              background: linear-gradient(to right, #f59a23 0%, #f59a23 50%, #2c78ff 50%, #2c78ff 100%);
-            }
-          }
+  @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;
         }
       }
     }

+ 307 - 122
src/view/entry/components/confirm.vue

@@ -8,27 +8,63 @@
 -->
 <template>
   <div class="facilities-dialog-container">
-    <el-dialog :title="state.dialog.title" @close="onCancel" :close-on-click-modal="false" v-model="state.dialog.isShowDialog" width="90%">
-      <header class="mb8">
-        <span>确认</span>
-        <el-form inline size="mini" label-width="0">
-          <el-form-item>
-            <el-radio-group v-model="resClass" v-if="cellType.length > 1" size="small" @change="filterOutList">
-              <el-radio label="">全部</el-radio>
-              <el-radio v-for="item in cellType" :key="item.dictValue" :label="item.dictValue">{{ item.dictLabel }}</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item class="ml12">
-            <el-button color="#2c78ff" @click="autoConfirm()" size="default" :loading="autoFlag">自动确认</el-button>
-          </el-form-item>
-        </el-form>
+    <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"
+          >
+            <van-radio name="">全部</van-radio>
+            <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>
+        </div>
       </header>
       <div class="table-container">
         <div class="out-table">
-          <el-alert :title="`下个月出室人员(共${state.outList.length}人)`" type="warning" :closable="false" class="mb8" />
+          <div class="info-banner warning">
+            下个月出室人员(共{{ state.outList.length }}人)
+          </div>
+
+          <van-empty
+            v-if="!state.outList.length"
+            image="search"
+            description="暂无出室人员"
+          />
 
-          <van-list finished-text="没有更多了">
-            <van-cell v-for="item in state.outList" :key="item">
+          <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">
@@ -99,8 +135,16 @@
           </vxe-table> -->
         </div>
         <div class="confirm-table">
-          <el-alert :title="`待确认队列 (已确认${comfirmTotal}人)`" type="warning" :closable="false" class="mb8" />
-          <ul>
+          <div class="info-banner warning">
+            待确认队列 (已确认{{ comfirmTotal }}人)
+          </div>
+
+          <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">
                 <p>
@@ -110,41 +154,75 @@
                 </p>
                 <p>{{ item.pgName }}-{{ item.mentorName }}</p>
               </div>
-              <el-icon v-if="item.checked">
-                <ele-Select />
-              </el-icon>
+              <van-icon
+                v-if="item.checked"
+                name="success"
+                class="checked-icon"
+              />
               <div class="btns">
-                <el-button v-if="!item.checked" link type="primary" @click="onConfirm(item)">确认</el-button>
-                <el-button v-else link @click="item.checked = false">取消</el-button>
-                <el-button link type="primary" @click="onDetail(item)">查看</el-button>
+                <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>
+                <van-button
+                  type="primary"
+                  size="small"
+                  plain
+                  @click="onDetail(item)"
+                >
+                  查看
+                </van-button>
               </div>
             </li>
           </ul>
         </div>
       </div>
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button type="info" @click="onCancel" size="default">取 消</el-button>
-          <el-button color="#2c78ff" @click="onSubmit()" size="default">确 定</el-button>
-        </span>
-      </template>
-    </el-dialog>
+      <div class="popup-footer">
+        <van-button
+          type="primary"
+          plain
+          block
+          @click="onCancel"
+        >
+          取 消
+        </van-button>
+        <van-button
+          type="primary"
+          block
+          @click="onSubmit"
+        >
+          确 定
+        </van-button>
+      </div>
+    </van-popup>
     <DetailsDialog ref="detailsDialog" />
   </div>
 </template>
 
 <script setup lang="ts" name="systemProDialog">
   import to from 'await-to-js'
-  import { defineAsyncComponent, reactive, ref, computed, onMounted } from 'vue'
-  import { ElMessage, ElMessageBox } from 'element-plus'
+  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 { deepClone, getDictLabel } from '/@/utils/other'
+  import { getDictLabel } from '/@/utils/other'
   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'))
@@ -159,9 +237,7 @@
   const Api = usePlatformAppointApi()
   const systemApi = useSystemApi()
   const platformApi = usePlatformApi()
-  const cellAssignApi = useCellAssignApi()
   const dictApi = useDictApi()
-  const deptRef = ref()
   const detailsDialog = ref()
   const userTypeList = ref<RowDicDataType[]>([])
   const projectGroupList = ref<any[]>([])
@@ -282,13 +358,13 @@
     // 验证同类型 下个月出室人员 = 已确认人数 提示无法确认
     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) {
-      ElMessage.warning('该类型下已确认人数与出室人员人数一致,无法确认')
+    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)
-    obj.checked = true
+    if (obj) obj.checked = true
   }
   // 自动确认
   const autoConfirm = () => {
@@ -302,7 +378,7 @@
       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++) {
-        confirm[i].checked = true
+        if (confirm[i]) confirm[i].checked = true
       }
     }
     autoFlag.value = false
@@ -333,22 +409,23 @@
         if (out[i]) outArr.push(out[i].id)
       }
     }
-    let confirm: Error | null = null
     if (arr.length < total) {
-      ;[confirm] = await to(
-        ElMessageBox.confirm('下个月出室人员未全部确认,是否继续提交?', '提示', {
+      try {
+        await showConfirmDialog({
+          title: '提示',
+          message: '下个月出室人员未全部确认,是否继续提交?',
           confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning'
+          cancelButtonText: '取消'
         })
-      )
+      } catch (error) {
+        return
+      }
     }
-    if (confirm !== null) return
     const [err]: ToResponse = await to(
       Api.markNextMonthEntry({ ids: arr.map((item: any) => item.id), assignIds: outArr, platformType: platform.value.platformType })
     )
     if (err) return
-    ElMessage.success('操作成功')
+    showToast({ message: '操作成功', type: 'success' })
     emit('refresh')
     closeDialog()
   }
@@ -361,94 +438,202 @@
   })
 </script>
 <style lang="scss" scoped>
-  h4 {
-    font-size: 18px;
-  }
-  header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 32px;
-    line-height: 32px;
-    .el-form.el-form--inline .el-form-item {
-      margin: 0;
-    }
-  }
-  .table-container {
-    /* display: flex; */
-    .out-table {
-      /* width: 808px; */
-    }
-    .confirm-table {
-      margin-top: 12px;
-      /* width: 340px; */
+  .facilities-dialog-container {
+    .facilities-popup {
       display: flex;
       flex-direction: column;
-      ul {
-        height: 500px;
-        overflow-y: auto;
-        li {
+      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-controls {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        flex-wrap: wrap;
+        .van-radio-group {
           display: flex;
           align-items: center;
-          justify-content: space-between;
-          border-radius: 4px;
-          border: 1px solid #eee;
-          padding: 8px;
-          & + li {
-            margin-top: 8px;
+          gap: 8px;
+          padding: 6px 10px;
+          background: #f5f7fb;
+          border-radius: 999px;
+          .van-radio__label {
+            color: #374151;
+            font-size: 13px;
           }
-          .text {
-            flex: 1;
-            overflow: hidden;
-            p {
-              white-space: nowrap;
-              overflow: hidden;
-              text-overflow: ellipsis;
+        }
+        .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-empty__description {
+            color: #6b7280;
+            font-size: 13px;
+          }
+        }
+      }
+      .out-table {
+        .van-cell {
+          background: transparent;
+          &:not(:last-child)::after {
+            left: 16px;
+            right: 16px;
+          }
+        }
+        .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;
             }
           }
-          .el-icon {
-            color: #fff;
-            background-color: #0dda83;
-            font-size: 20px;
-            padding: 4px;
-            border-radius: 50%;
-            margin: 0 12px;
+        }
+      }
+      .confirm-table {
+        display: flex;
+        flex-direction: column;
+        ul {
+          flex: 1;
+          max-height: 440px;
+          overflow-y: auto;
+          padding-right: 4px;
+          &::-webkit-scrollbar {
+            width: 6px;
+          }
+          &::-webkit-scrollbar-thumb {
+            background-color: rgba(156, 163, 175, 0.4);
+            border-radius: 999px;
           }
-          .btns {
+          li {
             display: flex;
-            flex-direction: column;
-            .el-button {
-              margin-left: 0;
+            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;
+              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;
+              }
+            }
+            .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;
+              }
             }
           }
         }
       }
     }
-  }
-  :deep(.disUoloadSty .el-upload--picture-card) {
-    display: none; /* 上传按钮隐藏 */
-  }
-  .avatar-uploader {
-    :deep(.el-upload) {
-      width: 100%;
-      height: 100%;
-      border: 1px dashed var(--el-border-color);
-      border-radius: 6px;
-      cursor: pointer;
-      position: relative;
-      overflow: hidden;
-      transition: var(--el-transition-duration-fast);
-      &:hover {
-        border-color: var(--el-color-primary);
+    .popup-footer {
+      display: flex;
+      gap: 12px;
+      margin-top: auto;
+      .van-button {
+        height: 40px;
+        font-size: 15px;
+        font-weight: 600;
       }
     }
   }
-
-  .el-icon.avatar-uploader-icon {
-    font-size: 28px;
-    color: #8c939d;
-    width: 178px;
-    height: 178px;
-    text-align: center;
+  @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>

+ 1 - 1
src/view/entry/detail.vue

@@ -103,7 +103,7 @@
           </li>
           <li>
             <div class="label">拟培养细胞种类:</div>
-            <div class="value">{{ state.form.cellType }}</div>
+            <div class="value">{{ state.form.platPlatformAppointCellRes[0].cellType }}</div>
           </li>
           <li>
             <div class="label">细胞房预约需求:</div>

+ 13 - 7
src/view/entry/manage.vue

@@ -361,16 +361,22 @@
 
   const onLoad = async () => {
     const [err, res]: ToResponse = await to(platformAppointApi.getPlatformAppointList(state.queryParams))
-    if (err) return
-    const list = res?.data?.list || []
-    for (const item of list) {
-      state.list.push(item)
+    if (err) {
+      state.loading = false
+      state.finished = true
+      return
     }
-    state.loading = false
-    state.queryParams.pageNum++
-    if (list.length < state.queryParams.pageSize) {
+    const list = Array.isArray(res?.data?.list) ? res?.data?.list : []
+    if (list.length) {
+      state.list.push(...list)
+      state.queryParams.pageNum++
+      if (list.length < state.queryParams.pageSize) {
+        state.finished = true
+      }
+    } else {
       state.finished = true
     }
+    state.loading = false
   }
 
   const toDetail = (id: number, platformType: string, entryType: string) => {

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

@@ -233,8 +233,8 @@
             <p class="inst-title">
               <span>预约时间</span>
               <span class="title ml8">
-                {{ formatDate(new Date(item.startTime), 'mm-dd HH:MM') }}~{{
-                  formatDate(new Date(item.endTime), 'mm-dd HH:MM')
+                {{ formatDate(new Date(item.startTime), 'YYYY-mm-dd HH:MM') }}~{{
+                  formatDate(new Date(item.endTime), 'YYYY-mm-dd HH:MM')
                 }}
               </span>
             </p>
@@ -273,10 +273,6 @@
       text="首页"
       @click="onRouterPush('/home')"
     />
-    <van-action-bar-icon
-      icon="calendar-o"
-      text="周视图"
-    />
     <van-action-bar-icon
       :icon="state.instDetail.following ? 'star' : 'star-o'"
       :class="{ follow: state.instDetail.following }"

+ 351 - 475
src/view/instr/list.vue

@@ -1,122 +1,38 @@
 <template>
   <div class="home">
     <div class="search-wrap">
-      <van-search
-        placeholder="请输入仪器名称"
-        v-model="state.queryForm.instName"
-        :clearabled="true"
-        style="padding: 0"
-      ></van-search>
-      <div class="ml10 mr10">
-        <van-button
-          type="primary"
-          style="width: 60px"
-          shape="circle"
-          size="small"
-          @click="state.filterVisible = true"
-        >
-          过滤
-        </van-button>
-      </div>
-      <div>
-        <van-button
-          type="primary"
-          style="width: 60px"
-          shape="circle"
-          size="small"
-          @click="onSearch"
-        >
-          搜索
-        </van-button>
-      </div>
-    </div>
-    <!-- 筛选 -->
-    <div
-      class="filter-popup"
-      v-if="state.filterVisible"
-    >
-      <div class="filter-wrap">
-        <div @click="openSelectLaboratory">
-          <van-field
-            readonly
-            v-model="state.queryForm.laboratoryName"
-            label="实验室:"
-            placeholder="请选择实验室"
-          />
-        </div>
-      </div>
-      <div class="btn-box">
-        <div
-          class="reset"
-          @click="reset()"
-        >
-          重置
-        </div>
-        <div
-          class="submit"
-          @click="confirmFilter()"
-        >
-          确定
-        </div>
-      </div>
+      <van-search placeholder="请输入仪器名称" v-model="state.queryForm.instName" :clearabled="true"
+        style="padding: 0;flex: 1;margin-right: 10px;"></van-search>
+      <van-button type="primary" style="width: 60px" shape="circle" size="small" @click="onSearch">
+        搜索
+      </van-button>
     </div>
     <div class="inst-list">
-      <van-empty
-        v-if="state.instList.length == 0"
-        image="search"
-        description="暂无数据"
-      ></van-empty>
+      <van-empty v-if="state.instList.length == 0" image="search" description="暂无数据"></van-empty>
       <div class="inst-wrap">
-        <van-list
-          v-model:loading="state.loading"
-          :finished="state.finished"
-          finished-text="没有更多了"
-          @load="onLoad"
-        >
-          <div
-            class="inst-item mb40"
-            v-for="(v, index) in state.instList"
-            :key="index"
-          >
+        <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+          <div class="inst-item mb40" v-for="(v, index) in state.instList" :key="index">
             <div class="inst-info mt4 mb4">
-              <div
-                class="i-left"
-                @click="openDetail(v)"
-              >
+              <div class="i-left" @click="openDetail(v)">
                 <!-- <img :showLoading="true" :src="v.instPicture" width="100px" height="100%" /> -->
-                <van-image
-                  width="100px"
-                  height="100px"
-                  :src="getImageUrl(v.instPicture)"
-                />
+                <van-image width="100px" height="100px" :src="getImageUrl(v.instPicture)" />
               </div>
               <div class="i-right ml10">
-                <div
-                  class="h100 flex flex-top flex-column flex-between"
-                  @click="openDetail(v)"
-                >
+                <div class="h100 flex flex-top flex-column flex-between" @click="openDetail(v)">
                   <!-- <van-button @click="openAppoint(v)">预约</van-button> -->
                   <div class="flex flex-top mb4 ml2">
-                    <div class="detailTxt name">{{ v.instName }}(编号{{v.instCode }})</div>
+                    <div class="detailTxt name">{{ v.instName }}(编号{{ v.instCode }})</div>
                   </div>
                   <div class="flex flex-top mb4 ml2">
                     <div class="detailTxt name">{{ v.instNameEn }}</div>
                   </div>
                   <footer>
                     <div class="flex flex-top mb4 mt-auto">
-                      <img
-                        class="i-r-icon"
-                        src="../../assets/img/user.png"
-                        v-if="v.instHeadName"
-                      />
+                      <img class="i-r-icon" src="../../assets/img/user.png" v-if="v.instHeadName" />
                       <div class="detailTxt">{{ v.instHeadName }}</div>
                     </div>
                     <div class="flex flex-top">
-                      <img
-                        class="i-r-icon"
-                        src="../../assets/img/address.png"
-                        v-if="v.placeAddress"
-                      />
+                      <img class="i-r-icon" src="../../assets/img/address.png" v-if="v.placeAddress" />
                       <div class="detailTxt">{{ v.placeAddress + setLaboratoryName(v.laboratoryName) }}</div>
                     </div>
                   </footer>
@@ -127,44 +43,22 @@
         </van-list>
       </div>
     </div>
-    <van-tabbar
-      route
-      :placeholder="true"
-    >
-      <van-tabbar-item
-        replace
-        to="/instr-follow"
-        icon="star"
-      >
+    <van-tabbar route :placeholder="true">
+      <van-tabbar-item replace to="/instr-follow" icon="star">
         收藏仪器
       </van-tabbar-item>
-      <van-tabbar-item
-        replace
-        to="/instr-list"
-        icon="printer"
-      >
+      <van-tabbar-item replace to="/instr-list" icon="printer">
         全部仪器
       </van-tabbar-item>
-      <van-tabbar-item
-        replace
-        to="/instr-appoint-record"
-        icon="label"
-      >
+      <van-tabbar-item replace to="/instr-appoint-record" icon="label">
         我的预约
       </van-tabbar-item>
     </van-tabbar>
     <!-- 仪器详情 -->
-    <van-popup
-      v-model:show="state.popupShow"
-      round
-      :closeable="true"
-    >
+    <van-popup v-model:show="state.popupShow" round :closeable="true">
       <div class="detail-box">
         <div class="instNameTxt">{{ state.instDetail.instName }}</div>
-        <div
-          v-if="state.instDetail.instName"
-          class="dc-content"
-        >
+        <div v-if="state.instDetail.instName" class="dc-content">
           <div class="flex">
             <div class="label-item">仪器型号:</div>
             <div class="detailTxt">{{ state.instDetail.instNameEn }}</div>
@@ -201,49 +95,20 @@
             <div class="detailTxt">{{ state.instDetail.instSpecParam }}</div>
           </div>
           <div class="mb10">
-            <div
-              class="detailTxt"
-              text="主要功能及特色"
-              customStyle="margin-bottom:10px"
-              align="center"
-              size="15"
-              bold
-            ></div>
-            <div
-              class="detailTxt"
-              :text="state.instDetail.instFunctFeat"
-              size="14"
-            ></div>
+            <div class="detailTxt" text="主要功能及特色" customStyle="margin-bottom:10px" align="center" size="15" bold></div>
+            <div class="detailTxt" :text="state.instDetail.instFunctFeat" size="14"></div>
           </div>
           <div class="mb10">
-            <div
-              class="detailTxt"
-              text="主要附件及配置"
-              customStyle="margin-bottom:10px"
-              align="center"
-              size="15"
-              bold
-            ></div>
-            <div
-              class="detailTxt"
-              :text="state.instDetail.instAttConfig"
-              size="14"
-            ></div>
+            <div class="detailTxt" text="主要附件及配置" customStyle="margin-bottom:10px" align="center" size="15" bold></div>
+            <div class="detailTxt" :text="state.instDetail.instAttConfig" size="14"></div>
           </div>
         </div>
       </div>
     </van-popup>
     <!-- 实验室 -->
-    <van-popup
-      v-model:show="state.showLab"
-      position="bottom"
-    >
-      <van-picker
-        :columns="state.laboratoryColumns"
-        :columns-field-names="{ text: 'name', value: 'id' }"
-        @confirm="pickLab"
-        @cancel="state.showLab = false"
-      />
+    <van-popup v-model:show="state.showLab" position="bottom">
+      <van-picker :columns="state.laboratoryColumns" :columns-field-names="{ text: 'name', value: 'id' }"
+        @confirm="pickLab" @cancel="state.showLab = false" />
     </van-popup>
     <!-- <van-picker :show="showLab" :columns="laboratoryColumns" keyName="name" @cancel="showLab = false" @confirm="pickLab"></van-picker> -->
     <!-- <shutDown ref="shutDownRef"></shutDown> -->
@@ -251,352 +116,363 @@
 </template>
 
 <script lang="ts" setup>
-  import to from 'await-to-js'
-  import { onMounted, reactive } from 'vue'
-  import { useRouter } from 'vue-router'
-  import { useInstrApi } from '/@/api/instr'
-  import { useBlackApi } from '/@/api/blacklist'
-  import { usePositionApi } from '/@/api/instr/position'
-  import { showNotify } from 'vant'
-  import { getImageUrl } from '/@/utils/url'
-  const props = defineProps({
-    following: {
-      type: String,
-      default: '20',
-    },
+import to from 'await-to-js'
+import { onMounted, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { useInstrApi } from '/@/api/instr'
+import { useBlackApi } from '/@/api/blacklist'
+import { usePositionApi } from '/@/api/instr/position'
+import { showNotify } from 'vant'
+import { getImageUrl } from '/@/utils/url'
+const props = defineProps({
+  following: {
+    type: String,
+    default: '20',
+  },
+})
+const router = useRouter()
+const instApi = useInstrApi()
+const blacklistApi = useBlackApi()
+const positionApi = usePositionApi()
+
+const state = reactive({
+  showLab: false,
+  laboratoryColumns: [],
+  instStatus: {
+    10: '正常',
+    20: '故障',
+    30: '报废',
+  },
+  instList: [],
+  current: 1,
+  queryForm: {
+    laboratoryId: 0,
+    laboratoryName: '',
+    pageNum: 1,
+    pageSize: 10,
+    instName: '',
+  },
+  popupShow: false,
+  instDetail: {} as any,
+  total: 0,
+  detailsLoading: false, //详情加载
+  loading: false,
+  finished: false,
+})
+const setLaboratoryName = (name) => {
+  return name ? `(${name})` : ''
+}
+const getPosition = () => {
+  Promise.all([positionApi.getLaboratoryList({ noPage: true })]).then(([lab]) => {
+    const instr_is_concat_lab = JSON.parse(localStorage.getItem('instr_is_concat_lab') || '{}')
+    if (instr_is_concat_lab == '10') {
+      const list = lab?.data?.list.map((item) => ({ id: item.id, name: item.labName })) || []
+      state.laboratoryColumns = list
+    } else {
+      state.laboratoryColumns = lab?.data.list
+    }
   })
-  const router = useRouter()
-  const instApi = useInstrApi()
-  const blacklistApi = useBlackApi()
-  const positionApi = usePositionApi()
-
-  const state = reactive({
-    showLab: false,
-    laboratoryColumns: [],
-    filterVisible: false,
-    instStatus: {
-      10: '正常',
-      20: '故障',
-      30: '报废',
-    },
-    instList: [],
-    current: 1,
-    queryForm: {
-      laboratoryId: 0,
-      laboratoryName: '',
-      pageNum: 1,
-      pageSize: 10,
-      instName: '',
+}
+const openSelectLaboratory = () => {
+  state.showLab = true
+}
+const pickLab = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  state.showLab = false
+  state.queryForm.laboratoryId = selectedOptions[selectedOptions.length - 1].id
+  state.queryForm.laboratoryName = selectedOptions[selectedOptions.length - 1].name
+}
+// 打开设备预约
+const openAppoint = async (v) => {
+  // 验证是否拉入了黑名单
+  const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
+  if (err) return
+  if (res.data) {
+    return showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
+  }
+  router.push(`/inst/appoint?id=${v.id}&name=${v.instName}`)
+}
+// 设备详情
+const openDetail = (v) => {
+  router.push({
+    path: '/instr-detail',
+    query: {
+      id: v.id,
     },
-    popupShow: false,
-    instDetail: {} as any,
-    total: 0,
-    detailsLoading: false, //详情加载
-    loading: false,
-    finished: false,
   })
-  const setLaboratoryName = (name) => {
-    return name ? `(${name})` : ''
-  }
-  const getPosition = () => {
-    Promise.all([positionApi.getLaboratoryList({ noPage: true })]).then(([lab]) => {
-      const instr_is_concat_lab = JSON.parse(localStorage.getItem('instr_is_concat_lab') || '{}')
-      if (instr_is_concat_lab == '10') {
-        const list = lab?.data?.list.map((item) => ({ id: item.id, name: item.labName })) || []
-        state.laboratoryColumns = list
-      } else {
-        state.laboratoryColumns = lab?.data.list
-      }
-    })
-  }
-  const openSelectLaboratory = () => {
-    state.showLab = true
-  }
-  const pickLab = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    state.showLab = false
-    state.queryForm.laboratoryId = selectedOptions[selectedOptions.length - 1].id
-    state.queryForm.laboratoryName = selectedOptions[selectedOptions.length - 1].name
-  }
-  // 打开设备预约
-  const openAppoint = async (v) => {
-    // 验证是否拉入了黑名单
-    const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
-    if (err) return
-    if (res.data) {
-      return showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
+  // state.popupShow = true
+  // getDetail(v.id)
+}
+const onLoad = () => {
+  state.queryForm.pageNum++
+  getInstList()
+}
+// 查询列表
+const getInstList = async () => {
+  state.loading = true
+  const params = JSON.parse(JSON.stringify(state.queryForm))
+  params.following = props.following
+  params.instStatus = "10"
+  const [err, res]: ToResponse = await to(instApi.getList(params))
+  state.loading = false
+  if (err) return
+  if (res?.code === 200) {
+    state.instList =
+      state.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...state.instList, ...(res?.data?.list || [])]
+    state.total = res?.data?.total
+    if (state.queryForm.pageNum * state.queryForm.pageSize >= res.data.total) {
+      state.finished = true
     }
-    router.push(`/inst/appoint?id=${v.id}&name=${v.instName}`)
-  }
-  // 设备详情
-  const openDetail = (v) => {
-    router.push({
-      path: '/instr-detail',
-      query: {
-        id: v.id,
-      },
-    })
-    // state.popupShow = true
-    // getDetail(v.id)
   }
-  const onLoad = () => {
-    state.queryForm.pageNum++
-    getInstList()
+}
+const onSearch = () => {
+  state.queryForm.pageNum = 1
+  getInstList()
+}
+// 获取仪器详情
+const getDetail = async (id) => {
+  state.detailsLoading = true
+  const [err, res]: ToResponse = await to(instApi.getDetail({ id }))
+  state.detailsLoading = false
+  if (err) return
+  if (res?.code === 200) {
+    state.instDetail = res.data
   }
-  // 查询列表
-  const getInstList = async () => {
-    state.loading = true
-    const params = JSON.parse(JSON.stringify(state.queryForm))
-    params.following = props.following
-    params.instStatus = "10"
-    const [err, res]: ToResponse = await to(instApi.getList(params))
-    state.loading = false
-    if (err) return
-    if (res?.code === 200) {
-      state.instList =
-        state.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...state.instList, ...(res?.data?.list || [])]
-      state.total = res?.data?.total
-      if (state.queryForm.pageNum * state.queryForm.pageSize >= res.data.total) {
-        state.finished = true
+}
+// 关注/取关
+const handleFollowInst = async (row) => {
+  const [err] = row.following
+    ? await to(instApi.unfollow({ ids: [row.id] }))
+    : await to(instApi.follow({ ids: [row.id] }))
+  if (err) return
+  showNotify({ type: 'success', message: !row.following ? '收藏成功' : '已取消收藏' })
+  state.instList.forEach((item, index) => {
+    if (item.id == row.id) {
+      // 关注之前的关注状态
+      if (item.following) {
+        state.queryForm.pageNum = 1
+        getInstList()
+      } else {
+        item.following = true
+        const obj = item
+        state.instList.splice(index, 1)
+        state.instList.unshift(obj)
       }
+      return
     }
-    state.filterVisible = false
-  }
-  const onSearch = () => {
-    state.queryForm.pageNum = 1
-    getInstList()
-  }
-  // 重置
-  const reset = () => {
-    state.queryForm.laboratoryId = 0
-    state.queryForm.laboratoryName = ''
-  }
-  // 确认
-  const confirmFilter = () => {
-    state.filterVisible = false
-    state.queryForm.pageNum = 1
-    getInstList()
-  }
-  // 获取仪器详情
-  const getDetail = async (id) => {
-    state.detailsLoading = true
-    const [err, res]: ToResponse = await to(instApi.getDetail({ id }))
-    state.detailsLoading = false
-    if (err) return
-    if (res?.code === 200) {
-      state.instDetail = res.data
-    }
-  }
-  // 关注/取关
-  const handleFollowInst = async (row) => {
-    const [err] = row.following
-      ? await to(instApi.unfollow({ ids: [row.id] }))
-      : await to(instApi.follow({ ids: [row.id] }))
-    if (err) return
-    showNotify({ type: 'success', message: !row.following ? '收藏成功' : '已取消收藏' })
-    state.instList.forEach((item, index) => {
-      if (item.id == row.id) {
-        // 关注之前的关注状态
-        if (item.following) {
-          state.queryForm.pageNum = 1
-          getInstList()
-        } else {
-          item.following = true
-          const obj = item
-          state.instList.splice(index, 1)
-          state.instList.unshift(obj)
-        }
-        return
-      }
-    })
-  }
-  // const onEnroll = (row: any) => {
-  //   router.push({
-  //     path: '/training/enroll',
-  //     query: {
-  //       id: row.id
-  //     }
-  //   })
-  // }
-  onMounted(() => {
-    getPosition()
-    state.queryForm.pageNum = 1
-    getInstList()
   })
+}
+// const onEnroll = (row: any) => {
+//   router.push({
+//     path: '/training/enroll',
+//     query: {
+//       id: row.id
+//     }
+//   })
+// }
+onMounted(() => {
+  getPosition()
+  state.queryForm.pageNum = 1
+  getInstList()
+})
 </script>
 
 <style lang="scss" scoped>
-  * {
-    box-sizing: border-box;
-  }
-  .home {
-    height: 100%;
-    overflow: hidden;
+* {
+  box-sizing: border-box;
+}
+
+.home {
+  height: 100%;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+
+  .search-wrap {
+    height: 40px;
     display: flex;
-    flex-direction: column;
-    .search-wrap {
-      height: 40px;
-      display: flex;
-      align-items: center;
-      justify-content: space-around;
-      padding: 0 15px;
-      margin: 10px 0 0;
+    align-items: center;
+    justify-content: space-around;
+    padding: 0 15px;
+    margin: 10px 0 0;
+  }
+
+  .inst-list {
+    flex: 1;
+    height: 0;
+    background-color: #f7f8fa;
+
+    // padding-bottom: 20px;
+    // padding: 0 15px;
+    // height: calc(100% - 60px);
+    .inst-wrap {
+      height: 100%;
+      overflow: auto;
+      padding: 10px 10px 0 10px;
+      // padding: 10px 10px 0 10px;
     }
-    .inst-list {
-      flex: 1;
-      height: 0;
-      background-color: #f7f8fa;
-      // padding-bottom: 20px;
-      // padding: 0 15px;
-      // height: calc(100% - 60px);
-      .inst-wrap {
-        height: 100%;
-        overflow: auto;
-        padding: 10px 10px 0 10px;
-        // padding: 10px 10px 0 10px;
+
+    .inst-item {
+      background-color: #fff;
+      padding: 8px;
+      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+      margin-bottom: 14px;
+      border-radius: 6px;
+
+      .follow-icon {
+        width: 24px;
+        height: 24px;
       }
-      .inst-item {
-        background-color: #fff;
-        padding: 8px;
-        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
-        margin-bottom: 14px;
-        border-radius: 6px;
-        .follow-icon {
-          width: 24px;
-          height: 24px;
-        }
-        .inst-info {
-          display: flex;
-        }
-        .i-right {
-          flex: 1;
-          font-size: 14px;
-          .i-r-icon {
-            width: 15px;
-            height: 15px;
-            margin-right: 10px;
-          }
+
+      .inst-info {
+        display: flex;
+      }
+
+      .i-right {
+        flex: 1;
+        font-size: 14px;
+
+        .i-r-icon {
+          width: 15px;
+          height: 15px;
+          margin-right: 10px;
         }
       }
     }
-    .detail-box {
-      height: 80vh;
-      background: #fff;
-      border-radius: 10px;
-      padding: 15px;
-      .dc-content {
-        width: 100%;
-        height: calc(100% - 30px);
-        overflow: auto;
-        .label-item {
-          width: 90px;
-          font-size: 14px;
-          padding: 6px;
-        }
-        .detailTxt {
-          flex: 1;
-        }
+  }
+
+  .detail-box {
+    height: 80vh;
+    background: #fff;
+    border-radius: 10px;
+    padding: 15px;
+
+    .dc-content {
+      width: 100%;
+      height: calc(100% - 30px);
+      overflow: auto;
+
+      .label-item {
+        width: 90px;
+        font-size: 14px;
+        padding: 6px;
+      }
+
+      .detailTxt {
+        flex: 1;
       }
     }
-    .filter-popup {
-      background: rgba(0, 0, 0, 0.8);
-      position: fixed;
+  }
+
+  .filter-popup {
+    background: rgba(0, 0, 0, 0.8);
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    z-index: 1;
+    top: 50px;
+
+    .filter-wrap {
       width: 100%;
-      height: 100%;
-      left: 0;
-      z-index: 1;
-      top: 50px;
-      .filter-wrap {
-        width: 100%;
-        padding: 10px;
-        background: #ffffff;
-
-        .filter-item {
-          padding-bottom: 14px;
-
-          .tit {
-            font-size: 14px;
-            color: #323232;
-            font-weight: bold;
-            padding-bottom: 10px;
-          }
+      padding: 10px;
+      background: #ffffff;
+
+      .filter-item {
+        padding-bottom: 14px;
 
-          .menu-list {
-            display: flex;
-            flex-wrap: wrap;
+        .tit {
+          font-size: 14px;
+          color: #323232;
+          font-weight: bold;
+          padding-bottom: 10px;
+        }
 
-            .menu-item {
-              margin-right: 20px;
-              margin-bottom: 10px;
-            }
+        .menu-list {
+          display: flex;
+          flex-wrap: wrap;
+
+          .menu-item {
+            margin-right: 20px;
+            margin-bottom: 10px;
           }
         }
       }
+    }
 
-      .btn-box {
-        width: 100%;
-        height: 40px;
-        padding: 10px;
-        background: #ffffff;
-        display: flex;
+    .btn-box {
+      width: 100%;
+      height: 40px;
+      padding: 10px;
+      background: #ffffff;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      >div {
+        width: 160px;
+        height: 32px;
+        border-radius: 20px;
+        border: solid 1px #ec652b;
         align-items: center;
-        justify-content: space-between;
-
-        > div {
-          width: 160px;
-          height: 32px;
-          border-radius: 20px;
-          border: solid 1px #ec652b;
-          align-items: center;
-          justify-content: center;
-          text-align: center;
-          line-height: 32px;
-        }
+        justify-content: center;
+        text-align: center;
+        line-height: 32px;
+      }
 
-        .reset {
-          color: #ec652b;
-        }
+      .reset {
+        color: #ec652b;
+      }
 
-        .submit {
-          color: #fff;
-          background-color: #ec652b;
-        }
+      .submit {
+        color: #fff;
+        background-color: #ec652b;
       }
     }
   }
-  .instNameTxt {
-    margin-bottom: 10px;
-    font-size: 16px;
-    font-weight: bold;
-    text-align: center;
-  }
-  .detailTxt {
-    font-size: 12px;
-    color: #333333;
-    &.name {
-      font-weight: bold;
-      font-size: 16px;
-    }
-  }
-  .labelTit {
-    margin-bottom: 10px;
-    text-align: center;
-    font-size: 15px;
-    font-weight: bold;
-  }
-  .primary-color {
-    color: #3c9cff;
-  }
-  .warning-color {
-    color: #ff976a;
-  }
-  .danger-color {
-    color: #ee0a24;
-  }
-  .instr-status-btn {
+}
+
+.instNameTxt {
+  margin-bottom: 10px;
+  font-size: 16px;
+  font-weight: bold;
+  text-align: center;
+}
+
+.detailTxt {
+  font-size: 12px;
+  color: #333333;
+
+  &.name {
     font-weight: bold;
-    font-size: 14px;
-  }
-  .mt-auto {
-    margin-top: auto;
+    font-size: 16px;
   }
+}
+
+.labelTit {
+  margin-bottom: 10px;
+  text-align: center;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+.primary-color {
+  color: #3c9cff;
+}
+
+.warning-color {
+  color: #ff976a;
+}
+
+.danger-color {
+  color: #ee0a24;
+}
+
+.instr-status-btn {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+.mt-auto {
+  margin-top: auto;
+}
 </style>

+ 172 - 171
src/view/login/index.vue

@@ -17,7 +17,7 @@
       <van-field v-model="state.form.password" type="password" placeholder="请输入密码" class="mt10" />
 
       <div class="mt10 input-box code">
-        <van-field v-model="state.form.idValueC" placeholder="验证码"/>
+        <van-field v-model="state.form.idValueC" placeholder="验证码" />
         <div class="login-code">
           <img class="captcha-image" width="120" height="40" :src="state.captchaImage" @click="getCaptchaImage" />
         </div>
@@ -38,195 +38,196 @@
 </template>
 
 <script lang="ts" setup>
-  import { onMounted, reactive } from 'vue'
-  import to from 'await-to-js'
-  import { useLoginApi } from '/@/api/login/index'
-  import { Local } from '/@/utils/storage'
-  import crypto from 'sm-crypto'
-  import { useRouter, useRoute } from 'vue-router'
-  import { storeToRefs } from 'pinia'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { showDialog } from 'vant'
-  import { HttpStatus } from '/@/constants/pageConstants'
-
-  const router = useRouter()
-  const route = useRoute()
-  const sm3 = crypto.sm3
-  const loginApi = useLoginApi()
-  const storesUseUserInfo = useUserInfo()
-  const { userInfos, openId, unionId } = storeToRefs(storesUseUserInfo)
-  const state = reactive({
-    captchaImage: '',
-    loading: {
-      signIn: false
-    },
-    form: {
-      userName: '',
-      password: '',
-      idValueC: '',
-      idKeyC: '',
-    },
-  })
-
-  const getCaptchaImage = async () => {
-    const [err, res]: ToResponse = await to(loginApi.getCaptchaImg())
-    if (err) return
-    if (res.code == HttpStatus.OK) {
-      state.captchaImage = res.data.base64stringC
-      state.form.idKeyC = res.data.idKeyC
-    }
-  }
-
-  const onSignIn = async () => {
-    state.loading.signIn = true
-    const params = JSON.parse(JSON.stringify(state.form))
-    params.password = sm3(params.password)
-    params.openId = openId.value
-    params.unionId = unionId.value
-    // params.password = (params.password)
-    // sm3
-    const post = params.openId ? loginApi.weChatLogin : loginApi.signIn
-    const [err, res]: ToResponse = await to(post(params))
-    if (err) {
-      state.loading.signIn = false
-      await getCaptchaImage()
-      return
-    }
-    // 存储 token 到浏览器缓存
-    Local.set('token', res?.data?.token)
-    router.push('/home')
-  }
-  const openIdLogin = async () => {
-    const [err, res]: ToResponse = await to(loginApi.weChatLoginUnionId({
-      openId: openId.value,
-      unionId: unionId.value,
-    }))
-    if (err) return
-    Local.set('token', res?.data.token)
-    router.push('/home')
+import { onMounted, reactive } from 'vue'
+import to from 'await-to-js'
+import { useLoginApi } from '/@/api/login/index'
+import { Local } from '/@/utils/storage'
+import crypto from 'sm-crypto'
+import { useRouter, useRoute } from 'vue-router'
+import { storeToRefs } from 'pinia'
+import { useUserInfo } from '/@/stores/userInfo'
+import { showDialog } from 'vant'
+import { HttpStatus } from '/@/constants/pageConstants'
+
+const router = useRouter()
+const route = useRoute()
+const sm3 = crypto.sm3
+const loginApi = useLoginApi()
+const storesUseUserInfo = useUserInfo()
+const { userInfos, openId, unionId } = storeToRefs(storesUseUserInfo)
+const state = reactive({
+  captchaImage: '',
+  loading: {
+    signIn: false
+  },
+  form: {
+    userName: '',
+    password: '',
+    idValueC: '',
+    idKeyC: '',
+  },
+})
+
+const getCaptchaImage = async () => {
+  const [err, res]: ToResponse = await to(loginApi.getCaptchaImg())
+  if (err) return
+  if (res.code == HttpStatus.OK) {
+    state.captchaImage = res.data.base64stringC
+    state.form.idKeyC = res.data.idKeyC
   }
-  const onRegister = () => {
-    router.push('/register')
+}
+
+const onSignIn = async () => {
+  state.loading.signIn = true
+  const params = JSON.parse(JSON.stringify(state.form))
+  params.password = sm3(params.password)
+  params.openId = openId.value
+  params.unionId = unionId.value
+  // params.password = (params.password)
+  // sm3
+  const post = params.openId ? loginApi.weChatLogin : loginApi.signIn
+  const [err, res]: ToResponse = await to(post(params))
+  if (err) {
+    state.loading.signIn = false
+    await getCaptchaImage()
+    state.form.idValueC = ''
+    return
   }
-  const blueTooth = async () => {
-    try {
-      // 请求蓝牙设备
-      // @ts-ignore
-      if (!navigator?.bluetooth?.requestDevice) {
-        showDialog({
-          message: '当前浏览器不支持蓝牙功能,请升级浏览器版本或更换浏览器。',
-          confirmButtonText: '确定'
-        })
-      }
-      // @ts-ignore
-      const device = await navigator.bluetooth.requestDevice({
-        filters: [{ services: ['generic_access'] }],
-        optionalServices: [],
-        acceptAllDevices: false
+  // 存储 token 到浏览器缓存
+  Local.set('token', res?.data?.token)
+  router.push('/home')
+}
+const openIdLogin = async () => {
+  const [err, res]: ToResponse = await to(loginApi.weChatLoginUnionId({
+    openId: openId.value,
+    unionId: unionId.value,
+  }))
+  if (err) return
+  Local.set('token', res?.data.token)
+  router.push('/home')
+}
+const onRegister = () => {
+  router.push('/register')
+}
+const blueTooth = async () => {
+  try {
+    // 请求蓝牙设备
+    // @ts-ignore
+    if (!navigator?.bluetooth?.requestDevice) {
+      showDialog({
+        message: '当前浏览器不支持蓝牙功能,请升级浏览器版本或更换浏览器。',
+        confirmButtonText: '确定'
       })
-      // 连接到设备
-      const server = await device.gatt.connect()
-
-      // 获取服务
-      const service = await server.getPrimaryService('generic_access')
-
-      // 获取特征
-      const characteristic = await service.getCharacteristic('device_name')
-
-      // 读取特征值
-      const value = await characteristic.readValue()
-
-      // 将特征值转换为字符串
-      const decoder = new TextDecoder('utf-8')
-      const deviceName = decoder.decode(value)
-
-      console.log('Device Name:', deviceName)
-    } catch (error) {
-      console.error('Error:', error)
     }
+    // @ts-ignore
+    const device = await navigator.bluetooth.requestDevice({
+      filters: [{ services: ['generic_access'] }],
+      optionalServices: [],
+      acceptAllDevices: false
+    })
+    // 连接到设备
+    const server = await device.gatt.connect()
+
+    // 获取服务
+    const service = await server.getPrimaryService('generic_access')
+
+    // 获取特征
+    const characteristic = await service.getCharacteristic('device_name')
+
+    // 读取特征值
+    const value = await characteristic.readValue()
+
+    // 将特征值转换为字符串
+    const decoder = new TextDecoder('utf-8')
+    const deviceName = decoder.decode(value)
+
+    console.log('Device Name:', deviceName)
+  } catch (error) {
+    console.error('Error:', error)
   }
-  onMounted(async () => {
-    const code: string = route.query.code ? route.query.code.toString() : ''
-    //  if (!code) {
-    //       const appid = "wxdd446f7e55c80c16";
-    //       const redirect_uri = encodeURIComponent(window.location.href);
-    //       const scope = "snsapi_base"; // 静默授权
-    //       const state = "dashoo88888";
-    //       const auth_url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
-    //       window.location.href = auth_url;
-    //     } else {
-    //       await storesUseUserInfo.setOpenId(code)
-    //     }
-
-    if(code) {
-      await storesUseUserInfo.setOpenId(code)
-      openIdLogin()
-    }
-    await getCaptchaImage()
-  })
+}
+onMounted(async () => {
+  const code: string = route.query.code ? route.query.code.toString() : ''
+  //  if (!code) {
+  //       const appid = "wxdd446f7e55c80c16";
+  //       const redirect_uri = encodeURIComponent(window.location.href);
+  //       const scope = "snsapi_base"; // 静默授权
+  //       const state = "dashoo88888";
+  //       const auth_url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
+  //       window.location.href = auth_url;
+  //     } else {
+  //       await storesUseUserInfo.setOpenId(code)
+  //     }
+
+  if (code) {
+    await storesUseUserInfo.setOpenId(code)
+    openIdLogin()
+  }
+  await getCaptchaImage()
+})
 </script>
 
 <style lang="scss" scoped>
-  .login-container {
-    height: calc(100% - 80px);
-    width: 300px;
+.login-container {
+  height: calc(100% - 80px);
+  width: 300px;
+  display: flex;
+  margin: 0 auto;
+  padding: 40px 0;
+  flex-direction: column;
+
+  .logo {
     display: flex;
-    margin: 0 auto;
-    padding: 40px 0;
     flex-direction: column;
+    align-items: center;
+  }
 
-    .logo {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-    }
-
-    header {
-      display: flex;
-      flex-direction: column;
-      flex: 1;
-    }
-
-    footer {
-      flex: 0 0 30px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
+  header {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+  }
 
-    h4 {
-      text-align: center;
-      font-size: 24px;
-      margin: 40px 0;
-    }
+  footer {
+    flex: 0 0 30px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
 
-    .w50 {
-      width: 120px;
-      background-color: #e8effc;
-    }
+  h4 {
+    text-align: center;
+    font-size: 24px;
+    margin: 40px 0;
+  }
 
-    .van-cell {
-      background-color: #f3f5f6;
-      border-radius: 22px;
-      overflow: hidden;
+  .w50 {
+    width: 120px;
+    background-color: #e8effc;
+  }
 
-      :v-deep .van-van-field__control {
-        text-align: center;
-      }
+  .van-cell {
+    background-color: #f3f5f6;
+    border-radius: 22px;
+    overflow: hidden;
 
-      :deep(.van-van-field__control) {
-        text-align: center;
-      }
+    :v-deep .van-van-field__control {
+      text-align: center;
     }
 
-    .input-box.code {
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
+    :deep(.van-van-field__control) {
+      text-align: center;
     }
+  }
 
-    .captcha-image {
-      margin-left: 16px;
-    }
+  .input-box.code {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .captcha-image {
+    margin-left: 16px;
   }
+}
 </style>

+ 608 - 654
src/view/register/index.vue

@@ -10,84 +10,36 @@
   <div class="register">
     <div class="form">
       <van-row class="pl10 pr10 pt10">
-        <van-button
-          type="success"
-          @click="changeType('10')"
-        >
+        <van-button type="success" @click="changeType('10')">
           注册课题组成员
         </van-button>
-        <van-button
-          type="primary"
-          @click="changeType('20')"
-        >
+        <van-button type="primary" @click="changeType('20')">
           注册课题组负责人
         </van-button>
       </van-row>
-      <van-steps
-        :active="state.active"
-        class="pl20 pr20 pt10"
-      >
+      <van-steps :active="state.active" class="pl20 pr20 pt10">
         <van-step>登录信息</van-step>
         <van-step>个人信息</van-step>
         <van-step v-if="state.form.registerType == '20'">项目信息</van-step>
       </van-steps>
-      <van-form
-        ref="loginInfoRef"
-        v-show="state.active == 0"
-        required="auto"
-      >
+      <van-form ref="loginInfoRef" v-show="state.active == 0" required="auto">
         <van-cell-group inset>
-          <van-field
-            v-model="state.form.userName"
-            label="登录账号"
-            placeholder="登录账号"
-            @blur="checkUserNamePhoneExists('userName')"
-            :rules="[{ required: true, message: '请填写登录账号' }]"
-          />
-          <van-field
-            v-model="state.form.password"
-            type="password"
-            label="密码"
-            placeholder="密码"
-            :rules="[{ required: true, validator: checkPassword, }]"
-          />
-          <van-field
-            v-model="state.form.confirmPassword"
-            type="password"
-            label="确认密码"
-            placeholder="确认密码"
-            :rules="[{ required: true, validator: confirmPasswordSame, message: '两次输入的密码不一致' }]"
-          />
+          <van-field v-model="state.form.userName" label="登录账号" placeholder="登录账号"
+            @blur="checkUserNamePhoneExists('userName')" :rules="[{ required: true, message: '请填写登录账号' }]" />
+          <van-field v-model="state.form.password" type="password" label="密码" placeholder="密码"
+            :rules="[{ required: true, validator: checkPassword, }]" />
+          <van-field v-model="state.form.confirmPassword" type="password" label="确认密码" placeholder="确认密码"
+            :rules="[{ required: true, validator: confirmPasswordSame, message: '两次输入的密码不一致' }]" />
         </van-cell-group>
       </van-form>
-      <van-form
-        ref="personInfoRef"
-        v-show="state.active == 1"
-        required="auto"
-      >
+      <van-form ref="personInfoRef" v-show="state.active == 1" required="auto">
         <van-cell-group inset>
-          <van-field
-            v-model="state.form.nickName"
-            label="姓名"
-            placeholder="姓名"
-            :rules="[{ required: true, message: '请填写姓名' }]"
-          />
-          <van-field
-            label="性别"
-            :rules="[{ required: false }]"
-          >
+          <van-field v-model="state.form.nickName" label="姓名" placeholder="姓名"
+            :rules="[{ required: true, message: '请填写姓名' }]" />
+          <van-field label="性别" :rules="[{ required: false }]">
             <template #input>
-              <van-radio-group
-                v-model="state.form.sex"
-                label="性别"
-                placeholder="性别"
-                direction="horizontal"
-              >
-                <van-radio
-                  v-for="item in userSexList"
-                  :name="item.dictValue"
-                  :key="item.dictValue"
-                >
+              <van-radio-group v-model="state.form.sex" label="性别" placeholder="性别" direction="horizontal">
+                <van-radio v-for="item in userSexList" :name="item.dictValue" :key="item.dictValue">
                   {{ item.dictLabel }}
                 </van-radio>
               </van-radio-group>
@@ -96,261 +48,151 @@
           <van-field>
             <template #label>
               <span>用户类型</span>
-              <el-tooltip
-                class="box-item"
-                effect="dark"
-                placement="top"
-                append-to="body"
-              >
+              <el-tooltip class="box-item" effect="dark" placement="top" append-to="body">
                 <template #content>
-                  <div
-                    v-for="item in UserTypeTooltip"
-                    :key="item"
-                  >
+                  <div v-for="item in UserTypeTooltip" :key="item">
                     {{ item }}
                   </div>
                 </template>
-                <el-icon style="margin-left: 10px;"><QuestionFilled /></el-icon>
+                <el-icon style="margin-left: 10px;">
+                  <QuestionFilled />
+                </el-icon>
               </el-tooltip>
             </template>
             <template #input>
-              <van-radio-group
-                v-model="state.form.userType"
-                label="用户类型"
-                placeholder="用户类型"
-                direction="horizontal"
-              >
-                <van-radio
-                  v-for="item in userTypeList"
-                  :name="item.dictValue"
-                  :key="item.dictValue"
-                >
+              <van-radio-group v-model="state.form.userType" label="用户类型" placeholder="用户类型" direction="horizontal">
+                <van-radio v-for="item in userTypeList" :name="item.dictValue" :key="item.dictValue">
                   {{ item.dictLabel }}
                 </van-radio>
               </van-radio-group>
             </template>
           </van-field>
-          <van-field
-            v-model="state.form.deptName"
-            is-link
-            readonly
-            label="组织部门"
-            placeholder="组织部门"
-            @click="showDeptPickerHandler"
-            :rules="[{ required: true, message: '请选择组织部门' }]"
-          />
-          <van-field
-            v-if="needInputDeptInfo"
-            v-model="state.form.deptDescribe"
-            label="部门描述"
-            placeholder="部门描述"
-            :rules="[{ required: true, message: '请填写部门描述' }]"
-          />
-          <van-field
-            v-model="state.form.phone"
-            type="tel"
-            label="手机号"
-            placeholder="手机号"
-            @blur="checkUserNamePhoneExists('phone')"
-            :rules="[{ required: true, message: '请填写手机号' }]"
-          />
-          <van-field
-            v-model="state.form.email"
-            label="邮箱"
-            placeholder="邮箱"
-            :rules="[{ required: true, message: '请填写邮箱' }]"
-          />
-          <van-field
-            label="证件类型"
-            :rules="[{ required: true, message: '请选择证件类型' }]"
-          >
+          <van-field v-model="state.form.deptName" is-link readonly label="组织部门" placeholder="组织部门"
+            @click="showDeptPickerHandler" :rules="[{ required: true, message: '请选择组织部门' }]" />
+          <van-field v-if="needInputDeptInfo" v-model="state.form.deptDescribe" label="部门描述" placeholder="部门描述"
+            :rules="[{ required: true, message: '请填写部门描述' }]" />
+          <van-field v-model="state.form.phone" type="tel" label="手机号" placeholder="手机号"
+            @blur="checkUserNamePhoneExists('phone')" :rules="[{ required: true, message: '请填写手机号' }]" />
+          <van-field v-model="state.form.email" label="邮箱" placeholder="邮箱"
+            :rules="[{ required: true, message: '请填写邮箱' }]" />
+          <van-field label="证件类型" :rules="[{ required: true, message: '请选择证件类型' }]">
             <template #input>
-              <van-radio-group
-                disabled
-                v-model="state.form.idType"
-                label="证件类型"
-                placeholder="证件类型"
-                direction="horizontal"
-              >
-                <van-radio
-                  v-for="item in userCertList"
-                  :name="item.dictValue"
-                  :key="item.id"
-                >
+              <van-radio-group disabled v-model="state.form.idType" label="证件类型" placeholder="证件类型"
+                direction="horizontal">
+                <van-radio v-for="item in userCertList" :name="item.dictValue" :key="item.id">
                   {{ item.dictLabel }}
                 </van-radio>
               </van-radio-group>
             </template>
           </van-field>
-          <van-field
-            v-model="state.form.idCode"
-            label="证件号"
-            placeholder="证件号"
-            :rules="[{ required: true, message: '请填写证件号' }]"
-          />
-          <van-field
-            v-model="state.form.projectGroupName"
-            v-if="state.form.registerType === '20'"
-            label="课题组"
-            placeholder="课题组"
-            :rules="[{ required: true, message: '请填写课题组' }]"
-          />
+          <van-field v-model="state.form.idCode" label="证件号" placeholder="证件号"
+            :rules="[{ required: true, message: '请填写证件号' }]" />
+          <van-field v-model="state.form.projectGroupName" v-if="state.form.registerType === '20'" label="课题组"
+            placeholder="课题组" :rules="[{ required: true, message: '请填写课题组' }]" />
           <template v-else>
-            <van-field
-              :disabled="!state.form.deptId"
-              v-model="state.form.projectGroupName"
-              is-link
-              readonly
-              label="课题组"
-              placeholder="课题组"
-              :rules="[{ required: true, message: '请填写课题组' }]"
-              @click="showPjtPicker = true"
-            />
+            <van-field :disabled="!state.form.deptId" v-model="state.form.projectGroupName" is-link readonly label="课题组"
+              placeholder="课题组" :rules="[{ required: true, message: '请填写课题组' }]" @click="showPjtPicker = true" />
           </template>
         </van-cell-group>
       </van-form>
-      <van-form
-        ref="projectInfoRef"
-        v-show="state.active == 2"
-        required="auto"
-      >
+      <van-form ref="projectInfoRef" v-show="state.active == 2" required="auto">
         <van-cell-group inset>
-          <van-field>
-            <template #input>
-              <div class="w100">
-                <van-button
-                  type="primary"
-                  block
-                  @click="addProject"
-                >
-                  新增
+          <div class="project-list-container">
+            <van-button type="primary" block @click="addProject" class="add-project-btn">
+              新增项目
+            </van-button>
+            <div v-if="state.form.projectList.length === 0" class="empty-project-tip">
+              请至少添加一个项目
+            </div>
+            <div v-for="(item, index) in state.form.projectList" :key="index" class="project-card">
+              <div class="project-card-header">
+                <span class="project-card-title">项目 {{ index + 1 }}</span>
+                <van-button 
+                  v-if="state.form.projectList.length > 1"
+                  size="mini" 
+                  type="danger" 
+                  plain
+                  @click="delProjectItem(index)"
+                  class="delete-btn">
+                  删除
                 </van-button>
-                <template
-                  v-for="(item, index) in state.form.projectList"
-                  :key="index"
-                >
-                  <van-field
-                    v-model="item.projectName"
-                    placeholder="项目名称"
-                    class="mt10"
-                  ></van-field>
-                  <van-field
-                    v-model="item.projectTypeName"
-                    is-link
-                    readonly
-                    label="项目类型"
-                    placeholder="项目类型"
-                    @click="openPjtType(item, index)"
-                  />
-                  <van-field
-                    v-model="item.projectSource"
-                    placeholder="项目来源"
-                  ></van-field>
-                  <van-button
-                    style="height: 30px"
-                    type="danger"
-                    icon="el-icon-delete"
-                    @click="delProjectItem(index)"
-                  >
-                    删除
-                  </van-button>
-                </template>
               </div>
-            </template>
-          </van-field>
+              <div class="project-card-body">
+                <van-field 
+                  v-model="item.projectName" 
+                  label="项目名称" 
+                  placeholder="请输入项目名称"
+                  :rules="[{ required: true, message: '请输入项目名称' }]"
+                  class="project-field">
+                </van-field>
+                <van-field 
+                  v-model="item.projectTypeName" 
+                  is-link 
+                  readonly 
+                  label="项目类型" 
+                  placeholder="请选择项目类型"
+                  @click="openPjtType(item, index)"
+                  :rules="[{ required: true, message: '请选择项目类型' }]"
+                  class="project-field">
+                </van-field>
+                <van-field 
+                  v-model="item.projectSource" 
+                  label="项目来源" 
+                  placeholder="请输入项目来源"
+                  :rules="[{ required: true, message: '请输入项目来源' }]"
+                  class="project-field">
+                </van-field>
+              </div>
+            </div>
+          </div>
         </van-cell-group>
       </van-form>
     </div>
     <footer>
-      <van-button
-        v-if="state.active > 0"
-        @click="preStep"
-      >
+      <van-button v-if="state.active > 0" @click="preStep">
         上一步
       </van-button>
-      <van-button
-        v-if="state.active < (state.form.registerType == '10' ? 1 : 2)"
-        type="primary"
-        class="ml10"
-        @click="nextStep"
-      >
+      <van-button v-if="state.active < (state.form.registerType == '10' ? 1 : 2)" type="primary" class="ml10"
+        @click="nextStep">
         下一步
       </van-button>
-      <van-button
-        v-if="
-          (state.form.registerType == '10' && state.active === 1) ||
-          (state.form.registerType == '20' && state.active === 2)
-        "
-        @click="onRegister"
-        type="primary"
-        class="ml10"
-      >
+      <van-button v-if="
+        (state.form.registerType == '10' && state.active === 1) ||
+        (state.form.registerType == '20' && state.active === 2)
+      " @click="onRegister" type="primary" class="ml10">
         提交
       </van-button>
     </footer>
     <!-- 部门选择器 -->
-    <van-popup
-      v-model:show="showDeptPicker"
-      position="bottom"
-    >
+    <van-popup v-model:show="showDeptPicker" position="bottom">
       <div class="cascade-picker">
         <div class="cascade-header">
           <span>请选择部门</span>
-          <van-icon
-            name="cross"
-            @click="showDeptPicker = false"
-          />
+          <van-icon name="cross" @click="showDeptPicker = false" />
         </div>
         <div class="cascade-content">
           <!-- 第一级 -->
           <div class="cascade-column">
-            <div
-              v-for="item in cascadeData.level1"
-              :key="item.id"
-              class="cascade-item"
-              :class="{ active: selectedLevel1?.id === item.id }"
-              @click="selectLevel1(item)"
-            >
+            <div v-for="item in cascadeData.level1" :key="item.id" class="cascade-item"
+              :class="{ active: selectedLevel1?.id === item.id }" @click="selectLevel1(item)">
               <span>{{ item.deptName }}{{ item.children?.length ? `(${item.children.length})` : '' }}</span>
-              <van-icon
-                v-if="item.children?.length"
-                name="arrow"
-              />
+              <van-icon v-if="item.children?.length" name="arrow" />
             </div>
           </div>
 
           <!-- 第二级 -->
-          <div
-            v-if="cascadeData.level2.length"
-            class="cascade-column"
-          >
-            <div
-              v-for="item in cascadeData.level2"
-              :key="item.id"
-              class="cascade-item"
-              :class="{ active: selectedLevel2?.id === item.id }"
-              @click="selectLevel2(item)"
-            >
+          <div v-if="cascadeData.level2.length" class="cascade-column">
+            <div v-for="item in cascadeData.level2" :key="item.id" class="cascade-item"
+              :class="{ active: selectedLevel2?.id === item.id }" @click="selectLevel2(item)">
               <span>{{ item.deptName }}{{ item.children?.length ? `(${item.children.length})` : '' }}</span>
-              <van-icon
-                v-if="item.children?.length"
-                name="arrow"
-              />
+              <van-icon v-if="item.children?.length" name="arrow" />
             </div>
           </div>
 
           <!-- 第三级 -->
-          <div
-            v-if="cascadeData.level3.length"
-            class="cascade-column"
-          >
-            <div
-              v-for="item in cascadeData.level3"
-              :key="item.id"
-              class="cascade-item"
-              :class="{ active: selectedLevel3?.id === item.id }"
-              @click="selectLevel3(item)"
-            >
+          <div v-if="cascadeData.level3.length" class="cascade-column">
+            <div v-for="item in cascadeData.level3" :key="item.id" class="cascade-item"
+              :class="{ active: selectedLevel3?.id === item.id }" @click="selectLevel3(item)">
               <span>{{ item.deptName }}</span>
             </div>
           </div>
@@ -358,488 +200,600 @@
 
         <div class="cascade-footer">
           <van-button @click="showDeptPicker = false">取消</van-button>
-          <van-button
-            type="primary"
-            @click="confirmCascadeSelection"
-          >
+          <van-button type="primary" @click="confirmCascadeSelection">
             确定
           </van-button>
         </div>
       </div>
     </van-popup>
     <!-- 课题组选择器 -->
-    <van-popup
-      v-model:show="showPjtPicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="pjtList"
-        :columns-field-names="{ text: 'pgName', value: 'id' }"
-        @confirm="onPjtPicker"
-        @cancel="showPjtPicker = false"
-      />
+    <van-popup v-model:show="showPjtPicker" position="bottom">
+      <van-picker :columns="pjtList" :columns-field-names="{ text: 'pgName', value: 'id' }" @confirm="onPjtPicker"
+        @cancel="showPjtPicker = false" />
     </van-popup>
     <!-- 所在时间 -->
-    <van-popup
-      v-model:show="showPjtDatePicker"
-      position="bottom"
-    >
-      <van-picker-group
-        title="预约日期"
-        :tabs="['开始日期', '结束日期']"
-        next-step-text="下一步"
-        @confirm="onPjtDatePicker"
-      >
+    <van-popup v-model:show="showPjtDatePicker" position="bottom">
+      <van-picker-group title="预约日期" :tabs="['开始日期', '结束日期']" next-step-text="下一步" @confirm="onPjtDatePicker">
         <van-date-picker v-model="state.form.startDate" />
         <van-date-picker v-model="state.form.endDate" />
       </van-picker-group>
     </van-popup>
     <!-- 项目类型 -->
-    <van-popup
-      v-model:show="showPjtTypePicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="pjtTypeList"
-        :columns-field-names="{ text: 'dictLabel', value: 'dictValue' }"
-        @confirm="onPjtTypePicker"
-        @cancel="showPjtTypePicker = false"
-      />
+    <van-popup v-model:show="showPjtTypePicker" position="bottom">
+      <van-picker :columns="pjtTypeList" :columns-field-names="{ text: 'dictLabel', value: 'dictValue' }"
+        @confirm="onPjtTypePicker" @cancel="showPjtTypePicker = false" />
     </van-popup>
-    <van-notify
-      v-model:show="show"
-      type="danger"
-    >
+    <van-notify v-model:show="show" type="danger">
       <span>操作失败</span>
     </van-notify>
   </div>
 </template>
 
 <script name="register" lang="ts" setup>
-  import { onMounted, reactive, ref, watch } from 'vue'
-  import to from 'await-to-js'
-  import { useLoginApi } from '/@/api/login/index'
-  import crypto from 'sm-crypto'
-  import { useDictApi } from '/@/api/system/dict'
-  import { useProApi } from '/@/api/project'
-  import { useDeptApi } from '/@/api/system/dept'
-  import { useRouter, useRoute } from 'vue-router'
-  import { showNotify } from 'vant'
-
-  import { UserTypeTooltip } from '/@/constants/pageConstants'
-  import { isPasswordValid } from '/@/utils/stringUtils'
-
-  const sm3 = crypto.sm3
-  const loginApi = useLoginApi()
-  const router = useRouter()
-  const dictApi = useDictApi()
-  const proApi = useProApi()
-  const deptApi = useDeptApi()
-  const loginInfoRef = ref()
-  const personInfoRef = ref()
-  const projectInfoRef = ref()
-  const showDeptPicker = ref(false)
-  const showPjtPicker = ref(false)
-  const showPjtDatePicker = ref(false)
-  const showPjtTypePicker = ref(false)
-  const show = ref(false)
-  const pjtTypeIndex = ref(-1)
-  const needInputDeptInfo = ref(false)
-  // 级联选择器相关数据
-  const cascadeData = ref({
-    level1: <any[]>[],
-    level2: <any[]>[],
-    level3: <any[]>[],
-  })
-  const selectedLevel1 = ref<any>(null)
-  const selectedLevel2 = ref<any>(null)
-  const selectedLevel3 = ref<any>(null)
-
-  const userTypeList = ref(<RowDicDataType[]>[])
-  const userSexList = ref(<RowDicDataType[]>[])
-  const userCertList = ref(<RowDicDataType[]>[])
-  const deptData = ref(<any[]>[])
-  const pjtList = ref(<any[]>[])
-  const pjtTypeList = ref(<any[]>[])
-  const state = reactive({
-    active: 0,
-    loading: {
-      signIn: false,
-    },
-    form: {
-      id: 0,
-      userName: '', // 账户名称
-      nickName: '', // 用户姓名
-      userType: '10', // 关联角色
-      deptId: null,
-      deptName: '', // 单位名称
-      phone: '', // 手机号
-      email: '', // 邮箱
-      sex: '30', // 性别
-      password: '', // 账户密码
-      confirmPassword: '',
-      status: '10', // 用户状态
-      describe: '', // 用户描述
-      avatar: '',
-      idType: '', //证件类型
-      idCode: '', // 证件号
-      personnelType: '', // 人员类型
-      projectId: null,
+import { onMounted, reactive, ref, watch } from 'vue'
+import to from 'await-to-js'
+import { useLoginApi } from '/@/api/login/index'
+import crypto from 'sm-crypto'
+import { useDictApi } from '/@/api/system/dict'
+import { useProApi } from '/@/api/project'
+import { useDeptApi } from '/@/api/system/dept'
+import { useRouter, useRoute } from 'vue-router'
+import { showNotify } from 'vant'
+
+import { UserTypeTooltip } from '/@/constants/pageConstants'
+import { isPasswordValid } from '/@/utils/stringUtils'
+
+const sm3 = crypto.sm3
+const loginApi = useLoginApi()
+const router = useRouter()
+const dictApi = useDictApi()
+const proApi = useProApi()
+const deptApi = useDeptApi()
+const loginInfoRef = ref()
+const personInfoRef = ref()
+const projectInfoRef = ref()
+const showDeptPicker = ref(false)
+const showPjtPicker = ref(false)
+const showPjtDatePicker = ref(false)
+const showPjtTypePicker = ref(false)
+const show = ref(false)
+const pjtTypeIndex = ref(-1)
+const needInputDeptInfo = ref(false)
+// 级联选择器相关数据
+const cascadeData = ref({
+  level1: <any[]>[],
+  level2: <any[]>[],
+  level3: <any[]>[],
+})
+const selectedLevel1 = ref<any>(null)
+const selectedLevel2 = ref<any>(null)
+const selectedLevel3 = ref<any>(null)
+
+const userTypeList = ref(<RowDicDataType[]>[])
+const userSexList = ref(<RowDicDataType[]>[])
+const userCertList = ref(<RowDicDataType[]>[])
+const deptData = ref(<any[]>[])
+const pjtList = ref(<any[]>[])
+const pjtTypeList = ref(<any[]>[])
+const state = reactive({
+  active: 0,
+  loading: {
+    signIn: false,
+  },
+  form: {
+    id: 0,
+    userName: '', // 账户名称
+    nickName: '', // 用户姓名
+    userType: '10', // 关联角色
+    deptId: null,
+    deptName: '', // 单位名称
+    phone: '', // 手机号
+    email: '', // 邮箱
+    sex: '30', // 性别
+    password: '', // 账户密码
+    confirmPassword: '',
+    status: '10', // 用户状态
+    describe: '', // 用户描述
+    avatar: '',
+    idType: '', //证件类型
+    idCode: '', // 证件号
+    personnelType: '', // 人员类型
+    projectId: null,
+    projectName: '',
+    projectDate: '',
+    startDate: [],
+    endDate: [],
+    registerType: '10',
+    applyPg: {},
+    projectGroupName: '',
+    projectGroupId: null,
+    projectList: <any[]>[{
       projectName: '',
-      projectDate: '',
-      startDate: [],
-      endDate: [],
-      registerType: '10',
-      applyPg: {},
-      projectGroupName: '',
-      projectGroupId: null,
-      projectList: <any[]>[],
-      unitName: '',
-      deptDescribe: '',
-    },
+      projectType: '',
+      projectTypeName: '',
+      projectSource: '',
+    }],
+    unitName: '',
+    deptDescribe: '',
+  },
+})
+const deptDataBackup = ref(<any[]>[])
+
+const getDicts = () => {
+  Promise.all([
+    dictApi.getDictDataByType('sys_user_type'),
+    dictApi.getDictDataByType('sys_com_sex'),
+    dictApi.getDictDataByType('sys_user_certificate'),
+    deptApi.getDeptTree(),
+    proApi.getProjectGroupListForApp({ noPage: true }),
+    dictApi.getDictDataByType('sci_pjt_level'),
+  ]).then(([type, sex, cert, dept, pjt, pjtType]) => {
+    userTypeList.value = type.data.values || []
+    userSexList.value = sex.data.values || []
+    userCertList.value = cert.data.values || []
+    deptDataBackup.value = dept.data || []
+    deptData.value = dept.data || []
+    pjtList.value = pjt.data.list || []
+    pjtTypeList.value = pjtType.data.values || []
+
+    // 初始化级联数据
+    initCascadeData()
   })
-  const deptDataBackup = ref(<any[]>[])
-
-  const getDicts = () => {
-    Promise.all([
-      dictApi.getDictDataByType('sys_user_type'),
-      dictApi.getDictDataByType('sys_com_sex'),
-      dictApi.getDictDataByType('sys_user_certificate'),
-      deptApi.getDeptTree(),
-      proApi.getProjectGroupListForApp({ noPage: true }),
-      dictApi.getDictDataByType('sci_pjt_level'),
-    ]).then(([type, sex, cert, dept, pjt, pjtType]) => {
-      userTypeList.value = type.data.values || []
-      userSexList.value = sex.data.values || []
-      userCertList.value = cert.data.values || []
-      deptDataBackup.value = dept.data || []
-      deptData.value = dept.data || []
-      pjtList.value = pjt.data.list || []
-      pjtTypeList.value = pjtType.data.values || []
-
-      // 初始化级联数据
-      initCascadeData()
-    })
-  }
+}
 
-  const checkUserNamePhoneExists = async (type: 'userName' | 'phone') => {
-    let resquest = loginApi.checkUserNamePhoneExists({ userName: state.form.userName, phone: '' })
+const checkUserNamePhoneExists = async (type: 'userName' | 'phone') => {
+  let resquest = loginApi.checkUserNamePhoneExists({ userName: state.form.userName, phone: '' })
 
-    if (type === 'phone') {
-      resquest = loginApi.checkUserNamePhoneExists({ userName: '', phone: state.form.phone })
-    }
-
-    await to(resquest)
+  if (type === 'phone') {
+    resquest = loginApi.checkUserNamePhoneExists({ userName: '', phone: state.form.phone })
   }
 
-  const renderDeptData = () => {
-    let daptTree = deptDataBackup.value
+  await to(resquest)
+}
 
-    if (state.form.userType === '10') {
-      daptTree = daptTree.filter((item) => item.id === 100001)
-    }
+const renderDeptData = () => {
+  let daptTree = deptDataBackup.value
 
-    if (state.form.userType === '15') {
-      daptTree = daptTree.filter((item) => item.id !== 1000220)
-    }
+  if (state.form.userType === '10') {
+    daptTree = daptTree.filter((item) => item.id === 100001)
+  }
 
-    if (state.form.userType === '20') {
-      daptTree = daptTree.filter((item) => item.id === 1000220)
-    }
+  if (state.form.userType === '15') {
+    daptTree = daptTree.filter((item) => item.id !== 1000220)
+  }
 
-    return daptTree
+  if (state.form.userType === '20') {
+    daptTree = daptTree.filter((item) => item.id === 1000220)
   }
 
-  const deptIncludesProjectGroup = async (deptId: number) => {
-    const [err, res]: ToResponse = await to(
-      proApi.getProjectGroupList({
-        pgOrg: deptId,
-        pgName: '',
-        pageNum: 1,
-        pageSize: 1000,
-      }),
-    )
-    if (err) return
+  return daptTree
+}
+
+const deptIncludesProjectGroup = async (deptId: number) => {
+  const [err, res]: ToResponse = await to(
+    proApi.getProjectGroupList({
+      pgOrg: deptId,
+      pgName: '',
+      pageNum: 1,
+      pageSize: 1000,
+    }),
+  )
+  if (err) return
 
-    pjtList.value = res?.data.list || []
-  }
+  pjtList.value = res?.data.list || []
+}
 
-  const preStep = () => {
-    state.active--
-  }
-  const nextStep = async () => {
-    if (state.active < 3) {
-      let form = loginInfoRef.value
-      if (state.active == 1) {
-        form = personInfoRef.value
-      } else if (state.active == 2) {
-        form = projectInfoRef.value
-      }
-      const [err, res] = await to(form.validate())
-      if (err) return
-      state.active++
-    }
+const preStep = () => {
+  state.active--
+}
+const validateProjectList = () => {
+  // 校验至少有一个项目
+  if (!state.form.projectList || state.form.projectList.length === 0) {
+    showNotify({
+      type: 'danger',
+      message: '请至少添加一个项目',
+    })
+    return false
   }
 
-  const checkPassword = (value: any) => {
-    let checkResult = isPasswordValid(value)
-    if(!checkResult.isPassed){
-      return checkResult.errorMsg
+  // 校验每个项目的必填字段
+  for (let i = 0; i < state.form.projectList.length; i++) {
+    const item = state.form.projectList[i]
+    if (!item.projectName || !item.projectName.trim()) {
+      showNotify({
+        type: 'danger',
+        message: `项目 ${i + 1} 的项目名称不能为空`,
+      })
+      return false
     }
-  }
-
-  const confirmPasswordSame = (value: any) => {
-    if (!value) {
+    if (!item.projectType || !item.projectTypeName) {
+      showNotify({
+        type: 'danger',
+        message: `项目 ${i + 1} 的项目类型不能为空`,
+      })
       return false
-    } else if (value !== state.form.password) {
+    }
+    if (!item.projectSource || !item.projectSource.trim()) {
+      showNotify({
+        type: 'danger',
+        message: `项目 ${i + 1} 的项目来源不能为空`,
+      })
       return false
-    } else {
-      return true
     }
   }
 
-  watch(
-    () => state.form.userType,
-    (newVal) => {
-      if (newVal === '20') {
-        state.form.idType = '30'
-        needInputDeptInfo.value = true
-      } else {
-        state.form.idType = '20'
-        state.form.deptId = null
-        state.form.deptName = ''
-        needInputDeptInfo.value = false
+  return true
+}
+
+const nextStep = async () => {
+  if (state.active < 3) {
+    let form = loginInfoRef.value
+    if (state.active == 1) {
+      form = personInfoRef.value
+    } else if (state.active == 2) {
+      form = projectInfoRef.value
+      // 校验项目列表
+      if (!validateProjectList()) {
+        return
       }
-    },
-    { immediate: true },
-  )
-
-  const changeType = (val: string) => {
-    state.form.registerType = val
-    if (val === '10' && state.active == 2) {
-      state.active = 1
+      // 校验表单字段
+      const [err] = await to(form.validate())
+      if (err) return
+      state.active++
+      return
     }
+    const [err, res] = await to(form.validate())
+    if (err) return
+    state.active++
   }
+}
 
-  const showDeptPickerHandler = () => {
-    initCascadeData()
-    showDeptPicker.value = true
-  }
-  const onPjtPicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    showPjtPicker.value = false
-    state.form.projectGroupName = selectedOptions[0].pgName
-    state.form.projectGroupId = selectedOptions[0].id
+const checkPassword = (value: any) => {
+  let checkResult = isPasswordValid(value)
+  if (!checkResult.isPassed) {
+    return checkResult.errorMsg
   }
-  const onPjtDatePicker = () => {
-    showPjtDatePicker.value = false
-    state.form.projectDate = `${state.form.startDate.join('-')}至${state.form.endDate.join('-')}`
+}
+
+const confirmPasswordSame = (value: any) => {
+  if (!value) {
+    return false
+  } else if (value !== state.form.password) {
+    return false
+  } else {
+    return true
   }
-  const addProject = () => {
-    state.form.projectList.push({
-      projectName: '',
-      projectType: '',
-      projectTypeName: '',
-      projectSource: '',
-    })
+}
+
+watch(
+  () => state.form.userType,
+  (newVal) => {
+    if (newVal === '20') {
+      state.form.idType = '30'
+      needInputDeptInfo.value = true
+    } else {
+      state.form.idType = '20'
+      state.form.deptId = null
+      state.form.deptName = ''
+      needInputDeptInfo.value = false
+    }
+  },
+  { immediate: true },
+)
+
+const changeType = (val: string) => {
+  state.form.registerType = val
+  if (val === '10' && state.active == 2) {
+    state.active = 1
   }
-  const delProjectItem = (idx: number) => {
+}
+
+const showDeptPickerHandler = () => {
+  initCascadeData()
+  showDeptPicker.value = true
+}
+const onPjtPicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  showPjtPicker.value = false
+  state.form.projectGroupName = selectedOptions[0].pgName
+  state.form.projectGroupId = selectedOptions[0].id
+}
+const onPjtDatePicker = () => {
+  showPjtDatePicker.value = false
+  state.form.projectDate = `${state.form.startDate.join('-')}至${state.form.endDate.join('-')}`
+}
+const addProject = () => {
+  state.form.projectList.push({
+    projectName: '',
+    projectType: '',
+    projectTypeName: '',
+    projectSource: '',
+  })
+}
+const delProjectItem = (idx: number) => {
+  if (state.form.projectList.length > 1) {
     state.form.projectList.splice(idx, 1)
+  } else {
+    showNotify({
+      type: 'danger',
+      message: '至少需要保留一个项目',
+    })
   }
-  const openPjtType = (row: any, idx: number) => {
-    pjtTypeIndex.value = idx
-    showPjtTypePicker.value = true
-  }
-  const onPjtTypePicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    showPjtTypePicker.value = false
-    state.form.projectList[pjtTypeIndex.value].projectType = selectedOptions[0].dictValue
-    state.form.projectList[pjtTypeIndex.value].projectTypeName = selectedOptions[0].dictLabel
-  }
-
-  watch(
-    () => state.form.deptId,
-    (newVal) => {
-      if (newVal) {
-        deptIncludesProjectGroup(newVal)
-      }
-    },
-  )
-
-  // 级联选择器方法
-  const initCascadeData = () => {
-    cascadeData.value.level1 = renderDeptData()
+}
+const openPjtType = (row: any, idx: number) => {
+  pjtTypeIndex.value = idx
+  showPjtTypePicker.value = true
+}
+const onPjtTypePicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  showPjtTypePicker.value = false
+  state.form.projectList[pjtTypeIndex.value].projectType = selectedOptions[0].dictValue
+  state.form.projectList[pjtTypeIndex.value].projectTypeName = selectedOptions[0].dictLabel
+}
+
+watch(
+  () => state.form.deptId,
+  (newVal) => {
+    if (newVal) {
+      deptIncludesProjectGroup(newVal)
+    }
+  },
+)
+
+// 级联选择器方法
+const initCascadeData = () => {
+  cascadeData.value.level1 = renderDeptData()
+  cascadeData.value.level2 = []
+  cascadeData.value.level3 = []
+  selectedLevel1.value = null
+  selectedLevel2.value = null
+  selectedLevel3.value = null
+}
+
+const selectLevel1 = (item: any) => {
+  selectedLevel1.value = item
+  selectedLevel2.value = null
+  selectedLevel3.value = null
+
+  if (item.children && item.children.length > 0) {
+    cascadeData.value.level2 = item.children
+    cascadeData.value.level3 = []
+  } else {
     cascadeData.value.level2 = []
     cascadeData.value.level3 = []
-    selectedLevel1.value = null
-    selectedLevel2.value = null
-    selectedLevel3.value = null
   }
+}
 
-  const selectLevel1 = (item: any) => {
-    selectedLevel1.value = item
-    selectedLevel2.value = null
-    selectedLevel3.value = null
+const selectLevel2 = (item: any) => {
+  selectedLevel2.value = item
+  selectedLevel3.value = null
 
-    if (item.children && item.children.length > 0) {
-      cascadeData.value.level2 = item.children
-      cascadeData.value.level3 = []
-    } else {
-      cascadeData.value.level2 = []
-      cascadeData.value.level3 = []
+  if (item.children && item.children.length > 0) {
+    cascadeData.value.level3 = item.children
+  } else {
+    cascadeData.value.level3 = []
+  }
+}
+
+const selectLevel3 = (item: any) => {
+  selectedLevel3.value = item
+}
+
+const confirmCascadeSelection = () => {
+  const selectedItem = selectedLevel3.value || selectedLevel2.value || selectedLevel1.value
+  if (selectedItem) {
+    state.form.deptName = selectedItem.deptName
+    state.form.deptId = selectedItem.id
+    showDeptPicker.value = false
+  }
+}
+
+const onRegister = async () => {
+  const form = state.form.registerType == '10' ? personInfoRef.value : personInfoRef.value
+  const [error] = await to(form.validate())
+  if (error) return
+  
+  // 如果是课题组负责人,需要校验项目信息
+  if (state.form.registerType === '20') {
+    if (!validateProjectList()) {
+      return
     }
   }
+  
+  const params = JSON.parse(JSON.stringify(state.form))
+  params.password = sm3(params.password)
+  delete params.confirmPassword
+  params.startDate = params.startDate.join('-')
+  params.endDate = params.endDate.join('-')
+  delete params.projectDate
+  const [err] = await to(loginApi.register(params))
+  if (err) {
+    // show.value = true
+    // setTimeout(() => {
+    //   show.value = false
+    // }, 2000)
+    return
+  }
+  showNotify({
+    type: 'success',
+    message:
+      state.form.registerType === '20'
+        ? '你已经注册为课题组负责人,等待系统管理员审核通过'
+        : '你已经注册为课题组成员,等待课题组负责人审核通过',
+  })
+  router.push('/login')
+}
+onMounted(async () => {
+  getDicts()
+})
+</script>
 
-  const selectLevel2 = (item: any) => {
-    selectedLevel2.value = item
-    selectedLevel3.value = null
+<style lang="scss" scoped>
+.register {
+  display: flex;
+  flex-direction: column;
+  height: calc(100% - 20px);
 
-    if (item.children && item.children.length > 0) {
-      cascadeData.value.level3 = item.children
-    } else {
-      cascadeData.value.level3 = []
+  .van-row .van-button {
+    flex: 1;
+
+    &+.van-button {
+      margin-left: 10px;
     }
   }
 
-  const selectLevel3 = (item: any) => {
-    selectedLevel3.value = item
+  .form {
+    flex: 1;
+    overflow-y: auto;
   }
 
-  const confirmCascadeSelection = () => {
-    const selectedItem = selectedLevel3.value || selectedLevel2.value || selectedLevel1.value
-    if (selectedItem) {
-      state.form.deptName = selectedItem.deptName
-      state.form.deptId = selectedItem.id
-      showDeptPicker.value = false
-    }
+  footer {
+    padding: 0 10px 10px;
+    flex: 0 0 40px;
+    display: flex;
+    justify-content: flex-end;
   }
+}
 
-  const onRegister = async () => {
-    const form = state.form.registerType == '10' ? personInfoRef.value : personInfoRef.value
-    const [error] = await to(form.validate())
-    if (error) return
-    const params = JSON.parse(JSON.stringify(state.form))
-    params.password = sm3(params.password)
-    delete params.confirmPassword
-    params.startDate = params.startDate.join('-')
-    params.endDate = params.endDate.join('-')
-    delete params.projectDate
-    const [err] = await to(loginApi.register(params))
-    if (err) {
-      show.value = true
-      setTimeout(() => {
-        show.value = false
-      }, 2000)
-      return
+.cascade-picker {
+  background: #fff;
+  border-radius: 8px 8px 0 0;
+  overflow: hidden;
+
+  .cascade-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px;
+    border-bottom: 1px solid #f5f5f5;
+    font-size: 16px;
+    font-weight: 500;
+
+    .van-icon {
+      font-size: 18px;
+      color: #969799;
+      cursor: pointer;
     }
-    showNotify({
-      type: 'success',
-      message:
-        state.form.registerType === '20'
-          ? '你已经注册为课题组负责人,等待系统管理员审核通过'
-          : '你已经注册为课题组成员,等待课题组负责人审核通过',
-    })
-    router.push('/login')
   }
-  onMounted(async () => {
-    getDicts()
-  })
-</script>
 
-<style lang="scss" scoped>
-  .register {
+  .cascade-content {
     display: flex;
-    flex-direction: column;
-    height: calc(100% - 20px);
-    .van-row .van-button {
+    height: 300px;
+
+    .cascade-column {
       flex: 1;
-      & + .van-button {
-        margin-left: 10px;
+      border-right: 1px solid #f5f5f5;
+      overflow-y: auto;
+
+      &:last-child {
+        border-right: none;
+      }
+
+      .cascade-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 12px 16px;
+        cursor: pointer;
+        transition: background-color 0.2s;
+
+        &:hover {
+          background-color: #f7f8fa;
+        }
+
+        &.active {
+          background-color: #e8f3ff;
+          color: #1989fa;
+        }
+
+        span {
+          flex: 1;
+          font-size: 14px;
+        }
+
+        .van-icon {
+          font-size: 12px;
+          color: #c8c9cc;
+        }
       }
     }
-    .form {
+  }
+
+  .cascade-footer {
+    display: flex;
+    padding: 12px 16px;
+    border-top: 1px solid #f5f5f5;
+    gap: 12px;
+
+    .van-button {
       flex: 1;
-      overflow-y: auto;
-    }
-    footer {
-      padding: 0 10px 10px;
-      flex: 0 0 40px;
-      display: flex;
-      justify-content: flex-end;
     }
   }
+}
+
+.project-list-container {
+  padding: 12px;
+
+  .add-project-btn {
+    margin-bottom: 12px;
+  }
+
+  .empty-project-tip {
+    padding: 20px;
+    text-align: center;
+    color: #969799;
+    font-size: 14px;
+    background-color: #f7f8fa;
+    border-radius: 8px;
+    margin-bottom: 16px;
+  }
 
-  .cascade-picker {
+  .project-card {
     background: #fff;
-    border-radius: 8px 8px 0 0;
+    border: 1px solid #ebedf0;
+    border-radius: 8px;
+    margin-bottom: 16px;
     overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+    transition: box-shadow 0.3s;
 
-    .cascade-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 16px;
-      border-bottom: 1px solid #f5f5f5;
-      font-size: 16px;
-      font-weight: 500;
-
-      .van-icon {
-        font-size: 18px;
-        color: #969799;
-        cursor: pointer;
-      }
+    &:hover {
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
     }
 
-    .cascade-content {
+    .project-card-header {
       display: flex;
-      height: 300px;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 16px;
+      background: linear-gradient(135deg, #1989fa 0%, #0050b3 100%);
+      color: #fff;
 
-      .cascade-column {
-        flex: 1;
-        border-right: 1px solid #f5f5f5;
-        overflow-y: auto;
+      .project-card-title {
+        font-size: 16px;
+        font-weight: 500;
+      }
 
-        &:last-child {
-          border-right: none;
-        }
+      .delete-btn {
+        background: rgba(255, 255, 255, 0.2);
+        border-color: rgba(255, 255, 255, 0.3);
+        color: #fff;
 
-        .cascade-item {
-          display: flex;
-          justify-content: space-between;
-          align-items: center;
-          padding: 12px 16px;
-          cursor: pointer;
-          transition: background-color 0.2s;
-
-          &:hover {
-            background-color: #f7f8fa;
-          }
-
-          &.active {
-            background-color: #e8f3ff;
-            color: #1989fa;
-          }
-
-          span {
-            flex: 1;
-            font-size: 14px;
-          }
-
-          .van-icon {
-            font-size: 12px;
-            color: #c8c9cc;
-          }
+        &:hover {
+          background: rgba(255, 255, 255, 0.3);
         }
       }
     }
 
-    .cascade-footer {
-      display: flex;
-      padding: 12px 16px;
-      border-top: 1px solid #f5f5f5;
-      gap: 12px;
+    .project-card-body {
+      padding: 12px 0;
 
-      .van-button {
-        flex: 1;
+      .project-field {
+        margin-bottom: 8px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
       }
     }
   }
+}
 </style>

+ 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.laboratoryName" />
+      <van-cell title="所属平台" title-class="cell-title" :value="state.form.placeAddress" />
       <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: '',
-      laboratoryName: '',
+      placeAddress: '',
       instClassDesc: '',
       costType: '',
       appointeeInstrumentName: '',

+ 1 - 0
src/view/todo/index.vue

@@ -88,6 +88,7 @@
   }
   const changeType = (str: string) => {
     state.list = []
+    checkedList.value = []
     state.queryParams.pageNum = 1
     onLoad()
   }

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

@@ -90,7 +90,7 @@
   <van-action-bar placeholder>
     <van-action-bar-icon icon="wap-home-o" text="首页" @click="onRouterPush('/home')" />
     <van-action-bar-icon icon="passed" text="我的培训" @click="onRouterPush('/training/done')" />
-    <van-action-bar-button type="primary" text="前往考试" @click="onRouterPush('/exam/cover', { state: state.form.relationExamId })" />
+    <van-action-bar-button type="primary" :disabled="state.form.status === '40'" text="前往考试" @click="onRouterPush('/exam/cover', { state: state.form.relationExamId })" />
   </van-action-bar>
 </template>
 

+ 112 - 98
src/view/training/index.vue

@@ -8,14 +8,16 @@
 -->
 <template>
   <div class="app-container">
-    <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad" class="mt10">
+    <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad"
+      class="mt10">
       <van-cell v-for="(item, index) in state.list" :key="index" :center="true">
         <template #title>
           <div class="list" @click="onEnroll(item)">
             <header class="flex justify-between">
               <van-text-ellipsis class="title" :content="item.title" />
-              <van-tag v-if="item.applyStatus === '10'" type="warning">未报名</van-tag>
-              <van-tag v-else-if="item.applyStatus === '20'" type="primary">已报名</van-tag>
+              <van-tag v-if="item.status === '40'">已结束</van-tag>
+              <van-tag v-if="item.status !== '40' && item.applyStatus === '10'" type="warning">未报名</van-tag>
+              <van-tag v-else-if="item.status !== '40' && item.applyStatus === '20'" type="primary">已报名</van-tag>
             </header>
             <footer class="flex justify-between">
               <van-text-ellipsis :content="item.createdName" />
@@ -29,109 +31,121 @@
 </template>
 
 <script lang="ts" setup>
-  import to from 'await-to-js'
-  import { onMounted, reactive } from 'vue'
-  import { useRouter } from 'vue-router'
-  import { useTrainingApi } from '/@/api/training'
-  import { formatDate } from '/@/utils/formatTime'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { showNotify } from 'vant'
-  const props = defineProps({
-    isAll: {
-      type: String,
-      default: '10'
-    }
-  })
-  const trainingApi = useTrainingApi()
-  const router = useRouter()
-  const state = reactive({
-    queryParams: {
-      pageNum: 1,
-      pageSize: 10,
-      isAll: false
-    },
-    list: [] as RowTraningApplyType[],
-    loading: false,
-    finished: false
-  })
-  const onLoad = async () => {
-    if (state.loading) return
-    state.loading = true
-    const params = JSON.parse(JSON.stringify(state.queryParams))
-    params.isAll = props.isAll
-    params.userId = useUserInfo().userInfos.id
-    const [err, res]: ToResponse = await to(trainingApi.getListForUser(params))
-    if (err) {
-      state.loading = false
-      return
-    }
-    const list = res?.data?.list || []
-    for (const item of list) {
-      state.list.push(item)
-    }
-    if (list.length < state.queryParams.pageSize) {
-      state.finished = true
-    }
-    state.queryParams.pageNum++
+import to from 'await-to-js'
+import { onMounted, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { useTrainingApi } from '/@/api/training'
+import { formatDate } from '/@/utils/formatTime'
+import { useUserInfo } from '/@/stores/userInfo'
+import { showNotify } from 'vant'
+const props = defineProps({
+  isAll: {
+    type: String,
+    default: '10'
+  }
+})
+const trainingApi = useTrainingApi()
+const router = useRouter()
+const state = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    isAll: false
+  },
+  list: [] as RowTraningApplyTypeItem[],
+  loading: false,
+  finished: false
+})
+const onLoad = async () => {
+  if (state.loading) return
+  state.loading = true
+  const params = JSON.parse(JSON.stringify(state.queryParams))
+  params.isAll = props.isAll
+  params.userId = useUserInfo().userInfos.id
+  const [err, res]: ToResponse = await to(trainingApi.getListForUser(params))
+  if (err) {
     state.loading = false
+    return
   }
-  const onEnroll = (row: any) => {
-    if (row.applyStatus === '20') {
-      if (props.isAll === '10') {
-        showNotify({
-          type: 'warning',
-          message: '您已报名该培训,请勿重复报名'
-        })
-      } else {
-        router.push({
-          path: '/training/detail',
-          query: {
-            id: row.id
-          }
-        })
-      }
-      return
-    }
-    router.push({
-      path: '/training/enroll',
-      query: {
-        state: row.id
-      }
+  const list = res?.data?.list || []
+  for (const item of list) {
+    state.list.push(item)
+  }
+  if (list.length < state.queryParams.pageSize) {
+    state.finished = true
+  }
+  state.queryParams.pageNum++
+  state.loading = false
+}
+const onEnroll = (row: any) => {
+  if (row.status === '40' && row.applyStatus === '10') {
+    showNotify({
+      type: 'warning',
+      message: '该培训已结束'
     })
+    return
+  }
+  if (row.applyStatus === '20') {
+    if (props.isAll === '10') {
+      showNotify({
+        type: 'warning',
+        message: '您已报名该培训,请勿重复报名'
+      })
+    } else {
+      router.push({
+        path: '/training/detail',
+        query: {
+          id: row.id
+        }
+      })
+    }
+    return
   }
-  onMounted(() => {
-    onLoad()
+  router.push({
+    path: '/training/enroll',
+    query: {
+      state: row.id
+    }
   })
+}
+onMounted(() => {
+  onLoad()
+})
 </script>
 
 <style lang="scss" scoped>
-  .app-container {
-    .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;
-      }
+.app-container {
+  .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;
     }
   }
+}
 </style>