更新关联客户功能

Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
2026-04-29 21:53:48 +08:00
parent da9a303cb6
commit 67c9f16301
8 changed files with 1159 additions and 46 deletions
+239 -42
View File
@@ -36,16 +36,22 @@
<text class="form-label">关联物品</text>
<!-- 已选择物品 -->
<view v-if="selectedItem" class="selected-item">
<view class="selected-item-info">
<text class="selected-item-name">{{ selectedItem.Name }}</text>
<text v-if="selectedItem.SerialNumber" class="selected-item-serial">{{ selectedItem.SerialNumber }}</text>
<view v-if="selectedItems.length > 0" class="selected-items">
<view
v-for="item in selectedItems"
:key="item.ID"
class="selected-item-tag"
>
<view class="selected-item-tag-info">
<text class="selected-item-tag-name">{{ item.Name }}</text>
<text v-if="item.SerialNumber" class="selected-item-tag-serial">{{ item.SerialNumber }}</text>
</view>
<text class="remove-item" @click="removeSelectedItem(item.ID)">×</text>
</view>
<text class="clear-btn" @click="clearSelectedItem">清除</text>
</view>
<!-- 搜索框 -->
<view v-else class="search-box">
<view class="search-box">
<input
v-model="searchQuery"
class="form-input"
@@ -63,8 +69,10 @@
class="dropdown-item"
@click="selectItem(item)"
>
<text class="dropdown-name">{{ item.Name }}</text>
<text v-if="item.SerialNumber" class="dropdown-serial">{{ item.SerialNumber }}</text>
<view class="dropdown-item-info">
<text class="dropdown-name">{{ item.Name }}</text>
<text v-if="item.SerialNumber" class="dropdown-serial">{{ item.SerialNumber }}</text>
</view>
</view>
</view>
@@ -78,6 +86,56 @@
</view>
</view>
<!-- 关联客户 -->
<view class="form-item">
<text class="form-label">关联客户</text>
<!-- 已选择客户 -->
<view v-if="selectedCustomers.length > 0" class="selected-customers">
<view
v-for="customer in selectedCustomers"
:key="customer.id"
class="selected-customer-tag"
>
<text class="customer-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
<text class="remove-customer" @click="removeSelectedCustomer(customer.id)">×</text>
</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input
v-model="customerSearchQuery"
class="form-input"
type="text"
placeholder="搜索客户姓名"
@input="onCustomerSearchInput"
@focus="onCustomerSearchFocus"
/>
<!-- 搜索结果下拉 -->
<view v-if="showCustomerDropdown && customerSearchResults.length > 0" class="dropdown">
<view
v-for="customer in customerSearchResults"
:key="customer.id"
class="dropdown-item"
@click="selectCustomer(customer)"
>
<text class="dropdown-name">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</text>
<text v-if="customer.primary_phone" class="dropdown-phone">{{ customer.primary_phone }}</text>
</view>
</view>
<view v-if="showCustomerDropdown && customerSearchLoading" class="dropdown">
<view class="dropdown-loading">搜索中...</view>
</view>
<view v-if="showCustomerDropdown && customerSearchResults.length === 0 && !customerSearchLoading && customerSearchQuery.trim()" class="dropdown">
<view class="dropdown-empty">未找到匹配的客户</view>
</view>
</view>
</view>
<!-- 图片上传 -->
<view class="form-item">
<text class="form-label">图片</text>
@@ -119,6 +177,7 @@ import { ref, reactive } from 'vue'
import api from '@/api/index.js'
import { workOrderApi } from '@/api/work_order.js'
import { warehouseApi } from '@/api/warehouse.js'
import { customerApi } from '@/api/customer.js'
import { useConfigStore } from '@/stores/config.js'
import { onLoad } from '@dcloudio/uni-app'
@@ -126,7 +185,7 @@ const configStore = useConfigStore()
const goBack = () => uni.navigateBack()
// 预填物品ID(从物品详情跳转时)
const presetItemId = ref(null)
const presetItemIds = ref([])
onLoad((options) => {
// 优先检查预填数据(从物品详情跳转时)
@@ -137,17 +196,26 @@ onLoad((options) => {
form.title = prefill.title || ''
form.description = prefill.description || ''
if (prefill.itemId) {
presetItemId.value = prefill.itemId
presetItemIds.value = [prefill.itemId]
fetchPresetItem(prefill.itemId)
}
// 如果有预填客户信息,自动填充
if (prefill.customers && prefill.customers.length > 0) {
selectedCustomers.value = prefill.customers.map(c => ({
id: c.id,
first_name: c.first_name || '',
last_name: c.last_name || '',
primary_phone: c.primary_phone || ''
}))
}
uni.removeStorageSync('prefill_work_order')
} catch (e) {
console.error('读取预填数据失败', e)
}
} else if (options && options.item_id) {
// 兼容旧方式:直接传 item_id 参数
presetItemId.value = parseInt(options.item_id)
fetchPresetItem(presetItemId.value)
presetItemIds.value = [parseInt(options.item_id)]
fetchPresetItem(parseInt(options.item_id))
}
})
@@ -155,8 +223,12 @@ async function fetchPresetItem(itemId) {
try {
const res = await warehouseApi.getItem(itemId)
if (res.errCode === 0 && res.data && res.data.item) {
selectedItem.value = res.data.item
linkedItemId.value = itemId
const item = res.data.item
// 检查是否已选中
if (!selectedItems.value.find(i => i.ID === item.ID)) {
selectedItems.value.push(item)
linkedItemIds.value.push(item.ID)
}
}
} catch (e) {
console.error('获取物品信息失败', e)
@@ -170,14 +242,22 @@ const form = reactive({
})
// 关联物品
const selectedItem = ref(null)
const linkedItemId = ref(null)
const selectedItems = ref([])
const linkedItemIds = ref([])
const searchQuery = ref('')
const searchResults = ref([])
const showDropdown = ref(false)
const searchLoading = ref(false)
let searchTimer = null
// 关联客户
const selectedCustomers = ref([])
const customerSearchQuery = ref('')
const customerSearchResults = ref([])
const showCustomerDropdown = ref(false)
const customerSearchLoading = ref(false)
let customerSearchTimer = null
// 图片
const photos = ref([])
@@ -225,16 +305,75 @@ async function doSearch() {
}
function selectItem(item) {
selectedItem.value = item
linkedItemId.value = item.ID
// 检查是否已选中
if (!selectedItems.value.find(i => i.ID === item.ID)) {
selectedItems.value.push(item)
linkedItemIds.value.push(item.ID)
}
searchQuery.value = ''
searchResults.value = []
showDropdown.value = false
}
function clearSelectedItem() {
selectedItem.value = null
linkedItemId.value = null
function removeSelectedItem(itemId) {
selectedItems.value = selectedItems.value.filter(i => i.ID !== itemId)
linkedItemIds.value = linkedItemIds.value.filter(id => id !== itemId)
}
function onCustomerSearchInput() {
clearTimeout(customerSearchTimer)
customerSearchTimer = setTimeout(() => {
doCustomerSearch()
}, 300)
}
function onCustomerSearchFocus() {
if (customerSearchQuery.value.trim()) {
showCustomerDropdown.value = true
}
}
async function doCustomerSearch() {
if (!customerSearchQuery.value.trim()) {
customerSearchResults.value = []
showCustomerDropdown.value = false
return
}
customerSearchLoading.value = true
showCustomerDropdown.value = true
try {
const res = await customerApi.list({
search: customerSearchQuery.value.trim(),
page: 1,
page_size: 10
})
if (res.errCode === 0 && res.data) {
customerSearchResults.value = (res.data.customers || []).slice(0, 10)
} else {
customerSearchResults.value = []
}
} catch (e) {
console.error('搜索客户失败', e)
customerSearchResults.value = []
} finally {
customerSearchLoading.value = false
}
}
function selectCustomer(customer) {
// 检查是否已选中
if (!selectedCustomers.value.find(c => c.id === customer.id)) {
selectedCustomers.value.push(customer)
}
customerSearchQuery.value = ''
customerSearchResults.value = []
showCustomerDropdown.value = false
}
function removeSelectedCustomer(customerId) {
selectedCustomers.value = selectedCustomers.value.filter(c => c.id !== customerId)
}
function getPhotoUrl(hash) {
@@ -299,11 +438,12 @@ async function submitForm() {
const data = {
title: form.title.trim(),
description: form.description.trim(),
photos: photos.value
photos: photos.value,
customer_ids: selectedCustomers.value.map(c => c.id)
}
if (linkedItemId.value) {
data.item_id = linkedItemId.value
if (linkedItemIds.value.length > 0) {
data.item_ids = linkedItemIds.value
}
const res = await workOrderApi.add(data)
@@ -408,37 +548,42 @@ async function submitForm() {
box-sizing: border-box;
}
.selected-item {
.selected-customers {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-bottom: 20rpx;
}
.selected-customer-tag {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
gap: 10rpx;
padding: 15rpx 20rpx;
background-color: #e6f7ff;
border: 1rpx solid #91d5ff;
border-radius: 10rpx;
}
.selected-item-info {
flex: 1;
}
.selected-item-name {
display: block;
font-size: 28rpx;
border-radius: 30rpx;
font-size: 26rpx;
color: #1890ff;
}
.selected-item-serial {
.customer-name {
display: block;
font-size: 28rpx;
color: #333;
}
.dropdown-phone {
display: block;
font-size: 24rpx;
color: #8c8c8c;
color: #999;
margin-top: 5rpx;
}
.clear-btn {
font-size: 24rpx;
.remove-customer {
font-size: 28rpx;
color: #ff4d4f;
padding: 10rpx;
padding: 5rpx;
}
.search-box {
@@ -546,6 +691,58 @@ async function submitForm() {
margin-top: 10rpx;
}
.selected-items {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-bottom: 20rpx;
}
.selected-item-tag {
display: flex;
align-items: flex-start;
gap: 10rpx;
padding: 15rpx 20rpx;
background-color: #e6f7ff;
border: 1rpx solid #91d5ff;
border-radius: 10rpx;
font-size: 26rpx;
color: #1890ff;
max-width: 100%;
}
.selected-item-tag-info {
flex: 1;
overflow: hidden;
}
.selected-item-tag-name {
display: block;
font-size: 26rpx;
color: #1890ff;
word-break: break-all;
}
.selected-item-tag-serial {
display: block;
font-size: 22rpx;
color: #8c8c8c;
margin-top: 5rpx;
word-break: break-all;
}
.remove-item {
font-size: 28rpx;
color: #ff4d4f;
padding: 5rpx;
flex-shrink: 0;
}
.dropdown-item-info {
display: flex;
flex-direction: column;
}
.submit-bar {
margin-top: 40rpx;
padding: 0 20rpx;