From 39f6fca28e3f78abe48f08e474ec011abb990b36 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期三, 09 九月 2026 10:39:21 +0800
Subject: [PATCH] update

---
 frontend/src/views/GuideUserDetailView.vue |  143 +++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 129 insertions(+), 14 deletions(-)

diff --git a/frontend/src/views/GuideUserDetailView.vue b/frontend/src/views/GuideUserDetailView.vue
index 3c90eb7..e2c9e97 100644
--- a/frontend/src/views/GuideUserDetailView.vue
+++ b/frontend/src/views/GuideUserDetailView.vue
@@ -1,23 +1,31 @@
 <script setup>
-import { computed, onMounted, ref } from 'vue'
+import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { ArrowLeft, Check, Plus, Upload } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import {
   addGuideScenicConfig,
   deleteGuideScenicConfig,
+  getGuideUserDocumentBlob,
   getGuideUserDetail,
   updateGuideUserDetail,
   updateGuideWhitelist,
+  uploadGuideUserDocument,
+  uploadGuideUserAvatar,
 } from '@/api/users'
+import { getGuideTeamDetail } from '@/api/guideTeams'
 import { getScenicSpots } from '@/api/scenicSpots'
 
 const route = useRoute()
 const router = useRouter()
 const loading = ref(false)
 const saving = ref(false)
+const avatarUploading = ref(false)
+const documentUploadingKey = ref('')
+const documentPreviewUrls = ref({})
 const user = ref(null)
 const scenicOptions = ref([])
+const teamScenicNames = ref(null)
 const scenicPopoverVisible = ref(false)
 const selectedScenicSpotId = ref(null)
 const scenicSaving = ref(false)
@@ -35,13 +43,50 @@
 const guideStatusLabel = (status) => ({ 0: '禁用', 1: '启用', 2: '待审核', 3: '已注销' }[status] || '未知')
 const availableScenicOptions = computed(() => {
   const configuredNames = new Set((user.value?.scenicConfigs || []).map((item) => item.scenicName))
-  return scenicOptions.value.filter((item) => !configuredNames.has(item.scenicName))
+  return scenicOptions.value.filter((item) => (
+    !configuredNames.has(item.scenicName)
+    && (!teamScenicNames.value || teamScenicNames.value.has(item.scenicName))
+  ))
 })
+
+const revokeDocumentPreviews = () => {
+  Object.values(documentPreviewUrls.value).forEach((url) => {
+    if (url?.startsWith('blob:')) URL.revokeObjectURL(url)
+  })
+  documentPreviewUrls.value = {}
+}
+
+const loadDocumentPreview = async (documentType, documentUrl) => {
+  const previousUrl = documentPreviewUrls.value[documentType]
+  if (previousUrl?.startsWith('blob:')) URL.revokeObjectURL(previousUrl)
+  if (!documentUrl) {
+    delete documentPreviewUrls.value[documentType]
+    return
+  }
+  try {
+    const blob = await getGuideUserDocumentBlob(documentUrl)
+    documentPreviewUrls.value[documentType] = blob ? URL.createObjectURL(blob) : documentUrl
+  } catch (error) {
+    delete documentPreviewUrls.value[documentType]
+  }
+}
+
+const loadDocumentPreviews = async () => {
+  revokeDocumentPreviews()
+  await Promise.all(documentImages.value.map((item) => loadDocumentPreview(item.key, user.value?.[item.key])))
+}
 
 const loadDetail = async () => {
   loading.value = true
   try {
     user.value = await getGuideUserDetail(route.params.id)
+    if (user.value.teamId) {
+      const team = await getGuideTeamDetail(user.value.teamId)
+      teamScenicNames.value = new Set((team.scenicConfigs || []).map((item) => item.scenicName))
+    } else {
+      teamScenicNames.value = null
+    }
+    await loadDocumentPreviews()
   } finally {
     loading.value = false
   }
@@ -52,6 +97,52 @@
   scenicOptions.value = result.records || []
 }
 
+const validateImage = (file, type) => {
+  if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
+    ElMessage.warning(`${type}仅支持 JPG、PNG 或 WebP 格式`)
+    return false
+  }
+  if (file.size > 5 * 1024 * 1024) {
+    ElMessage.warning(`${type}不能超过5MB`)
+    return false
+  }
+  return true
+}
+
+const beforeAvatarUpload = (file) => validateImage(file, '头像图片')
+
+const uploadAvatar = async ({ file, onSuccess, onError }) => {
+  avatarUploading.value = true
+  try {
+    const updatedUser = await uploadGuideUserAvatar(user.value.id, file)
+    user.value.avatar = updatedUser.avatar
+    onSuccess?.(updatedUser)
+    ElMessage.success('头像上传成功')
+  } catch (error) {
+    onError?.(error)
+  } finally {
+    avatarUploading.value = false
+  }
+}
+
+const beforeDocumentUpload = (file) => validateImage(file, '证件图片')
+
+const uploadDocument = async (documentType, { file, onSuccess, onError }) => {
+  if (documentUploadingKey.value) return
+  documentUploadingKey.value = documentType
+  try {
+    const result = await uploadGuideUserDocument(user.value.id, documentType, file)
+    user.value[documentType] = result.url
+    await loadDocumentPreview(documentType, result.url)
+    onSuccess?.(result)
+    ElMessage.success('证件图片上传成功')
+  } catch (error) {
+    onError?.(error)
+  } finally {
+    documentUploadingKey.value = ''
+  }
+}
+
 const updateWhitelist = async (enabled) => {
   await updateGuideWhitelist(user.value.id, enabled)
   ElMessage.success(enabled ? '已加入自动禁用白名单' : '已移出自动禁用白名单')
@@ -60,7 +151,7 @@
 const openScenicSelector = () => {
   selectedScenicSpotId.value = null
   if (!availableScenicOptions.value.length) {
-    ElMessage.info('暂无可添加的启用景区')
+    ElMessage.info(user.value.teamId ? '所属讲解队暂无可添加的启用景区' : '暂无可添加的启用景区')
     return
   }
   scenicPopoverVisible.value = true
@@ -137,6 +228,7 @@
 })
 
 onMounted(() => Promise.all([loadDetail(), loadScenicOptions()]))
+onBeforeUnmount(revokeDocumentPreviews)
 </script>
 
 <template>
@@ -152,16 +244,26 @@
         <div class="avatar-row">
           <span class="field-label">导游头像<br>(非必填)</span>
           <div class="image-box">
-            <el-image v-if="user.avatar" :src="user.avatar" fit="cover" />
+            <el-image v-if="user.avatar" :src="user.avatar" fit="cover" :preview-src-list="[user.avatar]" preview-teleported />
             <span v-else>暂无</span>
           </div>
-          <el-input v-model="user.avatar" class="image-url-input" clearable placeholder="请输入头像图片地址" />
-          <el-button type="primary" plain :icon="Upload" @click="ElMessage.info('图片上传服务接入后可直接上传,当前请填写图片地址')">上传</el-button>
+          <el-upload
+            accept="image/jpeg,image/png,image/webp"
+            :show-file-list="false"
+            :before-upload="beforeAvatarUpload"
+            :http-request="uploadAvatar"
+          >
+            <el-button type="primary" plain :icon="Upload" :loading="avatarUploading">上传图片</el-button>
+          </el-upload>
         </div>
 
         <div class="field-grid">
           <div class="field-item"><span class="field-label">导游编号</span><div class="field-value muted">{{ user.id }}</div></div>
-          <div class="field-item"><span class="field-label">导游手机号*</span><el-input v-model="user.mobile" /></div>
+          <div class="field-item mobile-field-item">
+            <span class="field-label">导游手机号*</span>
+            <el-input v-model="user.mobile" />
+            <small class="field-hint">修改会影响导游登录和游客联系</small>
+          </div>
           <div class="field-item"><span class="field-label">注册时间</span><div class="field-value muted">{{ formatDateTime(user.createdAt) }}</div></div>
           <div class="field-item"><span class="field-label">账号状态</span><div class="field-value muted">{{ guideStatusLabel(user.status) }}</div></div>
           <div class="field-item"><span class="field-label">姓名*</span><el-input v-model="user.realName" /></div>
@@ -169,11 +271,9 @@
             <span class="field-label">性别(非必填)</span>
             <el-select v-model="user.gender" clearable><el-option label="男" :value="1" /><el-option label="女" :value="2" /></el-select>
           </div>
-          <div class="field-item"><span class="field-label">出生日期</span><el-date-picker v-model="user.birthDate" value-format="YYYY-MM-DD" /></div>
           <div class="field-item"><span class="field-label">年龄(非必填)</span><div class="field-value">{{ displayValue(user.age) }}</div></div>
           <div class="field-item"><span class="field-label">工作年限<br>(非必填)</span><el-input-number v-model="user.workYears" :min="0" :max="100" controls-position="right" /></div>
           <div class="field-item"><span class="field-label">身份证号*</span><el-input v-model="user.idCardNo" maxlength="18" /></div>
-          <div class="field-item"><span class="field-label">导游证号</span><el-input v-model="user.certificateNo" /></div>
         </div>
 
         <div class="document-grid">
@@ -181,12 +281,26 @@
             <span class="field-label">{{ item.label }}</span>
             <div class="document-control">
               <div class="image-box">
-                <el-image v-if="user[item.key]" :src="user[item.key]" fit="cover" :preview-src-list="[user[item.key]]" />
-                <span v-else>暂无</span>
+                <el-image
+                  v-if="documentPreviewUrls[item.key]"
+                  :src="documentPreviewUrls[item.key]"
+                  fit="cover"
+                  :preview-src-list="[documentPreviewUrls[item.key]]"
+                  preview-teleported
+                />
+                <span v-else>{{ user[item.key] ? '加载失败' : '暂无' }}</span>
               </div>
               <div class="document-actions">
-                <el-link v-if="user[item.key]" type="primary" :href="user[item.key]" target="_blank">查看大图</el-link>
-                <el-button type="primary" plain :icon="Upload" @click="ElMessage.info('图片上传服务接入后可直接上传')">上传</el-button>
+                <el-link v-if="documentPreviewUrls[item.key]" type="primary" :href="documentPreviewUrls[item.key]" target="_blank">查看大图</el-link>
+                <el-upload
+                  accept="image/jpeg,image/png,image/webp"
+                  :show-file-list="false"
+                  :disabled="documentUploadingKey !== ''"
+                  :before-upload="beforeDocumentUpload"
+                  :http-request="(options) => uploadDocument(item.key, options)"
+                >
+                  <el-button type="primary" plain :icon="Upload" :loading="documentUploadingKey === item.key">上传图片</el-button>
+                </el-upload>
               </div>
             </div>
           </div>
@@ -205,7 +319,7 @@
         <div class="scenic-config">
           <div class="scenic-heading">
             <span class="field-label">景区配置</span>
-            <small>导游有配置讲解队时,景区配置会以讲解队配置为优先</small>
+            <small>导游属于讲解队时,仅可配置该讲解队下的景区</small>
           </div>
           <el-table :data="user.scenicConfigs" border class="scenic-table">
             <el-table-column prop="scenicName" label="景区名称" align="center" />
@@ -280,6 +394,7 @@
 .image-box :deep(.el-image) { width: 100%; height: 100%; }
 .field-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 26px 42px; }
 .field-item { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; gap: 10px; }
+.field-hint { grid-column: 1 / -1; color: #f05b61; font-size: 12px; line-height: 1.4; }
 .field-value { min-height: 34px; padding: 8px 10px; color: #667085; background: #f5f7fa; }
 .field-value.muted { background: #dedede; }
 .document-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px 72px; margin-top: 32px; }

--
Gitblit v1.7.1