up
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { api } from './index'
|
||||
|
||||
export const usersApi = {
|
||||
|
||||
|
||||
getUserInfoFromUserID(UserID) {
|
||||
return api.get('/users/getuserinfo/'+UserID)
|
||||
},
|
||||
|
||||
@@ -222,8 +222,10 @@
|
||||
"move_item": "Move Item",
|
||||
"move_history": "Move History",
|
||||
"work_orders": "Work Orders",
|
||||
"customers": "Customers",
|
||||
"no_move_history": "No move history",
|
||||
"no_work_orders": "No related work orders",
|
||||
"no_customers": "No related customers",
|
||||
"current_location": "Current Location",
|
||||
"target_container": "Target Container",
|
||||
"search_container": "Search container...",
|
||||
@@ -234,7 +236,12 @@
|
||||
"delete_item_msg": "Are you sure you want to delete item \"{name}\"? This action cannot be undone.",
|
||||
"items_in_containers": "Stored",
|
||||
"total_items": "{count} items",
|
||||
"search_item_placeholder": "Search by name or serial number"
|
||||
"search_item_placeholder": "Search by name or serial number",
|
||||
"linked_customers": "Linked Customers",
|
||||
"linked_customer_placeholder": "Search customer by name or phone...",
|
||||
"linked_customer_not_found": "No matching customers found",
|
||||
"linked_customer_selected": "Selected",
|
||||
"clear_linked_customer": "Clear"
|
||||
},
|
||||
"purchase_addorder": {
|
||||
"add_order": "Add Order",
|
||||
@@ -554,6 +561,7 @@
|
||||
"created_at": "Created At",
|
||||
"primary": "Primary",
|
||||
"set_primary": "Set Primary",
|
||||
"primary_contact": "Primary Contact",
|
||||
"label_mobile": "Mobile",
|
||||
"label_work": "Work",
|
||||
"label_home": "Home",
|
||||
|
||||
@@ -222,8 +222,10 @@
|
||||
"move_item": "移动物品",
|
||||
"move_history": "移动历史",
|
||||
"work_orders": "关联工单",
|
||||
"customers": "关联客户",
|
||||
"no_move_history": "暂无移动记录",
|
||||
"no_work_orders": "暂无关联工单",
|
||||
"no_customers": "暂无关联客户",
|
||||
"current_location": "当前位置",
|
||||
"target_container": "目标容器",
|
||||
"search_container": "搜索容器...",
|
||||
@@ -234,7 +236,12 @@
|
||||
"delete_item_msg": "确定要删除物品「{name}」吗?此操作不可撤销。",
|
||||
"items_in_containers": "已入库",
|
||||
"total_items": "共 {count} 条",
|
||||
"search_item_placeholder": "搜索物品名称或序列号"
|
||||
"search_item_placeholder": "搜索物品名称或序列号",
|
||||
"linked_customers": "关联客户",
|
||||
"linked_customer_placeholder": "搜索客户姓名或电话...",
|
||||
"linked_customer_not_found": "未找到匹配的客户",
|
||||
"linked_customer_selected": "已选择",
|
||||
"clear_linked_customer": "清除"
|
||||
},
|
||||
"purchase_addorder": {
|
||||
"add_order": "添加订单",
|
||||
@@ -554,6 +561,7 @@
|
||||
"created_at": "创建时间",
|
||||
"primary": "主",
|
||||
"set_primary": "设为主",
|
||||
"primary_contact": "主要联系方式",
|
||||
"label_mobile": "手机",
|
||||
"label_work": "工作",
|
||||
"label_home": "家庭",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { customerApi } from '@/api/customer'
|
||||
@@ -17,6 +17,12 @@ const emails = ref([])
|
||||
const companies = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 主要联系方式
|
||||
const primaryPhone = computed(() => phones.value.find(p => p.is_primary) || null)
|
||||
const primaryEmail = computed(() => emails.value.find(e => e.is_primary) || null)
|
||||
const primaryCompany = computed(() => companies.value.find(c => c.is_primary) || null)
|
||||
const hasPrimaryInfo = computed(() => primaryPhone.value || primaryEmail.value || primaryCompany.value)
|
||||
|
||||
const toast = ref({ show: false, message: '', type: 'success' })
|
||||
|
||||
// 获取客户详情
|
||||
@@ -51,9 +57,9 @@ async function fetchCustomerDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
router.push('/customer')
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
@@ -141,6 +147,40 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact Info Card -->
|
||||
<div v-if="hasPrimaryInfo" class="rounded-lg border border-amber-200 bg-amber-50 p-6 dark:border-amber-800 dark:bg-amber-900/20">
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('customer.primary_contact') }}</h2>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div v-if="primaryPhone">
|
||||
<label class="text-sm text-gray-500 dark:text-dk-subtle">{{ t('customer.phone') }}</label>
|
||||
<p class="font-medium text-gray-900 dark:text-dk-text">
|
||||
+{{ primaryPhone.prefix }} {{ primaryPhone.phone }}
|
||||
<span class="ml-1 rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
{{ getLabelText(primaryPhone.label) }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="primaryEmail">
|
||||
<label class="text-sm text-gray-500 dark:text-dk-subtle">{{ t('customer.email') }}</label>
|
||||
<p class="font-medium text-gray-900 dark:text-dk-text">
|
||||
{{ primaryEmail.email }}
|
||||
<span class="ml-1 rounded bg-amber-100 px-2 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
{{ getLabelText(primaryEmail.label) }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="primaryCompany" class="sm:col-span-2">
|
||||
<label class="text-sm text-gray-500 dark:text-dk-subtle">{{ t('customer.company') }}</label>
|
||||
<p class="font-medium text-gray-900 dark:text-dk-text">
|
||||
{{ primaryCompany.company_name }}
|
||||
</p>
|
||||
<div v-if="primaryCompany.department || primaryCompany.position" class="mt-1 text-sm text-gray-500 dark:text-dk-subtle">
|
||||
{{ primaryCompany.department }}{{ primaryCompany.department && primaryCompany.position ? ' · ' : '' }}{{ primaryCompany.position }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phones Card -->
|
||||
<div v-if="phones.length > 0" class="rounded-lg border border-gray-200 bg-white p-6 dark:border-dk-muted dark:bg-dk-card">
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-dk-text">{{ t('customer.phones') }}</h2>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import { usePageTitle } from '@/composables/usePageTitle'
|
||||
import { warehouseApi } from '@/api/warehouse'
|
||||
import { customerApi } from '@/api/customer'
|
||||
import useDropzone from '@/components/useDropzone.vue'
|
||||
import { IconUser, IconX } from '@tabler/icons-vue'
|
||||
|
||||
usePageTitle('warehouse.add_item')
|
||||
const { t } = useI18n()
|
||||
@@ -32,7 +34,66 @@ function getPhotoHashes() {
|
||||
return dropzoneRef.value?.return_files().map((f) => f.hash) ?? []
|
||||
}
|
||||
|
||||
// ==================== 关联客户搜索(多选) ====================
|
||||
const customerSearchQuery = ref('')
|
||||
const customerSearchResults = ref([])
|
||||
const customerSearchLoading = ref(false)
|
||||
const showCustomerDropdown = ref(false)
|
||||
const selectedCustomers = ref([])
|
||||
|
||||
let customerSearchTimer = null
|
||||
|
||||
function onCustomerSearchInput() {
|
||||
clearTimeout(customerSearchTimer)
|
||||
customerSearchTimer = setTimeout(async () => {
|
||||
customerSearchLoading.value = true
|
||||
showCustomerDropdown.value = true
|
||||
try {
|
||||
let res
|
||||
if (customerSearchQuery.value.trim().length > 0) {
|
||||
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 = []
|
||||
}
|
||||
} else {
|
||||
res = await customerApi.list({ page: 1, page_size: 5 })
|
||||
if (res.errCode === 0 && res.data) {
|
||||
customerSearchResults.value = (res.data.customers || []).sort((a, b) => b.ID - a.ID)
|
||||
} else {
|
||||
customerSearchResults.value = []
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
customerSearchResults.value = []
|
||||
} finally {
|
||||
customerSearchLoading.value = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
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 handleClickOutside(e) {
|
||||
if (!e.target.closest('.customer-search-wrapper')) {
|
||||
showCustomerDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
try {
|
||||
const { errCode, data } = await warehouseApi.getContainer(containerId.value)
|
||||
if (errCode === 0 && data?.container) {
|
||||
@@ -64,6 +125,7 @@ async function submit() {
|
||||
remark: form.remark.trim(),
|
||||
quantity: form.quantity > 0 ? form.quantity : 1,
|
||||
photos: hashes,
|
||||
customer_ids: selectedCustomers.value.map(c => c.id),
|
||||
})
|
||||
if (errCode === 0) {
|
||||
toast.success(t('message.save_success'))
|
||||
@@ -167,6 +229,75 @@ async function submit() {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 关联客户搜索(多选) -->
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('warehouse.linked_customers') || '关联客户' }}
|
||||
</label>
|
||||
|
||||
<!-- 已选择客户列表 -->
|
||||
<div v-if="selectedCustomers.length > 0" class="mb-2 flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="customer in selectedCustomers"
|
||||
:key="customer.id"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||||
>
|
||||
<IconUser :size="12" />
|
||||
{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-0.5 rounded-full p-0.5 transition-colors hover:bg-blue-200 dark:hover:bg-blue-800"
|
||||
@click="removeSelectedCustomer(customer.id)"
|
||||
>
|
||||
<IconX :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="customer-search-wrapper relative">
|
||||
<input
|
||||
v-model="customerSearchQuery"
|
||||
type="text"
|
||||
:placeholder="t('warehouse.linked_customer_placeholder') || '搜索客户...'"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||
@input="onCustomerSearchInput"
|
||||
@focus="customerSearchQuery || onCustomerSearchInput()"
|
||||
/>
|
||||
<!-- 下拉结果 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
<div
|
||||
v-for="customer in customerSearchResults"
|
||||
:key="customer.id"
|
||||
class="cursor-pointer px-3 py-2 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||
@click="selectCustomer(customer)"
|
||||
>
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</div>
|
||||
<div v-if="customer.primary_phone || customer.primary_company" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ customer.primary_phone }}{{ customer.primary_phone && customer.primary_company ? ' · ' : '' }}{{ customer.primary_company }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && customerSearchLoading"
|
||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
{{ t('message.loading') }}
|
||||
</div>
|
||||
<!-- 无结果 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
{{ t('warehouse.linked_customer_not_found') || '未找到客户' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -43,6 +43,9 @@ const containerBreadcrumb = ref('')
|
||||
// ── Tab ──
|
||||
const activeTab = ref('work_orders')
|
||||
|
||||
// ── 关联客户 ──
|
||||
const customers = ref([])
|
||||
|
||||
// ── 移动弹窗 ──
|
||||
const showMove = ref(false)
|
||||
const moveTarget = ref(null)
|
||||
@@ -77,6 +80,21 @@ function getStatusLabel(status) {
|
||||
return t(`work_order.status_${status}`) || status
|
||||
}
|
||||
|
||||
// ── 客户名称格式化 ──
|
||||
function getCustomerDisplayName(customer) {
|
||||
const lastName = customer.last_name || ''
|
||||
const firstName = customer.first_name || ''
|
||||
if (lastName && firstName) {
|
||||
return `${lastName} ${firstName}`
|
||||
}
|
||||
return lastName || firstName || `Customer#${customer.id}`
|
||||
}
|
||||
|
||||
function getCustomerTitleLabel(title) {
|
||||
if (!title) return ''
|
||||
return t(`customer.salutation_${title.toLowerCase()}`)
|
||||
}
|
||||
|
||||
// ── 时间格式化 ──
|
||||
function fmtTs(ts) {
|
||||
if (!ts) return '—'
|
||||
@@ -112,6 +130,7 @@ async function fetchItem() {
|
||||
photos.value = data.photos ?? []
|
||||
commits.value = data.commits ?? []
|
||||
workOrders.value = data.work_orders ?? []
|
||||
customers.value = data.customers ?? []
|
||||
canModifyItem.value = data.canModifyItem === true
|
||||
containerBreadcrumb.value = data.container_breadcrumb ?? ''
|
||||
loadContainerNames()
|
||||
@@ -175,6 +194,12 @@ function openLinkWorkOrder() {
|
||||
? `${item.value.Name}-${item.value.SerialNumber}`
|
||||
: item.value.Name,
|
||||
description: item.value.Remark || '',
|
||||
customers: customers.value.map(c => ({
|
||||
id: c.id,
|
||||
first_name: c.first_name || '',
|
||||
last_name: c.last_name || '',
|
||||
primary_phone: c.primary_phone || '',
|
||||
})),
|
||||
}
|
||||
localStorage.setItem('prefill_work_order', JSON.stringify(prefillData))
|
||||
router.push('/work_order/add')
|
||||
@@ -440,6 +465,15 @@ onMounted(() => {
|
||||
>
|
||||
{{ t('warehouse.work_orders') }} ({{ workOrders.length }})
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-1.5 text-sm rounded-md font-medium transition-colors"
|
||||
:class="activeTab === 'customers'
|
||||
? 'bg-white text-gray-900 shadow-sm dark:bg-dk-card dark:text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
@click="activeTab = 'customers'"
|
||||
>
|
||||
{{ t('warehouse.customers') }} ({{ customers.length }})
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-1.5 text-sm rounded-md font-medium transition-colors"
|
||||
:class="activeTab === 'history'
|
||||
@@ -477,6 +511,38 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联客户 -->
|
||||
<div v-if="activeTab === 'customers'">
|
||||
<div v-if="customers.length === 0" class="rounded-xl border border-gray-200 bg-white px-5 py-8 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card">
|
||||
{{ t('warehouse.no_customers') }}
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<RouterLink
|
||||
v-for="c in customers"
|
||||
:key="c.id"
|
||||
:to="`/customer/detail/${c.id}`"
|
||||
class="rounded-xl border border-gray-200 bg-white px-4 py-3 flex items-center justify-between gap-3 hover:shadow transition-shadow dark:border-dk-muted dark:bg-dk-card dark:hover:shadow-none"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/40 flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-sm font-medium text-blue-600 dark:text-blue-400">
|
||||
{{ (c.last_name || c.first_name || 'C').charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="font-medium text-sm text-gray-900 truncate dark:text-white block">
|
||||
{{ getCustomerDisplayName(c) }}
|
||||
</span>
|
||||
<span v-if="c.title" class="text-xs text-gray-400">
|
||||
{{ getCustomerTitleLabel(c.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<IconArrowRight :size="14" class="text-gray-400 flex-shrink-0" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动历史 -->
|
||||
<div v-if="activeTab === 'history'">
|
||||
<div v-if="commits.length === 0" class="rounded-xl border border-gray-200 bg-white px-5 py-8 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card">
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import { usePageTitle } from '@/composables/usePageTitle'
|
||||
import { warehouseApi } from '@/api/warehouse'
|
||||
import { customerApi } from '@/api/customer'
|
||||
import useDropzone from '@/components/useDropzone.vue'
|
||||
import { IconUser, IconX } from '@tabler/icons-vue'
|
||||
|
||||
usePageTitle('warehouse.edit_item')
|
||||
const { t } = useI18n()
|
||||
@@ -34,7 +36,66 @@ function getPhotoHashes() {
|
||||
return dropzoneRef.value?.return_files().map((f) => f.hash) ?? []
|
||||
}
|
||||
|
||||
// ==================== 关联客户搜索(多选) ====================
|
||||
const customerSearchQuery = ref('')
|
||||
const customerSearchResults = ref([])
|
||||
const customerSearchLoading = ref(false)
|
||||
const showCustomerDropdown = ref(false)
|
||||
const selectedCustomers = ref([])
|
||||
|
||||
let customerSearchTimer = null
|
||||
|
||||
function onCustomerSearchInput() {
|
||||
clearTimeout(customerSearchTimer)
|
||||
customerSearchTimer = setTimeout(async () => {
|
||||
customerSearchLoading.value = true
|
||||
showCustomerDropdown.value = true
|
||||
try {
|
||||
let res
|
||||
if (customerSearchQuery.value.trim().length > 0) {
|
||||
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 = []
|
||||
}
|
||||
} else {
|
||||
res = await customerApi.list({ page: 1, page_size: 5 })
|
||||
if (res.errCode === 0 && res.data) {
|
||||
customerSearchResults.value = (res.data.customers || []).sort((a, b) => b.ID - a.ID)
|
||||
} else {
|
||||
customerSearchResults.value = []
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
customerSearchResults.value = []
|
||||
} finally {
|
||||
customerSearchLoading.value = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
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 handleClickOutside(e) {
|
||||
if (!e.target.closest('.customer-search-wrapper')) {
|
||||
showCustomerDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
try {
|
||||
const { errCode, data } = await warehouseApi.getItem(itemId.value)
|
||||
if (errCode === 0 && data?.item) {
|
||||
@@ -44,6 +105,16 @@ onMounted(async () => {
|
||||
form.quantity = data.item.Quantity ?? 1
|
||||
existingPhotos.value = data.photos ?? []
|
||||
|
||||
// 回填已关联的客户
|
||||
if (data.customers && data.customers.length > 0) {
|
||||
selectedCustomers.value = data.customers.map(c => ({
|
||||
id: c.id,
|
||||
first_name: c.first_name,
|
||||
last_name: c.last_name,
|
||||
title: c.title
|
||||
}))
|
||||
}
|
||||
|
||||
// 获取容器名称
|
||||
if (data.item.ContainerID) {
|
||||
const { errCode: cErr, data: cData } = await warehouseApi.getContainer(data.item.ContainerID)
|
||||
@@ -80,6 +151,7 @@ async function submit() {
|
||||
remark: form.remark.trim(),
|
||||
quantity: form.quantity > 0 ? form.quantity : 1,
|
||||
photos: hashes,
|
||||
customer_ids: selectedCustomers.value.map(c => c.id),
|
||||
})
|
||||
if (errCode === 0) {
|
||||
toast.success(t('message.save_success'))
|
||||
@@ -202,6 +274,75 @@ async function submit() {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 关联客户搜索(多选) -->
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('warehouse.linked_customers') }}
|
||||
</label>
|
||||
|
||||
<!-- 已选择客户列表 -->
|
||||
<div v-if="selectedCustomers.length > 0" class="mb-2 flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="customer in selectedCustomers"
|
||||
:key="customer.id"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||||
>
|
||||
<IconUser :size="12" />
|
||||
{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-0.5 rounded-full p-0.5 transition-colors hover:bg-blue-200 dark:hover:bg-blue-800"
|
||||
@click="removeSelectedCustomer(customer.id)"
|
||||
>
|
||||
<IconX :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="customer-search-wrapper relative">
|
||||
<input
|
||||
v-model="customerSearchQuery"
|
||||
type="text"
|
||||
:placeholder="t('warehouse.linked_customer_placeholder')"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||
@input="onCustomerSearchInput"
|
||||
@focus="customerSearchQuery || onCustomerSearchInput()"
|
||||
/>
|
||||
<!-- 下拉结果 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
<div
|
||||
v-for="customer in customerSearchResults"
|
||||
:key="customer.id"
|
||||
class="cursor-pointer px-3 py-2 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||
@click="selectCustomer(customer)"
|
||||
>
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ (customer.last_name || '') + (customer.first_name ? ' ' + customer.first_name : '') }}</div>
|
||||
<div v-if="customer.primary_phone || customer.primary_company" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ customer.primary_phone }}{{ customer.primary_phone && customer.primary_company ? ' · ' : '' }}{{ customer.primary_company }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && customerSearchLoading"
|
||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
{{ t('message.loading') }}
|
||||
</div>
|
||||
<!-- 无结果 -->
|
||||
<div
|
||||
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
{{ t('warehouse.linked_customer_not_found') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -205,6 +205,17 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有客户信息,自动选中
|
||||
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,
|
||||
}))
|
||||
linkedCustomerIds.value = prefill.customers.map(c => c.id)
|
||||
}
|
||||
|
||||
localStorage.removeItem('prefill_work_order')
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
Reference in New Issue
Block a user