558 lines
17 KiB
Vue
558 lines
17 KiB
Vue
<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>
|