Ver código fonte

feat(web-admin): add personnel photo on driver/escort pages

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 meses atrás
pai
commit
8d9f5777e0
1 arquivos alterados com 90 adições e 4 exclusões
  1. 90 4
      web-admin/src/views/pl/personnel/index.vue

+ 90 - 4
web-admin/src/views/pl/personnel/index.vue

@@ -105,6 +105,23 @@
             header-align="center"
             :index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
           />
+          <el-table-column
+            v-if="isColumnVisible('photoUrls')"
+            label="照片"
+            min-width="80"
+            align="center"
+            header-align="center"
+          >
+            <template #default="{ row }">
+              <img
+                v-if="firstPhotoUrl(row.photoUrls)"
+                class="pl-photo-thumb"
+                :src="resolvePhotoDisplayUrl(firstPhotoUrl(row.photoUrls))"
+                alt="人员照片"
+              />
+              <span v-else class="pl-photo-thumb pl-photo-thumb--empty">未上传</span>
+            </template>
+          </el-table-column>
           <el-table-column
             prop="personnelName"
             label="姓名"
@@ -122,6 +139,17 @@
               </div>
             </template>
           </el-table-column>
+          <el-table-column
+            v-if="isColumnVisible('certAttachments')"
+            label="证书附件"
+            min-width="340"
+            align="left"
+            header-align="left"
+          >
+            <template #default="{ row }">
+              <PlCertListAttachmentCell :cert-list="row.certList" />
+            </template>
+          </el-table-column>
           <el-table-column
             v-if="isColumnVisible('personnelAffiliation')"
             label="人员归属"
@@ -289,6 +317,15 @@
               maxlength="512"
             />
           </el-form-item>
+          <el-form-item label="人员照片" style="grid-column: 1 / -1">
+            <AttachmentUploadField
+              ref="photoUploadRef"
+              v-model="formData.photoUrls"
+              :limit="1"
+              accept="image/*"
+              upload-folder="pl/personnel-photo"
+            />
+          </el-form-item>
         </div>
 
         <!-- 人员证书(列表) -->
@@ -341,6 +378,16 @@
           <el-icon><Delete /></el-icon>删除
         </el-button>
       </template>
+      <template #photo>
+        <AttachmentUploadField
+          v-if="viewDetail?.photoUrls"
+          :model-value="viewDetail.photoUrls"
+          disabled
+          :limit="1"
+          accept="image/*"
+        />
+        <span v-else>—</span>
+      </template>
       <template #certs>
         <PlCertViewList :cert-list="viewDetail?.certList || []" layout="table" />
       </template>
@@ -362,6 +409,8 @@ import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
 import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
 import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
 import PlPersonnelCertBadges from '@/components/business/pl-personnel-cert-badges/index.vue'
+import PlCertListAttachmentCell from '@/components/business/pl-cert-list-attachment-cell/index.vue'
+import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
 import SystemViewDialog, {
   type ViewSection
 } from '@/components/business/system-view-dialog/index.vue'
@@ -372,6 +421,7 @@ import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
 import { pickBxPageResult } from '@/utils/http/bx'
 import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
 import { formatDateTime } from '@/utils/format'
+import { firstPhotoUrl, resolvePhotoDisplayUrl } from '@/utils/pl-photo'
 import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
 import {
   collectRequiredCertSlotErrors,
@@ -401,6 +451,8 @@ const isCarrierPersonnel = computed(() => affiliationMode.value === 'carrier')
 const loading = ref(false)
 
 const listColumnDefs = [
+  { prop: 'photoUrls', label: '照片' },
+  { prop: 'certAttachments', label: '证书附件' },
   { prop: 'personnelAffiliation', label: '人员归属' },
   { prop: 'personnelType', label: '人员类型' },
   { prop: 'phoneNo', label: '手机号' },
@@ -417,7 +469,7 @@ const {
   isIndeterminate,
   isColumnVisible,
   resetToDefault
-} = usePersistedListColumns('pl-personnel-list-columns', listColumnDefs)
+} = usePersistedListColumns('pl-personnel-list-columns-v3', listColumnDefs)
 
 const submitLoading = ref(false)
 const personnelRows = ref<Personnel[]>([])
@@ -429,6 +481,7 @@ const formRef = ref()
 const isEdit = ref(false)
 const certList = ref<CertListItem[]>([])
 const certFormListRef = ref<CertFormListRef>()
+const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
 const viewVisible = ref(false)
 const viewLoading = ref(false)
 const viewDetail = ref<PersonnelDetail | null>(null)
@@ -463,7 +516,8 @@ const formData = reactive({
   personnelType: '',
   personnelAffiliation: 'self',
   status: '1',
-  remark: ''
+  remark: '',
+  photoUrls: ''
 })
 
 const requiredPersonnelCertNames = computed(() =>
@@ -559,6 +613,11 @@ const viewSections = computed<ViewSection[]>(() => {
       fieldLayout: 'table',
       tableMode: 'triple'
     },
+    {
+      title: '人员照片',
+      fields: [],
+      slot: 'photo'
+    },
     {
       title: `人员证书${
         viewDetail.value && !isCarrierPersonnel.value
@@ -697,7 +756,8 @@ function handleAdd() {
     personnelType: '',
     personnelAffiliation: affiliationMode.value,
     status: '1',
-    remark: ''
+    remark: '',
+    photoUrls: ''
   })
   resetCertList()
   dialogVisible.value = true
@@ -740,7 +800,8 @@ async function handleEdit(row: Personnel) {
       personnelType: detail.personnelType || 'both',
       personnelAffiliation: affiliationMode.value,
       status: detail.status,
-      remark: detail.remark || ''
+      remark: detail.remark || '',
+      photoUrls: detail.photoUrls || ''
     })
     certList.value = mergePersonnelCertSlots(
 
@@ -767,6 +828,10 @@ async function handleSubmit() {
     ElMessage.warning('证书附件上传中,请稍候')
     return
   }
+  if (photoUploadRef.value?.hasUploading?.()) {
+    ElMessage.warning('附件上传中,请稍候')
+    return
+  }
 
   submitLoading.value = true
   try {
@@ -779,6 +844,7 @@ async function handleSubmit() {
       personnelAffiliation: formData.personnelAffiliation,
       status: formData.status,
       remark: formData.remark || undefined,
+      photoUrls: formData.photoUrls || undefined,
       certList: certList.value.map(c => ({
         certId: c.certId,
         certName: c.certName.trim(),
@@ -866,4 +932,24 @@ onMounted(() => {
   flex-wrap: wrap;
   gap: 6px;
 }
+
+.pl-photo-thumb {
+  width: 48px;
+  height: 48px;
+  border-radius: 8px;
+  object-fit: cover;
+  border: 1px solid var(--el-border-color, #d9e8e2);
+  display: inline-block;
+  vertical-align: middle;
+  background: #f8fafc;
+}
+
+.pl-photo-thumb--empty {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 11px;
+  color: #94a3b8;
+  box-sizing: border-box;
+}
 </style>