采购人走用户的表

This commit is contained in:
dxc
2026-02-10 17:20:06 +08:00
parent b5b0677b01
commit 8ee2a9a45b
4 changed files with 254 additions and 78 deletions

View File

@ -47,17 +47,17 @@ export function searchMaterialBase(keyword: string) {
// 6. 文件上传 (用于图片/拍照)
export function uploadFile(data: FormData) {
return request({
url: '/common/upload', // 对应后端 /api/v1/common/upload
url: '/common/upload',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 7. [新增] 文件删除
// 7. 文件删除
export function deleteFile(filename: string) {
return request({
url: `/common/files/${filename}`, // 对应后端 /api/v1/common/files/<filename>
url: `/common/files/${filename}`,
method: 'delete'
})
}
@ -71,7 +71,7 @@ export function getSupplierSuggestions(params: any) {
})
}
// 9. 用户建议
// 9. 用户建议 (采购人)
export function getUserSuggestions(params: any) {
return request({
url: '/inbound/buy/suggestions/users',
@ -79,3 +79,12 @@ export function getUserSuggestions(params: any) {
params
})
}
// 10. [新增] 链接建议
export function getLinkSuggestions(params: any) {
return request({
url: '/inbound/buy/suggestions/links',
method: 'get',
params
})
}

View File

@ -318,7 +318,15 @@
<el-col :span="24">
<el-form-item label="到货图片" prop="arrival_photo">
<div class="upload-container">
<el-upload v-model:file-list="arrivalFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'arrival_photo')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'arrival_photo')" :before-upload="beforeAvatarUpload">
<el-upload
v-model:file-list="arrivalFileList"
action="#"
list-type="picture-card"
multiple
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
:on-preview="handlePreviewPicture"
:on-remove="(file) => handleRemoveImage(file, 'arrival_photo')"
:before-upload="beforeAvatarUpload">
<el-icon><Plus /></el-icon>
</el-upload>
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
@ -329,7 +337,15 @@
<el-col :span="24">
<el-form-item label="检测报告" prop="inspection_report">
<div class="upload-container">
<el-upload v-model:file-list="reportFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'inspection_report')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'inspection_report')" :before-upload="beforeAvatarUpload">
<el-upload
v-model:file-list="reportFileList"
action="#"
list-type="picture-card"
multiple
:http-request="(opts) => customUpload(opts, 'inspection_report')"
:on-preview="handlePreviewPicture"
:on-remove="(file) => handleRemoveImage(file, 'inspection_report')"
:before-upload="beforeAvatarUpload">
<el-icon><Plus /></el-icon>
</el-upload>
<div class="camera-card" @click="triggerCamera('inspection_report')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
@ -354,13 +370,76 @@
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="供应商"><el-autocomplete v-model="form.supplier_name" :fetch-suggestions="querySearchSupplier" placeholder="输入或选择供应商" style="width: 100%" clearable :trigger-on-focus="true" @select="handleSupplierSelect"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="采购人"><el-autocomplete v-model="form.purchaser" :fetch-suggestions="querySearchPurchaser" placeholder="输入采购人" style="width: 100%" clearable :trigger-on-focus="true" @select="handlePurchaserSelect"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="采购邮箱"><el-autocomplete v-model="form.purchaser_email" :fetch-suggestions="querySearchEmail" placeholder="输入邮箱" style="width: 100%" clearable :trigger-on-focus="true" @select="handleEmailSelect"/></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="供应商">
<el-autocomplete
v-model="form.supplier_name"
:fetch-suggestions="querySearchSupplier"
placeholder="输入或选择供应商"
style="width: 100%"
clearable
:trigger-on-focus="true"
@select="handleSupplierSelect"
>
<template #default="{ item }">
<div style="font-weight: 500">{{ item.value }}</div>
</template>
</el-autocomplete>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="采购人">
<el-autocomplete
v-model="form.purchaser"
:fetch-suggestions="querySearchPurchaser"
placeholder="输入采购人"
style="width: 100%"
clearable
:trigger-on-focus="true"
@select="handlePurchaserSelect"
>
<template #default="{ item }">
<span>{{ item.value }}</span>
<span v-if="item.email" style="float: right; color: #999; font-size: 12px; margin-left:10px">{{ item.email }}</span>
</template>
</el-autocomplete>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="采购邮箱">
<el-input v-model="form.purchaser_email" placeholder="自动填充或手动输入" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" placeholder="http://"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="http://"/></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="原始链接">
<el-autocomplete
v-model="form.source_link"
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'original')"
placeholder="http://"
style="width: 100%"
clearable
:trigger-on-focus="true"
>
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
</el-autocomplete>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详情链接">
<el-autocomplete
v-model="form.detail_link"
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'detail')"
placeholder="http://"
style="width: 100%"
clearable
:trigger-on-focus="true"
>
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
</el-autocomplete>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
@ -402,7 +481,6 @@
<script setup lang="ts">
import {ref, reactive, onMounted, watch} from 'vue'
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue'
// 修改:引入 ElLoading
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
import dayjs from 'dayjs'
import {
@ -412,11 +490,17 @@ import {
deleteBuyInbound,
searchMaterialBase,
uploadFile,
deleteFile
deleteFile,
getSupplierSuggestions,
getUserSuggestions,
getLinkSuggestions
} from '@/api/inbound/buy'
import {getLabelPreview, executePrint} from '@/api/common/print'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
// 获取环境变量中的 API Base URL用于图片拼接
const apiBaseUrl = import.meta.env.VITE_APP_BASE_API || ''
// ------------------------------------
// 状态与变量
// ------------------------------------
@ -510,9 +594,14 @@ const form = reactive({
})
// 供应商建议 API
// ------------------------------------
// 建议/Autocomplete 逻辑
// ------------------------------------
// 1. 供应商建议 (基于 base_id)
const fetchSupplierSuggestions = async (query: string, cb: any) => {
if (!form.base_id) {
// 如果没有选物料,不给建议,或者可以给空
cb([])
return
}
@ -529,12 +618,17 @@ const fetchSupplierSuggestions = async (query: string, cb: any) => {
cb([])
}
}
const querySearchSupplier = (qs: string, cb: any) => fetchSupplierSuggestions(qs, cb)
const handleSupplierSelect = (item: any) => {
form.supplier_name = item.value
}
// 用户建议 API
// 2. 采购人建议 (全局搜索 + 系统用户)
const fetchUserSuggestions = async (query: string, cb: any) => {
try {
const res: any = await getUserSuggestions({ keyword: query })
if (res.code === 200) {
// 假设后端返回 [{value: '张三', email: 'zhangsan@xxx.com'}, ...]
const users = res.data.map((user: any) => ({ value: user.value, email: user.email }))
cb(users)
} else {
@ -544,24 +638,31 @@ const fetchUserSuggestions = async (query: string, cb: any) => {
cb([])
}
}
const querySearchSupplier = (qs: string, cb: any) => fetchSupplierSuggestions(qs, cb)
const handleSupplierSelect = (item: any) => {
form.supplier_name = item.value
}
const querySearchPurchaser = (qs: string, cb: any) => fetchUserSuggestions(qs, cb)
const handlePurchaserSelect = (item: any) => {
form.purchaser = item.value
form.purchaser_email = item.email || ''
// 核心:选中采购人时,自动填入邮箱
if (item.email) {
form.purchaser_email = item.email
}
}
const querySearchEmail = (qs: string, cb: any) => fetchUserSuggestions(qs, (users: any[]) => {
const emailUsers = users.filter(u => u.email).map(u => ({ value: u.email }))
const filtered = qs ? emailUsers.filter((item: any) => item.value.toLowerCase().includes(qs.toLowerCase())) : emailUsers
cb(filtered)
})
const handleEmailSelect = (item: any) => {
form.purchaser_email = item.value
// 3. 链接建议 (基于 base_id)
const fetchLinkSuggestions = async (query: string, cb: any, type: 'original' | 'detail') => {
if (!form.base_id) { cb([]); return }
try {
const res: any = await getLinkSuggestions({ base_id: form.base_id, type })
if (res.code === 200) {
// 后端返回 ['http://...', 'http://...']
const links = res.data.map((link: string) => ({ value: link }))
const filtered = query ? links.filter((item:any) => item.value.toLowerCase().includes(query.toLowerCase())) : links
cb(filtered)
} else { cb([]) }
} catch(e) { cb([]) }
}
const querySearchLinks = (qs: string, cb: any, type: 'original' | 'detail') => fetchLinkSuggestions(qs, cb, type)
// 4. 币种
const currencyOptions = [{value: 'CNY', desc: '人民币'}, {value: 'USD', desc: '美元'}, {value: 'EUR', desc: '欧元'}]
const querySearchCurrency = (queryString: string, cb: any) => {
const filtered = queryString ? currencyOptions.filter(item => item.value.toLowerCase().includes(queryString.toLowerCase()) || item.desc.toLowerCase().includes(queryString.toLowerCase())) : currencyOptions
@ -585,6 +686,10 @@ const onMaterialSelected = (val: number) => {
form.category = item.category
form.unit = item.unit
form.material_type = item.type
// 切换物料后,清空跟物料相关的供应商、链接,因为它们不再适用新物料
// form.supplier_name = '' // 可选:是否清空
// form.source_link = ''
// form.detail_link = ''
checkHistoryAndSetMode(item.id)
}
}
@ -702,6 +807,7 @@ const handleUpdate = (row: any) => {
source_link: row.source_link, detail_link: row.detail_link,
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
})
// 核心:回显图片时,使用 getImageUrl 补全路径
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
const reports = form.inspection_report || []
const reportImgs = reports.filter(r => !isExternalLink(r))
@ -749,16 +855,34 @@ const submitForm = async () => {
})
}
// 其他辅助函数保持不变
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
// ------------------------------------
// 图片/文件处理 (核心修复)
// ------------------------------------
// 1. 路径补全如果是http开头则直接用否则拼接 apiBaseUrl
const getImageUrl = (url: string) => {
if (!url) return ''
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('blob:')) {
return url
}
// 拼接 API 基础路径,例如 http://localhost:5000 + /static/files/xxx.jpg
// 注意处理斜杠,防止双斜杠
const baseUrl = apiBaseUrl.endsWith('/') ? apiBaseUrl.slice(0, -1) : apiBaseUrl
const path = url.startsWith('/') ? url : '/' + url
return baseUrl + path
}
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) }
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false }
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
return true
}
// 2. 自定义上传 (修复回显)
const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => {
const { file, onSuccess, onError } = options
const formData = new FormData()
@ -766,38 +890,64 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec
try {
const res: any = await uploadFile(formData)
if (res.code === 200) {
const newUrl = res.data.url
const newUrl = res.data.url // 后端返回的相对路径
// 1. 存入表单数据
form[targetField].push(newUrl)
// 2. 核心修复:显式更新 fileList 以确保缩略图显示
// 需要拼接完整路径用于展示
const fullUrl = getImageUrl(newUrl)
const fileObj = { name: file.name, url: fullUrl, status: 'success', uid: file.uid }
if (targetField === 'arrival_photo') {
// 替换或追加
const idx = arrivalFileList.value.findIndex(f => f.uid === file.uid)
if (idx > -1) arrivalFileList.value[idx] = fileObj
else arrivalFileList.value.push(fileObj)
} else {
const idx = reportFileList.value.findIndex(f => f.uid === file.uid)
if (idx > -1) reportFileList.value[idx] = fileObj
else reportFileList.value.push(fileObj)
}
ElMessage.success('上传成功')
onSuccess(res)
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
} catch (e) { ElMessage.error('网络错误'); onError(e) }
} else {
ElMessage.error(res.msg || '上传失败')
onError(new Error(res.msg))
}
} catch (e) {
ElMessage.error('网络错误')
onError(e)
}
}
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => {
try {
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
// 这里需要反向查找,因为 uploadFile.url 可能是带域名的完整路径,而 form 里存的是相对路径
// 简单比对末尾文件名
const filename = uploadFile.url.split('/').pop()
const urlToRemove = form[targetField].find(u => u.endsWith(filename)) || uploadFile.url
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) }
if (!isExternalLink(urlToRemove)) {
if (filename) await deleteFile(filename)
}
ElMessage.success('已删除')
} catch (e) { console.error(e) }
}
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
currentCameraField.value = field;
cameraDialogVisible.value = true;
}
// ----------------------------------------------------
// 【修复核心】:处理拍照上传
// ----------------------------------------------------
const handleCameraConfirm = async (file: File) => {
console.log('✅ 父组件收到照片:', file.name, file.size)
if (!beforeAvatarUpload(file)) {
return
}
// 修复点:使用 ElLoading.service 替代报错的 ElMessage.loading
const loadingInstance = ElLoading.service({
lock: true,
text: '照片上传中,请稍候...',
@ -807,35 +957,32 @@ const handleCameraConfirm = async (file: File) => {
try {
const formData = new FormData()
formData.append('file', file)
console.log('🚀 开始上传...')
const res: any = await uploadFile(formData)
console.log('📡 上传结果:', res)
if (res.code === 200) {
const newUrl = res.data.url
const field = currentCameraField.value // 'arrival_photo' 或 'inspection_report'
const field = currentCameraField.value
// 更新表单数据
// 更新表单
form[field].push(newUrl)
// 更新文件展示列表
// 更新文件列表 (使用 getImageUrl 补全显示)
const fileObj = { name: file.name, url: getImageUrl(newUrl) }
if (field === 'arrival_photo') {
arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
arrivalFileList.value.push(fileObj)
} else if (field === 'inspection_report') {
reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
reportFileList.value.push(fileObj)
}
ElMessage.success('拍照上传成功')
cameraDialogVisible.value = false // 成功才关闭弹窗
cameraDialogVisible.value = false
} else {
ElMessage.error(res.msg || '上传失败')
}
} catch (e: any) {
console.error('上传异常:', e)
ElMessage.error('网络错误,上传失败')
} finally {
loadingInstance.close() // 关闭加载状态
loadingInstance.close()
}
}
@ -912,4 +1059,4 @@ onMounted(() => fetchData())
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
.camera-card .text { font-size: 12px; margin-top: 5px; }
.camera-card .el-icon { font-size: 24px; }
</style>
</style>