This commit is contained in:
2026-04-29 18:35:34 +08:00
parent 61fd83b870
commit 98dfa3ac02
12 changed files with 249 additions and 58 deletions
+4 -2
View File
@@ -174,7 +174,8 @@
"confirm_delete_commit": "Are you sure you want to delete this progress?",
"linked_items": "Linked Items",
"submit": "Submit",
"save_changes": "Save Changes"
"save_changes": "Save Changes",
"work_order_count": "Work Order Count"
},
"warehouse": {
"title": "Warehouse",
@@ -598,6 +599,7 @@
"detail_title": "Customer Detail",
"basic_info": "Basic Information",
"created_by": "Created By",
"not_found": "Customer not found"
"not_found": "Customer not found",
"related_customers": "Related Customers"
}
}
+4 -2
View File
@@ -174,7 +174,8 @@
"confirm_delete_commit": "确定要删除此进度吗?",
"linked_items": "关联物品",
"submit": "提交",
"save_changes": "保存修改"
"save_changes": "保存修改",
"work_order_count": "工单数量"
},
"warehouse": {
"title": "仓库",
@@ -598,6 +599,7 @@
"detail_title": "客户详情",
"basic_info": "基本信息",
"created_by": "创建者",
"not_found": "客户不存在"
"not_found": "客户不存在",
"related_customers": "关联客户"
}
}
@@ -255,7 +255,7 @@ async function submit() {
</div>
<!-- 搜索框 -->
<div class="customer-search-wrapper relative">
<div class="customer-search-wrapper relative" style="z-index: 9999;">
<input
v-model="customerSearchQuery"
type="text"
@@ -267,7 +267,7 @@ async function submit() {
<!-- 下拉结果 -->
<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"
class="absolute z-[9999] 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"
@@ -284,14 +284,14 @@ async function submit() {
<!-- 加载中 -->
<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"
class="absolute z-[9999] 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"
class="absolute z-[9999] 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>
@@ -1,6 +1,6 @@
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
@@ -16,6 +16,8 @@ import {
IconEdit,
IconTrash,
IconSearch,
IconTool,
IconUser,
} from '@tabler/icons-vue'
usePageTitle('warehouse.container_detail')
@@ -557,6 +559,8 @@ onMounted(async () => {
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
<th class="px-5 py-3 font-medium">{{ t('warehouse.remark') }}</th>
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('work_order.work_order_count') }}</th>
<th class="px-5 py-3 font-medium">{{ t('customer.related_customers') }}</th>
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
@@ -565,7 +569,7 @@ onMounted(async () => {
</thead>
<tbody>
<tr v-if="loadingItems">
<td colspan="8" class="px-5 py-8 text-center">
<td colspan="10" class="px-5 py-8 text-center">
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
@@ -573,7 +577,7 @@ onMounted(async () => {
</td>
</tr>
<tr v-else-if="items.length === 0">
<td colspan="8" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="10" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
{{ t('warehouse.no_items') }}
</td>
</tr>
@@ -593,6 +597,29 @@ onMounted(async () => {
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.SerialNumber || '—' }}</td>
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[200px] truncate">{{ item.Remark || '—' }}</td>
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
<td class="px-5 py-3 text-center">
<span v-if="item.WorkOrderCount > 0" class="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-900/40 dark:text-orange-400">
<IconTool :size="12" />
{{ item.WorkOrderCount }}
</span>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-5 py-3">
<div v-if="item.Customers && item.Customers.length > 0" class="flex flex-wrap gap-1">
<RouterLink
v-for="customer in item.Customers.slice(0, 3)"
:key="customer.id"
:to="`/customer/${customer.id}`"
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/60"
@click.stop
>
<IconUser :size="10" />
{{ customer.first_name }} {{ customer.last_name }}
</RouterLink>
<span v-if="item.Customers.length > 3" class="text-xs text-gray-400">+{{ item.Customers.length - 3 }}</span>
</div>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.CreatedAt) }}</td>
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.UpdatedAt) }}</td>
<td class="px-5 py-3">
@@ -28,6 +28,7 @@ const submitting = ref(false)
const loadingItem = ref(true)
const itemNotFound = ref(false)
const containerName = ref('')
const containerId = ref(null)
const existingPhotos = ref([])
const dropzoneRef = ref(null)
@@ -117,10 +118,13 @@ onMounted(async () => {
// 获取容器名称
if (data.item.ContainerID) {
containerId.value = data.item.ContainerID
const { errCode: cErr, data: cData } = await warehouseApi.getContainer(data.item.ContainerID)
if (cErr === 0 && cData?.container) {
containerName.value = cData.container.Title
}
} else {
containerId.value = null
}
} else {
itemNotFound.value = true
@@ -193,14 +197,15 @@ async function submit() {
<RouterLink to="/warehouse/container" class="text-blue-500 hover:underline">
{{ t('warehouse.container_list') }}
</RouterLink>
<span>/</span>
<RouterLink
v-if="containerName"
:to="`/warehouse/container/${itemId}`"
class="text-blue-500 hover:underline"
>
{{ containerName }}
</RouterLink>
<template v-if="containerName">
<span>/</span>
<RouterLink
:to="`/warehouse/container/${containerId}`"
class="text-blue-500 hover:underline"
>
{{ containerName }}
</RouterLink>
</template>
<span v-else>/</span>
<span>/</span>
<RouterLink
@@ -300,7 +305,7 @@ async function submit() {
</div>
<!-- 搜索框 -->
<div class="customer-search-wrapper relative">
<div class="customer-search-wrapper relative" style="z-index: 9999;">
<input
v-model="customerSearchQuery"
type="text"
@@ -312,7 +317,7 @@ async function submit() {
<!-- 下拉结果 -->
<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"
class="absolute z-[9999] 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"
@@ -329,14 +334,14 @@ async function submit() {
<!-- 加载中 -->
<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"
class="absolute z-[9999] 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"
class="absolute z-[9999] 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>
@@ -1,6 +1,6 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
@@ -13,6 +13,8 @@ import {
IconSearch,
IconTrash,
IconArrowRight,
IconTool,
IconUser,
} from '@tabler/icons-vue'
usePageTitle('warehouse.item_list')
@@ -215,6 +217,8 @@ onMounted(fetchItems)
<th class="px-5 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('work_order.work_order_count') }}</th>
<th class="px-5 py-3 font-medium">{{ t('customer.related_customers') }}</th>
<th class="px-5 py-3 font-medium">{{ t('warehouse.location') }}</th>
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
<th class="px-5 py-3 font-medium w-16 text-right">{{ t('warehouse.actions') }}</th>
@@ -230,6 +234,29 @@ onMounted(fetchItems)
<td class="px-5 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[160px] truncate">{{ item.SerialNumber || '—' }}</td>
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
<td class="px-5 py-3 text-center">
<span v-if="item.WorkOrderCount > 0" class="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-900/40 dark:text-orange-400">
<IconTool :size="12" />
{{ item.WorkOrderCount }}
</span>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-5 py-3">
<div v-if="item.Customers && item.Customers.length > 0" class="flex flex-wrap gap-1">
<RouterLink
v-for="customer in item.Customers.slice(0, 3)"
:key="customer.id"
:to="`/customer/${customer.id}`"
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/60"
@click.stop
>
<IconUser :size="10" />
{{ customer.first_name }} {{ customer.last_name }}
</RouterLink>
<span v-if="item.Customers.length > 3" class="text-xs text-gray-400">+{{ item.Customers.length - 3 }}</span>
</div>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-5 py-3">
<span v-if="item.ContainerBreadcrumb" class="inline-flex items-center gap-1 text-blue-600 text-sm">
<IconArrowRight :size="13" />
@@ -456,7 +456,7 @@ async function handleSubmit() {
</div>
<!-- 搜索框 -->
<div class="item-search-wrapper relative">
<div class="item-search-wrapper relative" style="z-index: 9999;">
<input
v-model="itemSearchQuery"
type="text"
@@ -468,7 +468,7 @@ async function handleSubmit() {
<!-- 下拉结果 -->
<div
v-if="showItemDropdown && itemSearchResults.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"
class="absolute z-[9999] 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="item in itemSearchResults"
@@ -483,14 +483,14 @@ async function handleSubmit() {
<!-- 加载中 -->
<div
v-if="showItemDropdown && itemSearchLoading"
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"
class="absolute z-[9999] 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="showItemDropdown && !itemSearchLoading && itemSearchResults.length === 0 && itemSearchQuery.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"
class="absolute z-[9999] 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('work_order.linked_item_not_found') }}
</div>
@@ -523,7 +523,7 @@ async function handleSubmit() {
</div>
<!-- 搜索框 -->
<div class="customer-search-wrapper relative">
<div class="customer-search-wrapper relative" style="z-index: 9999;">
<input
v-model="customerSearchQuery"
type="text"
@@ -535,7 +535,7 @@ async function handleSubmit() {
<!-- 下拉结果 -->
<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"
class="absolute z-[9999] 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"
@@ -554,14 +554,14 @@ async function handleSubmit() {
<!-- 加载中 -->
<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"
class="absolute z-[9999] 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"
class="absolute z-[9999] 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('work_order.linked_customer_not_found') }}
</div>
@@ -569,7 +569,7 @@ onUnmounted(() => {
</div>
</div>
<!-- 搜索框 -->
<div ref="purchaseDropdownRef" class="relative">
<div ref="purchaseDropdownRef" class="relative" style="z-index: 9999;">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<IconSearch :size="14" class="text-gray-400" />
</div>
@@ -584,7 +584,7 @@ onUnmounted(() => {
<!-- 搜索结果下拉 -->
<div
v-if="purchaseDropdownVisible && purchaseSearchResults.length > 0"
class="absolute z-10 mt-1 max-h-48 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card"
class="absolute z-[9999] mt-1 max-h-48 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card"
>
<button
v-for="po in purchaseSearchResults"
@@ -606,7 +606,7 @@ onUnmounted(() => {
</div>
<div
v-else-if="purchaseDropdownVisible && purchaseSearchQuery && purchaseSearchResults.length === 0 && !purchaseSearchLoading"
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white py-3 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card"
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white py-3 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card"
>
未找到匹配的订单
</div>
@@ -1,6 +1,6 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
@@ -144,6 +144,7 @@ onMounted(fetchOrders)
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-16">No.</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('work_order.title') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">描述</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">关联客户</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.created_at') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.updated_at') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-36">{{ t('work_order.status') }}</th>
@@ -151,7 +152,7 @@ onMounted(fetchOrders)
</thead>
<tbody>
<tr v-if="loading">
<td colspan="6" class="px-6 py-8 text-center text-gray-400">
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
@@ -160,7 +161,7 @@ onMounted(fetchOrders)
</td>
</tr>
<tr v-else-if="orders.length === 0">
<td colspan="6" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="7" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
暂无工单
</td>
</tr>
@@ -174,6 +175,19 @@ onMounted(fetchOrders)
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ order.ID }}</td>
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ order.Title }}</td>
<td class="px-6 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ order.Description || '—' }}</td>
<td class="px-6 py-3">
<div v-if="order.customers && order.customers.length > 0" class="flex flex-wrap gap-1">
<RouterLink
v-for="c in order.customers"
:key="c.id"
:to="`/customer/detail/${c.id}`"
class="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
>
{{ (c.last_name || '') + (c.first_name ? ' ' + c.first_name : '') }}
</RouterLink>
</div>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.UpdatedAt) }}</td>
<td class="px-6 py-3">