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