增加客户照片上传的功能
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() {
|
export function getCustomerStatistics() {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@@ -102,6 +102,22 @@
|
|||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column label="序号" type="index" width="50" />
|
<el-table-column label="序号" type="index" width="50" />
|
||||||
<el-table-column label="客户姓名" prop="customerName" min-width="120" />
|
<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="contact" min-width="150" />
|
||||||
<el-table-column label="所属机构" prop="dealershipName" min-width="150" />
|
<el-table-column label="所属机构" prop="dealershipName" min-width="150" />
|
||||||
<el-table-column label="所属销售" prop="salesName" min-width="150" />
|
<el-table-column label="所属销售" prop="salesName" min-width="150" />
|
||||||
@@ -264,14 +280,42 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="正面照片URL" prop="frontPhotoUrl">
|
<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>
|
||||||
<el-form-item label="侧面照片URL" prop="sidePhotoUrl">
|
<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>
|
||||||
<el-form-item label="生活照URL" prop="lifePhotoUrl">
|
<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>
|
</el-form-item>
|
||||||
|
<input
|
||||||
|
ref="customerPhotoFileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="customer-photo-file-input"
|
||||||
|
@change="onCustomerPhotoFileSelected"
|
||||||
|
>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="customerForm.remark"
|
v-model="customerForm.remark"
|
||||||
@@ -502,6 +546,28 @@
|
|||||||
<el-button type="primary" @click="handleCommunicationSubmit">提 交</el-button>
|
<el-button type="primary" @click="handleCommunicationSubmit">提 交</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -511,6 +577,8 @@ import {
|
|||||||
getCustomerById,
|
getCustomerById,
|
||||||
addCustomer,
|
addCustomer,
|
||||||
updateCustomer,
|
updateCustomer,
|
||||||
|
uploadCustomerPhoto,
|
||||||
|
loadCustomerPhotoForDisplay,
|
||||||
deleteCustomer,
|
deleteCustomer,
|
||||||
batchDeleteCustomer,
|
batchDeleteCustomer,
|
||||||
exportCustomerFlow,
|
exportCustomerFlow,
|
||||||
@@ -535,6 +603,12 @@ export default {
|
|||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: '',
|
dialogTitle: '',
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
|
customerPhotoUploadField: '',
|
||||||
|
photoUploadLoading: {
|
||||||
|
frontPhotoUrl: false,
|
||||||
|
sidePhotoUrl: false,
|
||||||
|
lifePhotoUrl: false
|
||||||
|
},
|
||||||
customerForm: {
|
customerForm: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
customerName: '',
|
customerName: '',
|
||||||
@@ -597,7 +671,13 @@ export default {
|
|||||||
communicationContent: [{ required: true, message: '请输入沟通内容', trigger: 'blur' }],
|
communicationContent: [{ required: true, message: '请输入沟通内容', trigger: 'blur' }],
|
||||||
communicationResult: [{ required: true, message: '请输入沟通结果', trigger: 'blur' }],
|
communicationResult: [{ required: true, message: '请输入沟通结果', trigger: 'blur' }],
|
||||||
ownerName: [{ required: true, message: '请输入所属人姓名', trigger: 'blur' }]
|
ownerName: [{ required: true, message: '请输入所属人姓名', trigger: 'blur' }]
|
||||||
}
|
},
|
||||||
|
photoPreviewVisible: false,
|
||||||
|
photoPreviewTitle: '',
|
||||||
|
photoPreviewSrc: '',
|
||||||
|
photoPreviewObjectUrl: '',
|
||||||
|
photoPreviewLoading: false,
|
||||||
|
photoPreviewLoadError: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -893,9 +973,114 @@ export default {
|
|||||||
|
|
||||||
// 对话框关闭
|
// 对话框关闭
|
||||||
handleDialogClose() {
|
handleDialogClose() {
|
||||||
|
this.customerPhotoUploadField = ''
|
||||||
this.$refs.customerForm.resetFields()
|
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() {
|
handleExport() {
|
||||||
const params = { ...this.queryParams }
|
const params = { ...this.queryParams }
|
||||||
@@ -1157,6 +1342,26 @@ export default {
|
|||||||
text-align: right;
|
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 {
|
.el-table .el-button--text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -1168,4 +1373,26 @@ export default {
|
|||||||
.el-table .el-button--text.el-button--danger:hover {
|
.el-table .el-button--text.el-button--danger:hover {
|
||||||
color: #F56C6C;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user