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 @@ + + + @@ -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; +}