This commit is contained in:
2026-04-24 00:37:24 +08:00
parent 1b1ec7f64d
commit 5693472f64
11 changed files with 101 additions and 164 deletions
+2
View File
@@ -131,6 +131,7 @@
"no_photos": "No photos",
"status": "Status",
"created_at": "Created At",
"updated_at": "Updated At",
"filter_all": "All",
"status_pending": "Pending",
"status_checked": "Checked",
@@ -171,6 +172,7 @@
"remark": "Remark",
"remark_placeholder": "Enter remark (optional)",
"created_at": "Created At",
"updated_at": "Updated At",
"created_by": "Created By",
"child_containers": "Sub-Containers",
"items": "Items",
+2
View File
@@ -131,6 +131,7 @@
"no_photos": "暂无图片",
"status": "状态",
"created_at": "创建时间",
"updated_at": "更新时间",
"filter_all": "全部",
"status_pending": "待处理",
"status_checked": "已检查",
@@ -171,6 +172,7 @@
"remark": "备注",
"remark_placeholder": "输入备注(可选)",
"created_at": "创建日期",
"updated_at": "更新日期",
"created_by": "创建人",
"child_containers": "子容器",
"items": "物品",
@@ -390,6 +390,7 @@ onMounted(async () => {
{{ usersStore.getUsernameFromUserID(container.CreatorID) }}
</span>
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(container.CreatedAt) }}</span>
<span v-if="container.UpdatedAt">{{ t('warehouse.updated_at') }}: {{ fmtTs(container.UpdatedAt) }}</span>
<span>{{ t('warehouse.child_containers') }}: {{ container.ChildCount }}</span>
<span>{{ t('warehouse.items') }}: {{ container.ItemCount }}</span>
</div>
@@ -428,13 +429,14 @@ onMounted(async () => {
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</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>
<th class="px-5 py-3 font-medium w-24 text-right">{{ t('warehouse.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingSub">
<td colspan="6" class="px-5 py-8 text-center">
<td colspan="7" 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" />
@@ -442,7 +444,7 @@ onMounted(async () => {
</td>
</tr>
<tr v-else-if="subContainers.length === 0">
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="7" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
{{ t('warehouse.no_containers') }}
</td>
</tr>
@@ -470,6 +472,7 @@ onMounted(async () => {
</span>
</td>
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(c.CreatedAt) }}</td>
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(c.UpdatedAt) }}</td>
<td class="px-5 py-3">
<div class="flex items-center gap-1.5">
<img
@@ -552,15 +555,17 @@ onMounted(async () => {
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
<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">{{ 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 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>
<th class="px-5 py-3 font-medium w-20 text-right">{{ t('warehouse.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingItems">
<td colspan="6" class="px-5 py-8 text-center">
<td colspan="8" 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" />
@@ -568,7 +573,7 @@ onMounted(async () => {
</td>
</tr>
<tr v-else-if="items.length === 0">
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="8" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
{{ t('warehouse.no_items') }}
</td>
</tr>
@@ -586,8 +591,10 @@ onMounted(async () => {
</div>
</td>
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.serial_number || '—' }}</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-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">
<div class="flex items-center gap-1.5">
<img
@@ -52,6 +52,7 @@ const targetContainers = ref([])
const targetSearch = ref('')
const targetLoading = ref(false)
const showTargetDropdown = ref(false)
let targetDropdownTimer = null
// ── 删除确认 ──
const showDeleteConfirm = ref(false)
@@ -186,18 +187,39 @@ async function openMove() {
showMove.value = true
}
function onTargetFocus() {
if (targetDropdownTimer) clearTimeout(targetDropdownTimer)
showTargetDropdown.value = true
loadTargetContainers(targetSearch.value)
}
function onTargetInput() {
if (targetDropdownTimer) clearTimeout(targetDropdownTimer)
showTargetDropdown.value = true
loadTargetContainers(targetSearch.value)
}
function closeTargetDropdown() {
targetDropdownTimer = setTimeout(() => {
showTargetDropdown.value = false
}, 150)
}
async function loadTargetContainers(search = '') {
targetLoading.value = true
try {
const isSearch = search.trim().length > 0
const { errCode, data } = await warehouseApi.getContainers({
search,
entries: 50,
all_levels: true,
entries: isSearch ? 50 : 10,
page: 1,
})
if (errCode === 0) {
targetContainers.value = (data.containers ?? []).filter(
(c) => c.ID !== itemId.value && c.ID !== item.value?.ContainerID
const filtered = (data.containers ?? []).filter(
(c) => c.ID !== item.value?.ContainerID
)
targetContainers.value = isSearch ? filtered : filtered.slice(0, 5)
}
} catch {
targetContainers.value = []
@@ -389,6 +411,7 @@ onMounted(() => {
{{ usersStore.getUsernameFromUserID(item.CreatorID) }}
</span>
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(item.CreatedAt) }}</span>
<span v-if="item.UpdatedAt">{{ t('warehouse.updated_at') }}: {{ fmtTs(item.UpdatedAt) }}</span>
</div>
</div>
@@ -503,19 +526,21 @@ onMounted(() => {
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.target_container') }}</label>
<div class="relative">
<div class="relative" @click.stop>
<IconSearch class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" :size="15" />
<input
v-model="targetSearch"
type="text"
:placeholder="t('warehouse.search_container')"
class="w-full rounded-lg border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
@focus="loadTargetContainers(''); showTargetDropdown = true"
@input="loadTargetContainers(targetSearch); showTargetDropdown = true"
@focus="onTargetFocus"
@input="onTargetInput"
@blur="closeTargetDropdown"
/>
<div
v-if="showTargetDropdown && (targetContainers.length > 0 || targetLoading)"
v-if="showTargetDropdown"
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card max-h-60 overflow-y-auto"
@mousedown.prevent
>
<div v-if="targetLoading" class="px-3 py-2 text-xs text-gray-400">
<svg class="inline h-3.5 w-3.5 animate-spin mr-1" viewBox="0 0 24 24" fill="none">
@@ -320,7 +320,7 @@ function formatDate(dateStr) {
d = new Date(dateStr)
}
if (isNaN(d.getTime())) return '—'
return d.toLocaleDateString(isEn.value ? 'en-US' : 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
return d.toLocaleString(isEn.value ? 'en-US' : 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
} catch { return dateStr }
}
@@ -426,12 +426,13 @@ onMounted(() => {
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</th>
<th class="px-6 py-3 font-medium whitespace-nowrap w-44">{{ t('warehouse.created_at') }}</th>
<th class="px-6 py-3 font-medium whitespace-nowrap w-44">{{ t('warehouse.updated_at') }}</th>
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="containerLoading">
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
<td colspan="8" 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" />
@@ -440,7 +441,7 @@ onMounted(() => {
</td>
</tr>
<tr v-else-if="containers.length === 0">
<td colspan="7" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="8" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
{{ t('warehouse.no_containers') }}
</td>
</tr>
@@ -472,6 +473,7 @@ onMounted(() => {
</span>
</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ fmtTs(c.CreatedAt) }}</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ fmtTs(c.UpdatedAt) }}</td>
<td class="px-6 py-3">
<div class="flex items-center gap-1.5">
<img
@@ -548,15 +550,17 @@ onMounted(() => {
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
<th class="px-6 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
<th class="px-6 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
<th class="px-6 py-3 font-medium">{{ t('warehouse.remark') }}</th>
<th class="px-6 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
<th class="px-6 py-3 font-medium">{{ t('warehouse.location') }}</th>
<th class="px-6 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
<th class="px-6 py-3 font-medium whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="itemLoading">
<td colspan="6" class="px-6 py-8 text-center text-gray-400">
<td colspan="8" 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" />
@@ -565,7 +569,7 @@ onMounted(() => {
</td>
</tr>
<tr v-else-if="items.length === 0">
<td colspan="6" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="8" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
{{ t('warehouse.no_items') }}
</td>
</tr>
@@ -577,6 +581,7 @@ onMounted(() => {
>
<td class="px-6 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
<td class="px-6 py-3 max-w-[160px] truncate text-xs text-gray-500 dark:text-gray-400">{{ item.SerialNumber || '—' }}</td>
<td class="px-6 py-3 max-w-[200px] truncate text-xs text-gray-500 dark:text-gray-400">{{ item.Remark || '—' }}</td>
<td class="px-6 py-3 text-center text-sm">{{ item.Quantity }}</td>
<td class="px-6 py-3">
<span v-if="item.ContainerID != null" class="inline-flex items-center gap-1 text-sm text-blue-600">
@@ -588,6 +593,7 @@ onMounted(() => {
</span>
</td>
<td class="px-6 py-3 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500">{{ formatDate(item.CreatedAt) }}</td>
<td class="px-6 py-3 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500">{{ formatDate(item.UpdatedAt) }}</td>
<td class="px-6 py-3">
<div class="flex items-center gap-1.5">
<img
@@ -393,7 +393,10 @@ onUnmounted(() => {
{{ usersStore.getUsernameFromUserID(order.UserID) }}
</span>
</div>
<span class="text-sm text-gray-400">{{ formatDate(order?.CreatedAt) }}</span>
<div class="text-sm text-gray-400 text-right">
<div>{{ t('work_order.created_at') }}: {{ formatDate(order?.CreatedAt) }}</div>
<div v-if="order?.UpdatedAt">{{ t('work_order.updated_at') }}: {{ formatDate(order.UpdatedAt) }}</div>
</div>
</div>
<!-- 状态快捷切换所有登录用户可见 -->
@@ -144,12 +144,13 @@ 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 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>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="4" class="px-6 py-8 text-center text-gray-400">
<td colspan="5" 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" />
@@ -158,7 +159,7 @@ onMounted(fetchOrders)
</td>
</tr>
<tr v-else-if="orders.length === 0">
<td colspan="4" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
<td colspan="5" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
暂无工单
</td>
</tr>
@@ -172,6 +173,7 @@ 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 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">
<span
class="inline-block rounded-full px-2.5 py-0.5 text-xs font-medium"