增加客户照片上传的功能
This commit is contained in:
@@ -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 }))
|
||||
}
|
||||
|
||||
/** 客户照片上传至 MinIO;id 可选,有则作为 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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user