mes-vue/src/views/biz/defectiveWarehouse/index.vue
shaoleiliu-netizen123 eec8421b61 提交代码
2026-09-10 11:04:27 +08:00

558 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="状态">
<n-select v-model:value="searchForm.status" placeholder="请选择状态" clearable style="width: 150px"
:options="statusOptions"/>
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon>
<n-icon>
<SearchOutline/>
</n-icon>
</template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon>
<n-icon>
<RefreshOutline/>
</n-icon>
</template>
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<n-button @click="importModalVisible = true">
<template #icon>
<n-icon>
<CloudUploadOutline/>
</n-icon>
</template>
导入
</n-button>
<n-button @click="handleExport">
<template #icon>
<n-icon>
<DownloadOutline/>
</n-icon>
</template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button>
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon>
<n-icon>
<TrashOutline/>
</n-icon>
</template>
删除
</n-button>
</n-space>
</div>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row:any) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
/>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="入库单号" path="inboundCode">
<n-input v-model:value="formData.inboundCode" disabled placeholder="请输入入库单号 OWR-yyyyMMdd-0001"/>
</n-form-item>
<n-form-item label="入库类型" path="inboundType">
<n-select v-model:value="formData.inboundType" placeholder="请选择入库类型"
:options="typeOptions"/>
</n-form-item>
<n-form-item label="入库数量" path="num">
<n-input-number v-model:value="formData.num" :min="0.0001" :precision="4" style="width: 100%" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入不良品库" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
<li>请先下载导入模板,按模板格式填写数据</li>
<li>支持 .xlsx 或 .xls 格式</li>
</ul>
</n-alert>
<n-space>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon>
<n-icon>
<DownloadOutline/>
</n-icon>
</template>
下载模板
</n-button>
</n-space>
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<CloudUploadOutline/>
</n-icon>
</div>
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
</n-upload-dragger>
</n-upload>
</n-space>
<template #footer>
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
<n-modal v-model:show="confirmVisible" preset="card" title="确认入库" style="width: 480px">
<n-form label-placement="left" label-width="120px">
<n-form-item label="入库单号">
<n-input :value="confirmRow?.inboundCode" disabled/>
</n-form-item>
<!-- <n-form-item label="金蝶入库单编码">-->
<!-- <n-input v-model:value="confirmKingdeeNo" placeholder="推送金蝶成功后的单号(可后补)" clearable />-->
<!-- </n-form-item>-->
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="confirmVisible = false">取消</n-button>
<n-button type="primary" :loading="submitLoading" @click="doConfirm">确认入库</n-button>
</n-space>
</template>
</n-modal>
<!-- 详情 -->
<n-modal v-model:show="detailVisible" preset="card" title="入库单详情" style="width: 720px">
<n-descriptions v-if="detailData" :column="2" bordered size="small" label-placement="left">
<n-descriptions-item label="入库单号">{{ detailData.inboundCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="当前阶段">{{ statusLabel(detailData.status) }}</n-descriptions-item>
<n-descriptions-item label="入库类型">{{ typeLabel(detailData.inboundType) }}</n-descriptions-item>
<n-descriptions-item label="来源单号">{{ detailData.sourceOrderId || '-' }}</n-descriptions-item>
<n-descriptions-item label="金蝶入库单编码">{{ detailData.kingdeeInboundNo || '-' }}</n-descriptions-item>
<!-- <n-descriptions-item label="物料编码">{{ detailData.materialCode || '-' }}</n-descriptions-item>-->
<!-- <n-descriptions-item label="物料名称">{{ detailData.materialName || '-' }}</n-descriptions-item>-->
<n-descriptions-item label="入库数量">{{ formatQty(detailData.num) }}</n-descriptions-item>
<!-- <n-descriptions-item label="入库仓库">{{ detailData.warehouseName || '-' }}</n-descriptions-item>-->
<!-- <n-descriptions-item label="入库进度">{{ detailData.progress ?? 0 }}%</n-descriptions-item>-->
<!-- <n-descriptions-item label="创建时间">{{ detailData.createTime || '-' }}</n-descriptions-item>-->
</n-descriptions>
</n-modal>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, h, onMounted} from 'vue'
import {
NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NTag, NInput, NForm, NModal, NFormItem, NDescriptionsItem, NDescriptions, NInputNumber
} from 'naive-ui'
import {
SearchOutline,
RefreshOutline,
TrashOutline,
CreateOutline,
CloudUploadOutline,
DownloadOutline
} from '@vicons/ionicons5'
import {STOCK_IN_STATUS_MAP, STOCK_IN_TYPE_MAP,defectiveWarehouseApi, type DefectiveWarehouse} from '@/api/defectiveWarehouse'
import {dictDataApi} from '@/api/org'
import { type StockIn} from "@/api/stockIn.ts";
const message = useMessage()
const dialog = useDialog()
// 搜索表单
const searchForm = reactive({
id: null as number | null,
status: null as number | null,
})
// 表格数据
const tableData = ref<DefectiveWarehouse[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
// 弹窗
const detailVisible = ref(false)
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
const formRef = ref()
const detailData = ref<DefectiveWarehouse>({})
const defaultFormData: DefectiveWarehouse = {
processId: undefined,
sourceOrderId: undefined,
inboundCode: '',
subjectName: '',
inboundType: undefined,
processPlanId: undefined,
warehouseId: undefined,
status: undefined,
businessType: '',
}
const formData = reactive<DefectiveWarehouse>({...defaultFormData})
// 字典选项(下拉框/单选框/复选框关联字典时使用)
const statusOptions = ref<{ label: string; value: any; class: any }[]>([])
// 表单校验规则
const formRules = {}
// 表格列
const columns: DataTableColumns<DefectiveWarehouse> = [
{type: 'selection'},
{title: '入库单号', key: 'inboundCode'},
{
title: '入库类型', key: 'inboundType',
render(row) {
const item = STOCK_IN_TYPE_MAP[row.inboundType ?? 1] ?? STOCK_IN_TYPE_MAP[1]
return h(NTag, {type: item.type, size: 'small', round: true}, {default: () => item.label})
}
},
{title: '入库数量', key: 'num'},
{title: '主体名称', key: 'subjectName'},
{
title: '状态', key: 'status',
render(row) {
const val = row.status
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return h(NTag, {type: opt.class, size: 'small'}, {default: () => opt.label})
}
},
{
title: '操作',
key: 'actions',
width: 140,
fixed: 'right',
render(row) {
var status = row.status
if (status == 0 || status == 1) {
return h('div', {style: {display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap'}}, [
h(NButton, {size: 'small', quaternary: true, onClick: () => handleDetail(row)}, {
default: () => ['详情']
}),
h(NButton, {size: 'small', quaternary: true, onClick: () => handleConfirm(row)}, {
default: () => [h(NIcon, null, {default: () => h(CreateOutline)}), '确认入库']
}),
h(NButton, {size: 'small', quaternary: true, onClick: () => handleEdit(row)}, {
default: () => [h(NIcon, null, {default: () => h(CreateOutline)}), ' 编辑']
}),
h(NButton, {size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row)}, {
default: () => [h(NIcon, null, {default: () => h(TrashOutline)}), ' 删除']
})
])
} else if (status == 2) {
return h('div', {style: {display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap'}}, [
h(NButton, {size: 'small', quaternary: true, onClick: () => handleDetail(row)}, {
default: () => ['详情']
}),
h(NButton, {size: 'small', quaternary: true, onClick: () => handleCancel(row)}, {
default: () => [h(NIcon, null, {default: () => h(CreateOutline)}), '撤回入库']
})
])
}
}
}
]
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await defectiveWarehouseApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
status: searchForm.status || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.page = 1
loadData()
}
// 重置
function handleReset() {
searchForm.id = null
searchForm.status = null
handleSearch()
}
// 分页
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
// 选择
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
}
// 编辑
function handleEdit(row: DefectiveWarehouse) {
modalTitle.value = '编辑不良品库'
Object.assign(formData, row)
modalVisible.value = true
}
// 提交
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = {...formData} as DefectiveWarehouse
if (submitData.id) {
await defectiveWarehouseApi.update(submitData)
message.success('修改成功')
} else {
await defectiveWarehouseApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 删除
function handleDelete(row: DefectiveWarehouse) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await defectiveWarehouseApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 批量删除
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await defectiveWarehouseApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 导出
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.id != null) params.id = searchForm.id
if (searchForm.status != null) params.status = searchForm.status
const blob = await defectiveWarehouseApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '不良品库数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 下载导入模板
async function handleDownloadTemplate() {
try {
const blob = await defectiveWarehouseApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '不良品库导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 导入上传
async function handleImportUpload({file}: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await defectiveWarehouseApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
positiveText: '确定'
})
} else {
message.success(`导入成功,共 ${result.success} 条数据`)
importModalVisible.value = false
}
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
const confirmVisible = ref(false)
const confirmRow = ref<StockIn | null>(null)
//确认入库
const handleConfirm = async (row: any) => {
confirmVisible.value = true
confirmRow.value = row
}
const doConfirm = async () => {
try {
await defectiveWarehouseApi.confirm(confirmRow.value.id)
message.success("入库成功")
loadData()
} catch (error) {
message.error("确认入库异常")
} finally {
confirmVisible.value = false
}
}
//撤回入库
const handleCancel = async (row: any) => {
dialog.warning({
title: '撤回入库',
content: `确认撤回入库单 ${row.inboundCode}?`,
positiveText: '撤回',
negativeText: '取消',
onPositiveClick: async () => {
try {
await defectiveWarehouseApi.cancel(row.id!)
message.success('已撤回')
loadData()
} catch (e: any) {
message.error(e?.message || '撤回失败')
}
},
})
}
//详情
const handleDetail = (row:any)=>{
detailVisible.value = true
detailData.value = row
}
function statusLabel(status?: number) {
if (status == null) return '-'
return STOCK_IN_STATUS_MAP[status]?.label ?? String(status)
}
function typeLabel(type?: number) {
if (type == null) return '-'
return STOCK_IN_TYPE_MAP[type]?.label ?? String(type)
}
function formatQty(val?: number | string | null) {
if (val == null || val === '') return '0'
return Number(val).toLocaleString()
}
const typeOptions = Object.entries(STOCK_IN_TYPE_MAP).map(([value, item]) => ({
label: item.label,
value: Number(value),
}))
// 加载字典选项
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('defetive_status')
statusOptions.value = data.map(d => ({label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue)}))
} catch {
}
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>