diff --git a/src/api/customer.js b/src/api/customer.js
index b6ff49c..1e7be9e 100644
--- a/src/api/customer.js
+++ b/src/api/customer.js
@@ -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({
diff --git a/src/views/customer/index.vue b/src/views/customer/index.vue
index 209b128..760e426 100644
--- a/src/views/customer/index.vue
+++ b/src/views/customer/index.vue
@@ -102,6 +102,22 @@
+
+
+ handleListPhotoTypeChange(scope.row, v)"
+ >
+
+
+
+
+
+
@@ -264,14 +280,42 @@
/>
-
+
+
+ 上传
+
-
+
+
+ 上传
+
-
+
+
+ 上传
+
+
提 交
+
+
+
+
+
+
+ 图片加载失败,请检查地址或权限
+
+
+
@@ -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;
+}