更新关联客户功能

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
+397
View File
@@ -41,6 +41,111 @@
/>
</view>
<!-- 关联物品 -->
<view class="form-item">
<text class="form-label">关联物品</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>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input
v-model="searchQuery"
class="form-input"
type="text"
placeholder="搜索物品名称或编号"
@input="onSearchInput"
@focus="onSearchFocus"
/>
<!-- 搜索结果下拉 -->
<view v-if="showDropdown && searchResults.length > 0" class="dropdown">
<view
v-for="item in searchResults"
:key="item.ID"
class="dropdown-item"
@click="selectItem(item)"
>
<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>
<view v-if="showDropdown && searchLoading" class="dropdown">
<view class="dropdown-loading">搜索中...</view>
</view>
<view v-if="showDropdown && searchResults.length === 0 && !searchLoading && searchQuery.trim()" class="dropdown">
<view class="dropdown-empty">未找到匹配的物品</view>
</view>
</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>
@@ -92,6 +197,8 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { workOrderApi } from '@/api/work_order.js'
import { warehouseApi } from '@/api/warehouse.js'
import { customerApi } from '@/api/customer.js'
import api from '@/api/index.js'
import { useConfigStore } from '@/stores/config.js'
@@ -107,6 +214,23 @@ const pageError = ref('')
const submitting = ref(false)
const showDeleteConfirm = ref(false)
// 关联物品
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 form = reactive({
title: '',
@@ -128,6 +252,120 @@ function previewPhoto(index) {
})
}
// 物品搜索
function onSearchInput() {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
doSearch()
}, 300)
}
function onSearchFocus() {
if (searchQuery.value.trim()) {
showDropdown.value = true
}
}
async function doSearch() {
if (!searchQuery.value.trim()) {
searchResults.value = []
showDropdown.value = false
return
}
searchLoading.value = true
showDropdown.value = true
try {
const res = await warehouseApi.listItem({
search: searchQuery.value.trim()
})
if (res.errCode === 0 && res.data) {
searchResults.value = (res.data.items || []).slice(0, 10)
} else {
searchResults.value = []
}
} catch (e) {
console.error('搜索物品失败', e)
searchResults.value = []
} finally {
searchLoading.value = false
}
}
function selectItem(item) {
// 检查是否已选中
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 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)
}
// 加载工单数据
async function fetchOrder() {
pageLoading.value = true
@@ -138,6 +376,17 @@ async function fetchOrder() {
form.title = order.Title || ''
form.description = order.Description || ''
photos.value = res.data.photos || []
// 加载已关联的物品
if (res.data.linkedItems && res.data.linkedItems.length > 0) {
selectedItems.value = res.data.linkedItems
linkedItemIds.value = res.data.linkedItems.map(item => item.ID)
}
// 加载已关联的客户
if (res.data.linkedCustomers && res.data.linkedCustomers.length > 0) {
selectedCustomers.value = res.data.linkedCustomers
}
} else {
pageError.value = '工单不存在'
}
@@ -207,6 +456,16 @@ async function submitForm() {
photos: photos.value.map(p => p.Sha256)
}
// 添加关联物品
if (linkedItemIds.value.length > 0) {
data.item_ids = linkedItemIds.value
}
// 添加关联客户
if (selectedCustomers.value.length > 0) {
data.customer_ids = selectedCustomers.value.map(c => c.id)
}
const res = await workOrderApi.update(data)
if (res.errCode === 0) {
@@ -488,4 +747,142 @@ onMounted(() => {
font-size: 32rpx;
color: #fff;
}
.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;
}
.selected-customers {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-bottom: 20rpx;
}
.selected-customer-tag {
display: flex;
align-items: center;
gap: 10rpx;
padding: 15rpx 20rpx;
background-color: #e6f7ff;
border:1rpx solid #91d5ff;
border-radius: 30rpx;
font-size: 26rpx;
color: #1890ff;
}
.customer-name {
display: block;
font-size: 28rpx;
color: #333;
}
.dropdown-phone {
display: block;
font-size: 24rpx;
color: #999;
margin-top: 5rpx;
}
.remove-customer {
font-size: 28rpx;
color: #ff4d4f;
padding: 5rpx;
}
.search-box {
position: relative;
}
.dropdown {
position: absolute;
top: 90rpx;
left: 0;
right: 0;
background-color: #fff;
border:1rpx solid #e5e5e5;
border-radius: 10rpx;
max-height: 400rpx;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.dropdown-item {
padding: 20rpx;
border-bottom:1rpx solid #f0f0f0;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item-info {
display: flex;
flex-direction: column;
}
.dropdown-name {
display: block;
font-size: 28rpx;
color: #333;
}
.dropdown-serial {
display: block;
font-size: 24rpx;
color: #999;
margin-top: 5rpx;
}
.dropdown-loading,
.dropdown-empty {
padding: 30rpx;
text-align: center;
font-size: 26rpx;
color: #999;
}
</style>