新增仓储模块页面与 API、店铺授权/店铺订单页面及路由权限目录; 扩展菜单管理同步能力支持二级子菜单与三级按钮;优化公告、仓库地址、下单等页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
317 lines
12 KiB
Vue
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>
|