增加客户照片上传的功能

This commit is contained in:
zhonghua.li
2026-05-02 19:03:59 +08:00
parent 836974fc31
commit c73a2535fb
2 changed files with 293 additions and 4 deletions

View File

@@ -89,6 +89,68 @@ export function updateCustomer(data) {
})
}
function isLikelyBackendOrProxyAsset(absoluteUrl) {
try {
const p = new URL(absoluteUrl).pathname || ''
return p.startsWith('/api/') || p.includes('/customerManagement')
} catch (e) {
return false
}
}
/**
* 按列表/详情中保存的地址拉取图片:相对路径或指向本系统接口的完整 URL 走 axios含 Token、业务头经 dev 代理);
* 其余完整 URL如公网图床、MinIO 外链)返回 { useDirect: true, url },由前端用 img 直链展示。
*/
export function loadCustomerPhotoForDisplay(storedUrl) {
if (storedUrl == null || String(storedUrl).trim() === '') {
return Promise.reject(new Error('EMPTY_URL'))
}
const raw = String(storedUrl).trim()
const isAbs = /^https?:\/\//i.test(raw)
if (isAbs && !isLikelyBackendOrProxyAsset(raw)) {
return Promise.resolve({ useDirect: true, url: raw })
}
let path = raw
if (isAbs) {
try {
const u = new URL(raw)
path = u.pathname + (u.search || '')
} catch (e) {
return Promise.reject(e)
}
}
if (!path.startsWith('/')) {
path = `/${path}`
}
if (path.startsWith('/api/')) {
path = path.slice(4)
}
return request({
url: path,
method: 'get',
responseType: 'blob',
timeout: 120000
}).then(blob => ({ useDirect: false, blob }))
}
/** 客户照片上传至 MinIOid 可选,有则作为 query 传入;成功时响应 data 中含 url */
export function uploadCustomerPhoto(customerId, file) {
const formData = new FormData()
formData.append('file', file)
const config = {
url: '/customerManagement/uploadPhoto',
method: 'post',
data: formData,
timeout: 120000
}
if (customerId != null && String(customerId).trim() !== '') {
config.params = { id: String(customerId).trim() }
}
return request(config)
}
// 获取客户统计信息
export function getCustomerStatistics() {
return request({

View File

@@ -102,6 +102,22 @@
<el-table-column type="selection" width="55" />
<el-table-column label="序号" type="index" width="50" />
<el-table-column label="客户姓名" prop="customerName" min-width="120" />
<el-table-column label="照片" width="168" align="center">
<template slot-scope="scope">
<el-select
v-model="scope.row._listPhotoKind"
placeholder="选择照片"
size="mini"
class="list-photo-select"
clearable
@change="(v) => handleListPhotoTypeChange(scope.row, v)"
>
<el-option label="显示正面照片" value="front" />
<el-option label="显示侧面照片" value="side" />
<el-option label="显示生活照片" value="life" />
</el-select>
</template>
</el-table-column>
<el-table-column label="联系方式" prop="contact" min-width="150" />
<el-table-column label="所属机构" prop="dealershipName" min-width="150" />
<el-table-column label="所属销售" prop="salesName" min-width="150" />
@@ -264,14 +280,42 @@
/>
</el-form-item>
<el-form-item label="正面照片URL" prop="frontPhotoUrl">
<el-input v-model="customerForm.frontPhotoUrl" placeholder="请输入正面照片 URL" />
<div class="photo-url-row">
<el-input v-model="customerForm.frontPhotoUrl" class="photo-url-input" placeholder="上传后自动填入,也可手动输入 URL" />
<el-button
type="primary"
:loading="photoUploadLoading.frontPhotoUrl"
@click="openCustomerPhotoUpload('frontPhotoUrl')"
>上传</el-button>
</div>
</el-form-item>
<el-form-item label="侧面照片URL" prop="sidePhotoUrl">
<el-input v-model="customerForm.sidePhotoUrl" placeholder="请输入侧面照片 URL" />
<div class="photo-url-row">
<el-input v-model="customerForm.sidePhotoUrl" class="photo-url-input" placeholder="上传后自动填入,也可手动输入 URL" />
<el-button
type="primary"
:loading="photoUploadLoading.sidePhotoUrl"
@click="openCustomerPhotoUpload('sidePhotoUrl')"
>上传</el-button>
</div>
</el-form-item>
<el-form-item label="生活照URL" prop="lifePhotoUrl">
<el-input v-model="customerForm.lifePhotoUrl" placeholder="请输入生活照 URL" />
<div class="photo-url-row">
<el-input v-model="customerForm.lifePhotoUrl" class="photo-url-input" placeholder="上传后自动填入,也可手动输入 URL" />
<el-button
type="primary"
:loading="photoUploadLoading.lifePhotoUrl"
@click="openCustomerPhotoUpload('lifePhotoUrl')"
>上传</el-button>
</div>
</el-form-item>
<input
ref="customerPhotoFileInput"
type="file"
accept="image/*"
class="customer-photo-file-input"
@change="onCustomerPhotoFileSelected"
>
<el-form-item label="备注" prop="remark">
<el-input
v-model="customerForm.remark"
@@ -502,6 +546,28 @@
<el-button type="primary" @click="handleCommunicationSubmit"> </el-button>
</div>
</el-dialog>
<!-- 列表中按类型查看客户照片 -->
<el-dialog
:title="photoPreviewTitle"
:visible.sync="photoPreviewVisible"
width="560px"
append-to-body
@closed="onPhotoPreviewDialogClosed"
>
<div v-loading="photoPreviewLoading" class="customer-photo-preview-wrap">
<template v-if="photoPreviewSrc">
<img
v-if="!photoPreviewLoadError"
:src="photoPreviewSrc"
alt="客户照片"
class="customer-photo-preview-img"
@error="photoPreviewLoadError = true"
>
<span v-else class="customer-photo-preview-fallback">图片加载失败请检查地址或权限</span>
</template>
</div>
</el-dialog>
</div>
</template>
@@ -511,6 +577,8 @@ import {
getCustomerById,
addCustomer,
updateCustomer,
uploadCustomerPhoto,
loadCustomerPhotoForDisplay,
deleteCustomer,
batchDeleteCustomer,
exportCustomerFlow,
@@ -535,6 +603,12 @@ export default {
dialogVisible: false,
dialogTitle: '',
isEdit: false,
customerPhotoUploadField: '',
photoUploadLoading: {
frontPhotoUrl: false,
sidePhotoUrl: false,
lifePhotoUrl: false
},
customerForm: {
id: undefined,
customerName: '',
@@ -597,7 +671,13 @@ export default {
communicationContent: [{ required: true, message: '请输入沟通内容', trigger: 'blur' }],
communicationResult: [{ required: true, message: '请输入沟通结果', trigger: 'blur' }],
ownerName: [{ required: true, message: '请输入所属人姓名', trigger: 'blur' }]
}
},
photoPreviewVisible: false,
photoPreviewTitle: '',
photoPreviewSrc: '',
photoPreviewObjectUrl: '',
photoPreviewLoading: false,
photoPreviewLoadError: false
}
},
created() {
@@ -893,9 +973,114 @@ export default {
// 对话框关闭
handleDialogClose() {
this.customerPhotoUploadField = ''
this.$refs.customerForm.resetFields()
},
openCustomerPhotoUpload(formField) {
this.customerPhotoUploadField = formField
const el = this.$refs.customerPhotoFileInput
if (el) el.click()
},
async handleListPhotoTypeChange(row, kind) {
this.$nextTick(() => {
this.$set(row, '_listPhotoKind', undefined)
})
if (!kind) return
const fieldMap = { front: 'frontPhotoUrl', side: 'sidePhotoUrl', life: 'lifePhotoUrl' }
const titleMap = { front: '正面照片', side: '侧面照片', life: '生活照片' }
const field = fieldMap[kind]
const stored = field && row[field] ? String(row[field]).trim() : ''
if (!stored) {
this.$message.warning(`该客户未设置${titleMap[kind] || '该类型'}地址`)
return
}
this.photoPreviewTitle = `${row.customerName || '客户'}${titleMap[kind]}`
this.photoPreviewLoadError = false
this.revokePhotoPreviewObjectUrl()
this.photoPreviewSrc = ''
this.photoPreviewVisible = true
this.photoPreviewLoading = true
try {
const result = await loadCustomerPhotoForDisplay(stored)
if (result.useDirect) {
this.photoPreviewSrc = result.url
} else {
const blob = result.blob
if (!(blob instanceof Blob) || blob.size === 0) {
this.$message.error('未获取到图片数据')
return
}
const ct = (blob.type || '').toLowerCase()
if (ct.includes('json') || ct === 'text/plain') {
const text = await blob.text()
let msg = '加载失败'
try {
const j = JSON.parse(text)
msg = j.message || j.msg || msg
} catch (e) {
if (text) msg = text.slice(0, 200)
}
this.$message.error(msg)
this.photoPreviewVisible = false
return
}
this.photoPreviewObjectUrl = URL.createObjectURL(blob)
this.photoPreviewSrc = this.photoPreviewObjectUrl
}
} catch (err) {
console.error('加载客户照片失败:', err)
this.$message.error((err && err.message) || '加载照片失败,请稍后重试')
this.photoPreviewVisible = false
} finally {
this.photoPreviewLoading = false
}
},
revokePhotoPreviewObjectUrl() {
if (this.photoPreviewObjectUrl) {
URL.revokeObjectURL(this.photoPreviewObjectUrl)
this.photoPreviewObjectUrl = ''
}
},
onPhotoPreviewDialogClosed() {
this.revokePhotoPreviewObjectUrl()
this.photoPreviewSrc = ''
this.photoPreviewTitle = ''
this.photoPreviewLoadError = false
},
async onCustomerPhotoFileSelected(e) {
const input = e.target
const file = input.files && input.files[0]
const formField = this.customerPhotoUploadField
this.customerPhotoUploadField = ''
input.value = ''
if (!file || !formField) return
this.photoUploadLoading[formField] = true
try {
const response = await uploadCustomerPhoto(this.customerForm.id, file)
const payload = response && response.data
const url = payload && payload.url
if (url) {
this.customerForm[formField] = url
this.$message.success((payload && payload.message) || response.message || '上传成功')
} else {
this.$message.error('上传成功但未返回文件地址')
}
} catch (err) {
console.error('客户照片上传失败:', err)
} finally {
this.photoUploadLoading[formField] = false
}
},
// 导出客流
handleExport() {
const params = { ...this.queryParams }
@@ -1157,6 +1342,26 @@ export default {
text-align: right;
}
.photo-url-row {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.photo-url-input {
flex: 1;
min-width: 0;
}
.customer-photo-file-input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
overflow: hidden;
}
.el-table .el-button--text {
font-size: 12px;
}
@@ -1168,4 +1373,26 @@ export default {
.el-table .el-button--text.el-button--danger:hover {
color: #F56C6C;
}
.list-photo-select {
width: 100%;
}
.customer-photo-preview-wrap {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.customer-photo-preview-img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.customer-photo-preview-fallback {
color: #909399;
font-size: 14px;
}
</style>