up
This commit is contained in:
@@ -115,6 +115,47 @@ func canModifyWarehouse(userID, creatorUserID uint) bool {
|
|||||||
return userID == creatorUserID
|
return userID == creatorUserID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildContainerBreadcrumb 根据容器 ID 和映射表构建面包屑路径(如 "仓库A / 柜子B / 抽屉C")
|
||||||
|
func buildContainerBreadcrumb(containerID uint, containerMap map[uint]TabWarehouseContainer) string {
|
||||||
|
parts := []string{}
|
||||||
|
visited := map[uint]bool{}
|
||||||
|
cur := containerID
|
||||||
|
for cur != 0 && !visited[cur] {
|
||||||
|
visited[cur] = true
|
||||||
|
c, ok := containerMap[cur]
|
||||||
|
if !ok {
|
||||||
|
// 映射表中没有,查询数据库补全
|
||||||
|
var missing TabWarehouseContainer
|
||||||
|
if models.DB.Where("id = ?", cur).First(&missing).Error == nil {
|
||||||
|
containerMap[cur] = missing
|
||||||
|
parts = append([]string{missing.Title}, parts...)
|
||||||
|
if missing.ParentID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cur = *missing.ParentID
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = append([]string{c.Title}, parts...)
|
||||||
|
if c.ParentID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cur = *c.ParentID
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
result := ""
|
||||||
|
for i, p := range parts {
|
||||||
|
if i > 0 {
|
||||||
|
result += " / "
|
||||||
|
}
|
||||||
|
result += p
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 初始化 ----------
|
// ---------- 初始化 ----------
|
||||||
|
|
||||||
func ApiWarehouseInit() {
|
func ApiWarehouseInit() {
|
||||||
@@ -382,7 +423,10 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
ReturnJson(ctx, "jsonErr", nil)
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if from.Entries <= 0 || from.Entries > 300 {
|
if from.AllLevels && from.Entries <= 0 {
|
||||||
|
from.Entries = 5000
|
||||||
|
}
|
||||||
|
if from.Entries <= 0 || (!from.AllLevels && from.Entries > 300) {
|
||||||
from.Entries = 10
|
from.Entries = 10
|
||||||
}
|
}
|
||||||
if from.Page <= 0 {
|
if from.Page <= 0 {
|
||||||
@@ -813,13 +857,45 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
Find(&items)
|
Find(&items)
|
||||||
|
|
||||||
canModifyItems := make([]bool, len(items))
|
canModifyItems := make([]bool, len(items))
|
||||||
|
// 收集所有涉及的容器 ID
|
||||||
|
containerIDs := make(map[uint]bool)
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
canModifyItems[i] = canModifyWarehouse(user.ID, item.CreatorID)
|
canModifyItems[i] = canModifyWarehouse(user.ID, item.CreatorID)
|
||||||
|
if item.ContainerID != nil {
|
||||||
|
containerIDs[*item.ContainerID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询容器,构建 ID→Container 映射
|
||||||
|
containerMap := make(map[uint]TabWarehouseContainer)
|
||||||
|
if len(containerIDs) > 0 {
|
||||||
|
ids := make([]uint, 0, len(containerIDs))
|
||||||
|
for id := range containerIDs {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
var containers []TabWarehouseContainer
|
||||||
|
models.DB.Where("id IN ?", ids).Find(&containers)
|
||||||
|
for _, c := range containers {
|
||||||
|
containerMap[c.ID] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个物品计算面包屑
|
||||||
|
type ItemWithBreadcrumb struct {
|
||||||
|
TabWarehouseItem
|
||||||
|
ContainerBreadcrumb string `json:"ContainerBreadcrumb"`
|
||||||
|
}
|
||||||
|
itemsWithBreadcrumb := make([]ItemWithBreadcrumb, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
itemsWithBreadcrumb[i] = ItemWithBreadcrumb{TabWarehouseItem: item}
|
||||||
|
if item.ContainerID != nil {
|
||||||
|
itemsWithBreadcrumb[i].ContainerBreadcrumb = buildContainerBreadcrumb(*item.ContainerID, containerMap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"all_count": count,
|
"all_count": count,
|
||||||
"items": items,
|
"items": itemsWithBreadcrumb,
|
||||||
"canModifyItems": canModifyItems,
|
"canModifyItems": canModifyItems,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -863,6 +939,24 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
var commits []TabWarehouseItemCommit
|
var commits []TabWarehouseItemCommit
|
||||||
models.DB.Where("item_id = ?", from.ID).Order("created_at DESC").Find(&commits)
|
models.DB.Where("item_id = ?", from.ID).Order("created_at DESC").Find(&commits)
|
||||||
|
|
||||||
|
// 为 commits 构建容器面包屑
|
||||||
|
type CommitWithBreadcrumb struct {
|
||||||
|
TabWarehouseItemCommit
|
||||||
|
OldContainerBreadcrumb string `json:"OldContainerBreadcrumb"`
|
||||||
|
NewContainerBreadcrumb string `json:"NewContainerBreadcrumb"`
|
||||||
|
}
|
||||||
|
commitMap := make(map[uint]TabWarehouseContainer)
|
||||||
|
commitsWithBreadcrumb := make([]CommitWithBreadcrumb, len(commits))
|
||||||
|
for i, c := range commits {
|
||||||
|
commitsWithBreadcrumb[i] = CommitWithBreadcrumb{TabWarehouseItemCommit: c}
|
||||||
|
if c.OldContainer != nil {
|
||||||
|
commitsWithBreadcrumb[i].OldContainerBreadcrumb = buildContainerBreadcrumb(*c.OldContainer, commitMap)
|
||||||
|
}
|
||||||
|
if c.NewContainer != nil {
|
||||||
|
commitsWithBreadcrumb[i].NewContainerBreadcrumb = buildContainerBreadcrumb(*c.NewContainer, commitMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 关联工单
|
// 关联工单
|
||||||
var woBinds []TabWarehouseItemWorkOrderBind
|
var woBinds []TabWarehouseItemWorkOrderBind
|
||||||
models.DB.Where("item_id = ?", from.ID).Find(&woBinds)
|
models.DB.Where("item_id = ?", from.ID).Find(&woBinds)
|
||||||
@@ -883,9 +977,16 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"item": item,
|
"item": item,
|
||||||
"photos": files,
|
"photos": files,
|
||||||
"commits": commits,
|
"commits": commitsWithBreadcrumb,
|
||||||
"work_orders": workOrders,
|
"work_orders": workOrders,
|
||||||
"canModifyItem": canModifyWarehouse(user.ID, item.CreatorID),
|
"canModifyItem": canModifyWarehouse(user.ID, item.CreatorID),
|
||||||
|
"container_breadcrumb": func() string {
|
||||||
|
if item.ContainerID == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
m := make(map[uint]TabWarehouseContainer)
|
||||||
|
return buildContainerBreadcrumb(*item.ContainerID, m)
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const notFound = ref(false)
|
|||||||
|
|
||||||
// ── 容器名缓存 ──
|
// ── 容器名缓存 ──
|
||||||
const containerNames = reactive({})
|
const containerNames = reactive({})
|
||||||
|
const containerBreadcrumb = ref('')
|
||||||
|
|
||||||
// ── Tab ──
|
// ── Tab ──
|
||||||
const activeTab = ref('work_orders')
|
const activeTab = ref('work_orders')
|
||||||
@@ -112,6 +113,7 @@ async function fetchItem() {
|
|||||||
commits.value = data.commits ?? []
|
commits.value = data.commits ?? []
|
||||||
workOrders.value = data.work_orders ?? []
|
workOrders.value = data.work_orders ?? []
|
||||||
canModifyItem.value = data.canModifyItem === true
|
canModifyItem.value = data.canModifyItem === true
|
||||||
|
containerBreadcrumb.value = data.container_breadcrumb ?? ''
|
||||||
loadContainerNames()
|
loadContainerNames()
|
||||||
} else {
|
} else {
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
@@ -375,12 +377,13 @@ onMounted(() => {
|
|||||||
<span>{{ t('warehouse.quantity') }}: {{ item.Quantity }}</span>
|
<span>{{ t('warehouse.quantity') }}: {{ item.Quantity }}</span>
|
||||||
<span>{{ t('warehouse.location') }}:
|
<span>{{ t('warehouse.location') }}:
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="item.ContainerID"
|
v-if="containerBreadcrumb"
|
||||||
:to="`/warehouse/container/${item.ContainerID}`"
|
:to="`/warehouse/container/${item.ContainerID}`"
|
||||||
class="text-blue-500 hover:underline ml-1"
|
class="text-blue-500 hover:underline ml-1"
|
||||||
>
|
>
|
||||||
{{ containerNames[item.ContainerID] || `#${item.ContainerID}` }}
|
{{ containerBreadcrumb }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<span v-else-if="item.ContainerID" class="text-blue-500 ml-1">{{ `#${item.ContainerID}` }}</span>
|
||||||
<span v-else class="text-orange-500 ml-1">{{ t('warehouse.unstored') }}</span>
|
<span v-else class="text-orange-500 ml-1">{{ t('warehouse.unstored') }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -499,9 +502,9 @@ onMounted(() => {
|
|||||||
<span>{{ fmtTs(commit.CreatedAt) }}</span>
|
<span>{{ fmtTs(commit.CreatedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 mt-0.5 flex-wrap text-sm font-medium text-gray-700 dark:text-gray-200">
|
<div class="flex items-center gap-1.5 mt-0.5 flex-wrap text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||||
<span>{{ getContainerName(commit.OldContainer) }}</span>
|
<span>{{ commit.OldContainerBreadcrumb || t('warehouse.unstored') }}</span>
|
||||||
<IconArrowRight :size="13" class="text-blue-500 flex-shrink-0" />
|
<IconArrowRight :size="13" class="text-blue-500 flex-shrink-0" />
|
||||||
<span>{{ getContainerName(commit.NewContainer) }}</span>
|
<span>{{ commit.NewContainerBreadcrumb || t('warehouse.unstored') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="commit.Remark" class="text-xs text-gray-400 mt-0.5">{{ commit.Remark }}</p>
|
<p v-if="commit.Remark" class="text-xs text-gray-400 mt-0.5">{{ commit.Remark }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -526,12 +529,13 @@ onMounted(() => {
|
|||||||
{{ t('warehouse.current_location') }}:
|
{{ t('warehouse.current_location') }}:
|
||||||
<span class="font-medium text-gray-700 dark:text-gray-300">
|
<span class="font-medium text-gray-700 dark:text-gray-300">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="item?.ContainerID"
|
v-if="containerBreadcrumb"
|
||||||
:to="`/warehouse/container/${item.ContainerID}`"
|
:to="`/warehouse/container/${item.ContainerID}`"
|
||||||
class="text-blue-500 hover:underline"
|
class="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{{ containerNames[item.ContainerID] || `#${item.ContainerID}` }}
|
{{ containerBreadcrumb }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<span v-else-if="item?.ContainerID">#{{ item.ContainerID }}</span>
|
||||||
<span v-else class="text-orange-500">{{ t('warehouse.unstored') }}</span>
|
<span v-else class="text-orange-500">{{ t('warehouse.unstored') }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,67 +29,11 @@ const currentPage = ref(1)
|
|||||||
const search = ref('')
|
const search = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// 容器名映射表
|
|
||||||
const containerMap = ref({})
|
|
||||||
const allContainerCount = ref(0)
|
|
||||||
|
|
||||||
const isEn = computed(() => locale.value === 'en')
|
|
||||||
|
|
||||||
// ── 统计数据 ──
|
|
||||||
const stats = reactive({
|
|
||||||
total: 0,
|
|
||||||
inContainer: 0,
|
|
||||||
unstored: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── 分页信息 ──
|
|
||||||
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value) || 1)
|
|
||||||
|
|
||||||
function getContainerTitle(cid) {
|
|
||||||
if (cid == null) return `<span class="text-gray-400">${t('warehouse.unstored_items')}</span>`
|
|
||||||
return containerMap.value[cid] || `#${cid}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 权限判断 ──
|
// ── 权限判断 ──
|
||||||
function canModifyItem(idx) {
|
function canModifyItem(idx) {
|
||||||
return canModifyItems.value[idx] === true
|
return canModifyItems.value[idx] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 获取容器名映射 ──
|
|
||||||
async function fetchContainerMap() {
|
|
||||||
try {
|
|
||||||
const { errCode, data } = await warehouseApi.getContainers({ entries: 500, page: 1 })
|
|
||||||
if (errCode === 0 && data) {
|
|
||||||
allContainerCount.value = data.all_count || 0
|
|
||||||
const map = {}
|
|
||||||
for (const c of (data.containers || [])) {
|
|
||||||
map[c.ID] = c.Title
|
|
||||||
}
|
|
||||||
for (const c of (data.containers || [])) {
|
|
||||||
if (c.ChildCount > 0) {
|
|
||||||
await fetchChildContainers(c.ID, map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
containerMap.value = map
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchChildContainers(parentId, map) {
|
|
||||||
try {
|
|
||||||
const { errCode, data } = await warehouseApi.getContainers({ entries: 500, page: 1, parent_id: parentId })
|
|
||||||
if (errCode === 0 && data) {
|
|
||||||
for (const c of (data.containers || [])) {
|
|
||||||
map[c.ID] = c.Title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 获取物品列表 ──
|
// ── 获取物品列表 ──
|
||||||
async function fetchItems() {
|
async function fetchItems() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -205,9 +149,7 @@ function formatDate(dateStr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(fetchItems)
|
||||||
fetchContainerMap().then(fetchItems)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -289,9 +231,9 @@ onMounted(() => {
|
|||||||
<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-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 text-sm">{{ item.Quantity }}</td>
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3">
|
||||||
<span v-if="item.ContainerID != null" class="inline-flex items-center gap-1 text-blue-600 text-sm">
|
<span v-if="item.ContainerBreadcrumb" class="inline-flex items-center gap-1 text-blue-600 text-sm">
|
||||||
<IconArrowRight :size="13" />
|
<IconArrowRight :size="13" />
|
||||||
<span class="truncate max-w-[140px]">{{ getContainerTitle(item.ContainerID) }}</span>
|
<span class="truncate max-w-[200px]">{{ item.ContainerBreadcrumb }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="inline-flex items-center gap-1 text-xs text-orange-500">
|
<span v-else class="inline-flex items-center gap-1 text-xs text-orange-500">
|
||||||
{{ t('warehouse.unstored_items') }}
|
{{ t('warehouse.unstored_items') }}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const containerPage = ref(1)
|
|||||||
const containerPageSize = ref(10)
|
const containerPageSize = ref(10)
|
||||||
const containerSearch = ref('')
|
const containerSearch = ref('')
|
||||||
const containerLoading = ref(false)
|
const containerLoading = ref(false)
|
||||||
const containerMap = ref({}) // id -> title
|
|
||||||
|
|
||||||
// 新增/编辑弹窗
|
// 新增/编辑弹窗
|
||||||
const showContainerForm = ref(false)
|
const showContainerForm = ref(false)
|
||||||
@@ -109,17 +108,6 @@ async function fetchContainers() {
|
|||||||
finally { containerLoading.value = false }
|
finally { containerLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAllContainerMap() {
|
|
||||||
try {
|
|
||||||
const { errCode, data } = await warehouseApi.getContainers({ entries: 500, page: 1 })
|
|
||||||
if (errCode === 0 && data) {
|
|
||||||
const map = {}
|
|
||||||
for (const c of (data.containers || [])) map[c.ID] = c.Title
|
|
||||||
containerMap.value = map
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function goContainerPage(page) {
|
function goContainerPage(page) {
|
||||||
if (page < 1 || page > containerTotalPages.value) return
|
if (page < 1 || page > containerTotalPages.value) return
|
||||||
containerPage.value = page
|
containerPage.value = page
|
||||||
@@ -304,10 +292,6 @@ async function doDeleteItem() {
|
|||||||
finally { deletingItem.value = false }
|
finally { deletingItem.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerTitle(cid) {
|
|
||||||
return containerMap.value[cid] || `#${cid}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 工具函数 ──
|
// ── 工具函数 ──
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return '—'
|
if (!dateStr) return '—'
|
||||||
@@ -349,7 +333,7 @@ function fmtTs(ts) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchContainerStats()
|
fetchContainerStats()
|
||||||
fetchContainers()
|
fetchContainers()
|
||||||
fetchAllContainerMap().then(() => fetchItems())
|
fetchItems()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -584,9 +568,9 @@ onMounted(() => {
|
|||||||
<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 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 text-center text-sm">{{ item.Quantity }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<span v-if="item.ContainerID != null" class="inline-flex items-center gap-1 text-sm text-blue-600">
|
<span v-if="item.ContainerBreadcrumb" class="inline-flex items-center gap-1 text-sm text-blue-600">
|
||||||
<IconArrowRight :size="13" />
|
<IconArrowRight :size="13" />
|
||||||
<span class="max-w-[140px] truncate">{{ getContainerTitle(item.ContainerID) }}</span>
|
<span class="max-w-[200px] truncate">{{ item.ContainerBreadcrumb }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="inline-flex items-center gap-1 text-xs text-orange-500">
|
<span v-else class="inline-flex items-center gap-1 text-xs text-orange-500">
|
||||||
{{ t('warehouse.unstored_items') }}
|
{{ t('warehouse.unstored_items') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user