front/src/views/warehouse/inboundNotice/index.vue
zzh 5191d1981f feat: 仓储模块前端、菜单同步扩展及相关页面优化
新增仓储模块页面与 API、店铺授权/店铺订单页面及路由权限目录;
扩展菜单管理同步能力支持二级子菜单与三级按钮;优化公告、仓库地址、下单等页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 15:19:35 +08:00

317 lines
12 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import FormPro from '@/components/FormPro/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import {
deleteWarehouseInbound,
getWarehouseInboundDetail,
getWarehouseInboundList,
getWarehouseInboundSummary,
saveWarehouseInbound,
} from '@/api/warehouse'
import { getWarehouseList } from '@/api/system/warehouse'
import { getWarehouseSkuList } from '@/api/warehouse'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
defineOptions({ name: 'WarehouseInboundNotice' })
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const receiptStatus = ref<string>()
const summary = ref({ forecasted: 0, received: 0, completed: 0 })
const keyword = ref('')
const warehouseId = ref<number>()
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const skuOptions = ref<Array<{ label: string; value: number; code: string }>>([])
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(30)
const totalCount = ref(0)
const statusTabs = [
{ value: undefined, labelKey: 'warehouse.inbound.status.all', countKey: null },
{ value: 'forecasted', labelKey: 'warehouse.inbound.status.forecasted', countKey: 'forecasted' },
{ value: 'completed', labelKey: 'warehouse.inbound.status.completed', countKey: 'completed' },
]
const detailVisible = ref(false)
const detailData = ref<any>(null)
const { visible: modalVisible, open: openModal, close: closeModal } = useModal()
const saving = ref(false)
const formModel = reactive({
id: 0,
warehouseId: undefined as number | undefined,
markReference: '',
inboundType: 'normal',
remark: '',
skuId: undefined as number | undefined,
plannedQty: 1,
markBoxNo: '',
})
const hotColumns = computed(() => [
{ key: 'noticeNo', label: t('warehouse.inbound.table.noticeNo'), width: 180 },
{ key: 'markReference', label: t('warehouse.inbound.table.markReference'), width: 120 },
{ key: 'receiptStatus', label: t('warehouse.inbound.table.receiptStatus'), width: 100 },
{ key: 'boxCount', label: t('warehouse.inbound.table.boxCount'), width: 80 },
{ key: 'plannedQty', label: t('warehouse.inbound.table.plannedQty'), width: 90 },
{ key: 'createTime', label: t('warehouse.inbound.table.createTime'), width: 170 },
])
async function loadWarehouses() {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
warehouseOptions.value = (items || []).map((w: any) => ({
label: w.warehouseName || `#${w.id}`,
value: Number(w.id),
}))
}
async function loadSkus() {
const { items } = await getWarehouseSkuList({ page: 1, pageSize: 500 }).catch(() => ({ items: [] }))
skuOptions.value = (items || []).map((s: any) => ({
label: `${s.skuCode} - ${s.skuName}`,
value: Number(s.id),
code: s.skuCode,
}))
}
async function loadSummary() {
try {
const data = await getWarehouseInboundSummary({ warehouseId: warehouseId.value })
summary.value = {
forecasted: data?.forecasted ?? data?.Forecasted ?? 0,
received: data?.received ?? data?.Received ?? 0,
completed: data?.completed ?? data?.Completed ?? 0,
}
} catch {
summary.value = { forecasted: 0, received: 0, completed: 0 }
}
}
async function loadList() {
listLoading.value = true
try {
const { items, total } = await getWarehouseInboundList({
page: currentPage.value,
pageSize: pageSize.value,
receiptStatus: receiptStatus.value,
keyword: keyword.value || undefined,
warehouseId: warehouseId.value,
})
tableRows.value = (items || []).map((r: any) => ({
...r,
receiptStatus: t(`warehouse.inbound.status.${r.receiptStatus}`, r.receiptStatus),
_raw: r,
}))
totalCount.value = Number(total || 0)
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
async function showDetail(row: any) {
try {
detailData.value = await getWarehouseInboundDetail(row._raw?.id || row.id)
detailVisible.value = true
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
}
}
function handleAdd() {
if (!hasPermission('Warehouse.InboundNotice.Save')) return
formModel.id = 0
formModel.warehouseId = warehouseId.value
formModel.markReference = ''
formModel.remark = ''
formModel.skuId = undefined
formModel.plannedQty = 1
formModel.markBoxNo = ''
openModal()
}
async function handleSave() {
if (!formModel.warehouseId || !formModel.skuId) {
$baseMessage(t('warehouse.inbound.message.required'), 'warning')
return
}
const sku = skuOptions.value.find((s) => s.value === formModel.skuId)
saving.value = true
try {
await saveWarehouseInbound({
id: formModel.id,
warehouseId: formModel.warehouseId,
markReference: formModel.markReference,
inboundType: formModel.inboundType,
remark: formModel.remark,
boxes: [
{
boxSeqNo: '1',
markBoxNo: formModel.markBoxNo || 'BOX-001',
lines: [{ skuId: formModel.skuId, skuCode: sku?.code || '', plannedQty: formModel.plannedQty }],
},
],
})
$baseMessage(t('warehouse.inbound.message.saveSuccess'), 'success')
closeModal()
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.saveFailed'), 'error')
} finally {
saving.value = false
}
}
function handleDelete(row: any) {
if (!hasPermission('Warehouse.InboundNotice.Delete')) return
Modal.confirm({
title: t('warehouse.inbound.actions.delete'),
content: row._raw?.noticeNo || row.noticeNo,
onOk: async () => {
await deleteWarehouseInbound(row._raw?.id || row.id)
$baseMessage(t('warehouse.inbound.message.deleteSuccess'), 'success')
await loadSummary()
await loadList()
},
})
}
function actionRenderer(row: any) {
const parts = [`<button type="button" class="hot-action-btn" data-action="detail">${t('warehouse.common.detail')}</button>`]
const rawStatus = row._raw?.receiptStatus
if (hasPermission('Warehouse.InboundNotice.Delete') && rawStatus === 'forecasted') {
parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`)
}
return parts.join('')
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'detail') showDetail(row)
if (action === 'delete') handleDelete(row)
}
onMounted(async () => {
await loadWarehouses()
await loadSkus()
await loadSummary()
await loadList()
})
</script>
<template>
<div class="warehouse-inbound-container">
<div class="page-card">
<a-tabs v-model:active-key="receiptStatus" @change="() => { currentPage = 1; loadList() }">
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'">
<template #title>
{{ t(tab.labelKey) }}
<span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey as 'forecasted' | 'completed'] }})</span>
</template>
</a-tab-pane>
</a-tabs>
<div class="table-toolbar">
<div class="toolbar-left">
<a-space wrap :size="8">
<a-select v-model="warehouseId" allow-clear :placeholder="t('warehouse.common.warehouse')" :options="warehouseOptions" style="width: 200px" @change="() => { loadSummary(); currentPage = 1; loadList() }" />
<a-input v-model="keyword" allow-clear :placeholder="t('warehouse.inbound.search.keyword')" style="width: 200px" />
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Warehouse.InboundNotice.Save')" type="primary" @click="handleAdd">{{ t('warehouse.common.add') }}</a-button>
</a-space>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro :columns="hotColumns" :data="tableRows" :height="tableHeight" :loading="listLoading" show-action-column :action-renderer="actionRenderer" @action-click="handleActionClick" />
</div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div>
<a-drawer v-model:visible="detailVisible" :title="t('warehouse.inbound.drawer.detail')" :width="640">
<template v-if="detailData">
<p>{{ t('warehouse.inbound.table.noticeNo') }}: {{ detailData.noticeNo }}</p>
<div v-for="box in detailData.boxes || []" :key="box.id" class="box-block">
<div class="box-title">{{ t('warehouse.inbound.box') }}: {{ box.markBoxNo || box.boxSeqNo }}</div>
<a-table :data="box.lines || []" :pagination="false" size="small">
<template #columns>
<a-table-column :title="t('warehouse.inbound.line.skuCode')" data-index="skuCode" />
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" data-index="plannedQty" />
<a-table-column :title="t('warehouse.inbound.line.receivedQty')" data-index="receivedQty" />
</template>
</a-table>
</div>
</template>
</a-drawer>
<ModalPro v-model:visible="modalVisible" :title="t('warehouse.inbound.modal.add')" :width="640" @cancel="closeModal">
<FormPro :model="formModel" layout="horizontal" :row-gutter="20" :default-span="12">
<a-form-item :label="t('warehouse.common.warehouse')" required>
<a-select v-model="formModel.warehouseId" :options="warehouseOptions" />
</a-form-item>
<a-form-item :label="t('warehouse.inbound.form.markReference')">
<a-input v-model="formModel.markReference" />
</a-form-item>
<a-form-item :label="t('warehouse.inbound.form.markBoxNo')">
<a-input v-model="formModel.markBoxNo" />
</a-form-item>
<a-form-item :label="t('warehouse.inbound.form.sku')" required>
<a-select v-model="formModel.skuId" :options="skuOptions" allow-search />
</a-form-item>
<a-form-item :label="t('warehouse.inbound.form.plannedQty')" required>
<a-input-number v-model="formModel.plannedQty" :min="1" style="width: 100%" />
</a-form-item>
</FormPro>
<template #footer>
<a-button @click="closeModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">{{ t('warehouse.common.save') }}</a-button>
</template>
</ModalPro>
</div>
</template>
<style scoped lang="less">
.warehouse-inbound-container {
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
.box-block {
margin-bottom: 16px;
}
.box-title {
font-weight: 500;
margin-bottom: 8px;
}
.tab-count {
margin-left: 4px;
color: var(--app-primary-color);
}
</style>