feat(return): 前端退回入口与不良品在管台账看板
出库记录页(views/outbound/index.vue): - 明细行新增「退回」列,returnable_quantity <= 0 时按钮置灰,行内显示「已退 N」 - 对话框展示 原出库数量 / 已退回数量 / 本次可退最大,默认带入可退最大值 - 退回类型用下拉单选:良品(加回库存)/ 不良品(转入异常待处理) - 退回原因必填,提交前三重校验;按钮 :loading + 函数内 submitting 双保险防抖 - 成功后刷新列表。错误提示不重复弹——request 拦截器对 HTTP 400 已取 data.msg 展示,对话框 catch 只收尾 配套后端(services/outbound_service.py): - get_grouped_list 的出库明细补 id / returned_quantity / returnable_quantity。 原先明细不带 id,退回接口无从指定 outbound_id 新增页面(views/stock/defective/index.vue,路由 /inventory/defective): - 展示 物料名称/规格/SKU/退回时间/操作人/退回总数/剩余待处理/状态 - 顶部 alert 提示在管总量,并明确「这批实物不在库存表中,盘点请以本台账 为准」——在管坏件对盘点不可见是本方案的固有盲区,必须在页面上主动提醒 - 仅对 remaining_qty > 0 的行提供「修复回库」「报废销毁」,终态行显示已结案 - 两个弹窗均带数量上限约束与 loading 防抖,成功后刷新 新增 api/inbound/return.ts 承载四个逆向物流接口。 侧边栏由 router/index.ts 驱动,加路由即入菜单;注意侧边栏只过滤 meta.hidden、不看 meta.permission,故菜单对所有角色可见,访问控制由后端 接口负责(与既有「维修管理」一致)。
This commit is contained in:
86
inventory-web/src/api/inbound/return.ts
Normal file
86
inventory-web/src/api/inbound/return.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ============================================================================
|
||||
// 逆向物流(原单退回 + 不良品在管台账)
|
||||
//
|
||||
// 后端路由见 inventory-backend/app/api/v1/inbound/stock.py
|
||||
// 设计要点:
|
||||
// · 良品退回 → 直接加回原库存行
|
||||
// · 不良品退回 → 不进库存表,转入 trans_defective_goods 在管台账,
|
||||
// 修好后回库 / 无法维修则报废销毁
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 原单退回。
|
||||
*
|
||||
* @param data {
|
||||
* outbound_id: number 必填,trans_outbound.id(出库**明细行**主键,非单号)
|
||||
* return_qty: number 必填,本次退回数量(>0)
|
||||
* is_defective: boolean 必填,true=不良品转在管台账,false=良品加回库存
|
||||
* reason: string 可选,退回原因
|
||||
* }
|
||||
*/
|
||||
export function returnFromOutbound(data: {
|
||||
outbound_id: number
|
||||
return_qty: number
|
||||
is_defective: boolean
|
||||
reason?: string
|
||||
}) {
|
||||
return request({
|
||||
url: '/inbound/stock/return-from-outbound',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 不良品在管台账分页查询。
|
||||
*
|
||||
* @param params { page, page_size, status?, keyword?, start_date?, end_date? }
|
||||
* status 传 '全部' 或留空表示不过滤
|
||||
*/
|
||||
export function getDefectiveList(params: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: string
|
||||
keyword?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
company_name?: string
|
||||
}) {
|
||||
return request({
|
||||
url: '/inbound/stock/defective',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复回库:把修好的数量加回原库存行。
|
||||
*
|
||||
* @param id 在管台账 id
|
||||
* @param data { restock_qty?: number, remark?: string }
|
||||
* restock_qty 缺省 = 全部剩余在管量
|
||||
*/
|
||||
export function restockDefective(id: number, data: { restock_qty?: number; remark?: string }) {
|
||||
return request({
|
||||
url: `/inbound/stock/defective/${id}/restock`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 报废销毁:在管坏件确认无法维修时销毁。
|
||||
*
|
||||
* @param id 在管台账 id
|
||||
* @param data { scrap_qty?: number, reason: string }
|
||||
* reason 必填;scrap_qty 缺省 = 全部剩余在管量
|
||||
*/
|
||||
export function scrapDefective(id: number, data: { scrap_qty?: number; reason: string }) {
|
||||
return request({
|
||||
url: `/inbound/stock/defective/${id}/scrap`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
@ -106,6 +106,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'RepairManagement',
|
||||
component: () => import('@/views/stock/inbound/repair.vue'),
|
||||
meta: { title: '维修管理', permission: 'inbound_repair' }
|
||||
},
|
||||
// ★ 不良品在管台账(逆向物流):退回的坏件不入库存表,
|
||||
// 实物在仓但不在库存行中,故单独成页承载其去向与处置。
|
||||
{
|
||||
path: 'defective',
|
||||
name: 'DefectiveGoods',
|
||||
component: () => import('@/views/stock/defective/index.vue'),
|
||||
meta: { title: '不良品在管台账', permission: 'inventory_stocktake' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -120,6 +120,28 @@
|
||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- ★ 原单退回:良品加回库存 / 不良品转入异常待处理台账。
|
||||
已退满的行(returnable_quantity <= 0)按钮置灰不可点。 -->
|
||||
<el-table-column label="退回" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="canReturn">
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
:disabled="!row.returnable_quantity || row.returnable_quantity <= 0"
|
||||
@click="openReturnDialog(row)"
|
||||
>
|
||||
退回
|
||||
</el-button>
|
||||
<span v-if="row.returned_quantity > 0" class="returned-hint">
|
||||
已退 {{ row.returned_quantity }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="returned-hint">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
@ -183,18 +205,94 @@
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ★ 原单退回对话框 -->
|
||||
<el-dialog
|
||||
v-model="returnDialog.visible"
|
||||
title="原单退回"
|
||||
width="540px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetReturnDialog"
|
||||
>
|
||||
<el-form :model="returnDialog.form" label-width="120px">
|
||||
<el-form-item label="物料">
|
||||
<span class="dialog-text">
|
||||
{{ returnDialog.row?.name || '-' }}
|
||||
<span class="dialog-sub">/ {{ returnDialog.row?.sku || '-' }}</span>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="原出库数量">
|
||||
<span class="dialog-text">{{ returnDialog.row?.quantity ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="已退回数量">
|
||||
<span class="dialog-text">{{ returnDialog.row?.returned_quantity ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="本次可退最大">
|
||||
<span class="dialog-text dialog-strong">
|
||||
{{ returnDialog.row?.returnable_quantity ?? 0 }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="本次退回数量" required>
|
||||
<el-input-number
|
||||
v-model="returnDialog.form.return_qty"
|
||||
:min="0"
|
||||
:max="returnDialog.row?.returnable_quantity || 0"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退回类型" required>
|
||||
<el-select v-model="returnDialog.form.is_defective" style="width: 260px">
|
||||
<el-option :value="false" label="良品(加回库存)" />
|
||||
<el-option :value="true" label="不良品(转入异常待处理)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退回原因" required>
|
||||
<el-input
|
||||
v-model="returnDialog.form.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请填写退回原因(必填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="returnDialog.visible = false">取消</el-button>
|
||||
<!-- loading 兼作前端防抖:提交期间按钮不可再点 -->
|
||||
<el-button type="primary" :loading="returnDialog.submitting" @click="submitReturn">
|
||||
确定提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOutboundList } from '@/api/outbound'
|
||||
import { returnFromOutbound } from '@/api/inbound/return'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import CompanySelector from '@/components/CompanySelector.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 退回按钮的可见性。后端同样会校验权限(inventory_stocktake:operation),
|
||||
// 前端只是不给无权限用户显示一个必然失败的按钮。
|
||||
const canReturn = computed(() =>
|
||||
userStore.role === 'SUPER_ADMIN'
|
||||
|| userStore.username === 'IRIS'
|
||||
|| userStore.hasPermission('inventory_stocktake:operation')
|
||||
)
|
||||
|
||||
// 防抖定时器
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@ -381,6 +479,77 @@ const getTagType = (type: string) => {
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ★ 原单退回
|
||||
// ============================================================
|
||||
const returnDialog = reactive({
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: null as any,
|
||||
form: {
|
||||
return_qty: 1,
|
||||
is_defective: false,
|
||||
reason: '',
|
||||
},
|
||||
})
|
||||
|
||||
const openReturnDialog = (row: any) => {
|
||||
returnDialog.row = row
|
||||
// 默认带入「本次可退最大」,业务上整行退回最常见;用户可再调小做部分退回
|
||||
returnDialog.form.return_qty = Number(row?.returnable_quantity || 0)
|
||||
returnDialog.form.is_defective = false
|
||||
returnDialog.form.reason = ''
|
||||
returnDialog.visible = true
|
||||
}
|
||||
|
||||
const resetReturnDialog = () => {
|
||||
returnDialog.row = null
|
||||
returnDialog.submitting = false
|
||||
returnDialog.form.return_qty = 1
|
||||
returnDialog.form.is_defective = false
|
||||
returnDialog.form.reason = ''
|
||||
}
|
||||
|
||||
const submitReturn = async () => {
|
||||
if (returnDialog.submitting) return // 双保险:即便按钮 loading 被绕过也不重复提交
|
||||
|
||||
const maxQty = Number(returnDialog.row?.returnable_quantity || 0)
|
||||
const qty = Number(returnDialog.form.return_qty || 0)
|
||||
const reason = (returnDialog.form.reason || '').trim()
|
||||
|
||||
if (!qty || qty <= 0) {
|
||||
ElMessage.warning('请填写本次退回数量')
|
||||
return
|
||||
}
|
||||
if (qty > maxQty) {
|
||||
ElMessage.warning(`本次退回数量不能超过可退最大数量 ${maxQty}`)
|
||||
return
|
||||
}
|
||||
if (!reason) {
|
||||
ElMessage.warning('请填写退回原因')
|
||||
return
|
||||
}
|
||||
|
||||
returnDialog.submitting = true
|
||||
try {
|
||||
const res = await returnFromOutbound({
|
||||
outbound_id: returnDialog.row.id,
|
||||
return_qty: qty,
|
||||
is_defective: returnDialog.form.is_defective,
|
||||
reason,
|
||||
})
|
||||
ElMessage.success(res?.msg || '退回成功')
|
||||
returnDialog.visible = false
|
||||
// 刷新列表:退回额度与库存状态都已变化
|
||||
await fetchData()
|
||||
} catch (e) {
|
||||
// 错误提示由 request 拦截器统一弹出(400 会展示后端业务文案),此处只收尾
|
||||
console.error('[退回] 失败', e)
|
||||
} finally {
|
||||
returnDialog.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@ -419,6 +588,24 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* ★ 原单退回:明细行的「已退 N」提示与对话框文案样式 */
|
||||
.returned-hint {
|
||||
color: #E6A23C;
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.dialog-text {
|
||||
color: #303133;
|
||||
}
|
||||
.dialog-sub {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog-strong {
|
||||
color: #409EFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
448
inventory-web/src/views/stock/defective/index.vue
Normal file
448
inventory-web/src/views/stock/defective/index.vue
Normal file
@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 顶部汇总:在管坏件是「不在库存表里」的实物,用这个数字提醒别漏盘 -->
|
||||
<el-alert
|
||||
v-if="pendingTotal > 0"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
当前在管不良品剩余待处理:<b>{{ pendingTotal }}</b>
|
||||
件。这批实物不在库存表中,盘点时请以本台账为准。
|
||||
</el-alert>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<el-form :inline="true" class="filter-form" @submit.prevent>
|
||||
<el-form-item>
|
||||
<CompanySelector v-model="query.company_name" @change="handleSearch" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="query.status" placeholder="全部" style="width: 150px" clearable>
|
||||
<el-option label="全部" value="全部" />
|
||||
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
placeholder="物料名称 / SKU / 规格"
|
||||
style="width: 240px;"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退回时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
border
|
||||
style="width: 100%; margin-top: 16px;"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||||
>
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.material_name">{{ row.material_name }}</span>
|
||||
<span v-else style="color:#c0c4cc">(原库存行已删除)</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.spec_model">{{ row.spec_model }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sku" label="SKU" width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="退回时间" width="170" align="center">
|
||||
<template #default="{ row }">{{ row.created_at || '-' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="operator" label="操作人" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.operator">{{ row.operator }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="quantity" label="退回总数" width="100" align="right" />
|
||||
|
||||
<el-table-column label="剩余待处理" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.remaining_qty > 0 ? '#E6A23C' : '#909399', fontWeight: 'bold' }">
|
||||
{{ row.remaining_qty }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- ★ 仅在还有在管量时提供处置入口:终态(已回库/已报废/已闭环)
|
||||
的 remaining_qty 已归零,再操作只会拿到后端 400 -->
|
||||
<template v-if="row.remaining_qty > 0">
|
||||
<el-button type="success" link size="small" @click="openRestockDialog(row)">
|
||||
修复回库
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="openScrapDialog(row)">
|
||||
报废销毁
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">已结案</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:total="total"
|
||||
:page-size="query.page_size"
|
||||
:current-page="query.page"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ============ 修复回库 ============ -->
|
||||
<el-dialog
|
||||
v-model="restockDialog.visible"
|
||||
title="修复回库"
|
||||
width="460px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetRestockDialog"
|
||||
>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="物料">
|
||||
<span class="dlg-text">{{ restockDialog.row?.material_name || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="在管数量">
|
||||
<span class="dlg-text dlg-strong">{{ restockDialog.row?.remaining_qty ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="本次回库数量" required>
|
||||
<el-input-number
|
||||
v-model="restockDialog.form.restock_qty"
|
||||
:min="0"
|
||||
:max="restockDialog.row?.remaining_qty || 0"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="restockDialog.form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="如:已更换主板(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dlg-tip">回库后数量将加回原库存行的实物数与可用数,状态变为「在库」。</div>
|
||||
<template #footer>
|
||||
<el-button @click="restockDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="restockDialog.submitting" @click="submitRestock">
|
||||
确定回库
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============ 报废销毁 ============ -->
|
||||
<el-dialog
|
||||
v-model="scrapDialog.visible"
|
||||
title="报废销毁"
|
||||
width="460px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetScrapDialog"
|
||||
>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="物料">
|
||||
<span class="dlg-text">{{ scrapDialog.row?.material_name || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="在管数量">
|
||||
<span class="dlg-text dlg-strong">{{ scrapDialog.row?.remaining_qty ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="本次报废数量" required>
|
||||
<el-input-number
|
||||
v-model="scrapDialog.form.scrap_qty"
|
||||
:min="0"
|
||||
:max="scrapDialog.row?.remaining_qty || 0"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="报废原因" required>
|
||||
<el-input
|
||||
v-model="scrapDialog.form.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请填写报废原因(必填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dlg-tip dlg-danger">
|
||||
报废后实物视为已销毁,不可撤销;同时会写入报废台账。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="scrapDialog.visible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="scrapDialog.submitting" @click="submitScrap">
|
||||
确定报废
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDefectiveList, restockDefective, scrapDefective } from '@/api/inbound/return'
|
||||
import CompanySelector from '@/components/CompanySelector.vue'
|
||||
|
||||
// 与后端 app/models/transaction.py 的 VALID_DEFECTIVE_STATUSES 保持一致
|
||||
const statusOptions = ['待处理', '处理中', '已回库', '已报废', '已闭环']
|
||||
|
||||
const statusTagType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'待处理': 'warning',
|
||||
'处理中': 'primary',
|
||||
'已回库': 'success',
|
||||
'已报废': 'danger',
|
||||
'已闭环': 'info',
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const pendingTotal = ref(0)
|
||||
const loading = ref(false)
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: '全部',
|
||||
keyword: '',
|
||||
company_name: '' as string,
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDefectiveList({
|
||||
page: query.page,
|
||||
page_size: query.page_size,
|
||||
status: query.status,
|
||||
keyword: query.keyword,
|
||||
start_date: dateRange.value?.[0] || undefined,
|
||||
end_date: dateRange.value?.[1] || undefined,
|
||||
company_name: query.company_name || undefined,
|
||||
})
|
||||
list.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
pendingTotal.value = res.data.pending_total || 0
|
||||
} catch (e) {
|
||||
console.error('[不良品台账] 查询失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
query.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePageChange = (val: number) => {
|
||||
query.page = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
query.page = 1
|
||||
query.status = '全部'
|
||||
query.keyword = ''
|
||||
query.company_name = ''
|
||||
dateRange.value = []
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ★ 修复回库
|
||||
// ============================================================
|
||||
const restockDialog = reactive({
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: null as any,
|
||||
form: { restock_qty: 0, remark: '' },
|
||||
})
|
||||
|
||||
const openRestockDialog = (row: any) => {
|
||||
restockDialog.row = row
|
||||
restockDialog.form.restock_qty = Number(row?.remaining_qty || 0)
|
||||
restockDialog.form.remark = ''
|
||||
restockDialog.visible = true
|
||||
}
|
||||
|
||||
const resetRestockDialog = () => {
|
||||
restockDialog.row = null
|
||||
restockDialog.submitting = false
|
||||
restockDialog.form.restock_qty = 0
|
||||
restockDialog.form.remark = ''
|
||||
}
|
||||
|
||||
const submitRestock = async () => {
|
||||
if (restockDialog.submitting) return // 双保险,防手抖连击
|
||||
|
||||
const max = Number(restockDialog.row?.remaining_qty || 0)
|
||||
const qty = Number(restockDialog.form.restock_qty || 0)
|
||||
if (!qty || qty <= 0) {
|
||||
ElMessage.warning('请填写本次回库数量')
|
||||
return
|
||||
}
|
||||
if (qty > max) {
|
||||
ElMessage.warning(`回库数量不能超过在管数量 ${max}`)
|
||||
return
|
||||
}
|
||||
|
||||
restockDialog.submitting = true
|
||||
try {
|
||||
const res = await restockDefective(restockDialog.row.id, {
|
||||
restock_qty: qty,
|
||||
remark: restockDialog.form.remark || undefined,
|
||||
})
|
||||
ElMessage.success(res?.msg || '回库成功')
|
||||
restockDialog.visible = false
|
||||
await fetchData()
|
||||
} catch (e) {
|
||||
console.error('[修复回库] 失败', e)
|
||||
} finally {
|
||||
restockDialog.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ★ 报废销毁
|
||||
// ============================================================
|
||||
const scrapDialog = reactive({
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: null as any,
|
||||
form: { scrap_qty: 0, reason: '' },
|
||||
})
|
||||
|
||||
const openScrapDialog = (row: any) => {
|
||||
scrapDialog.row = row
|
||||
scrapDialog.form.scrap_qty = Number(row?.remaining_qty || 0)
|
||||
scrapDialog.form.reason = ''
|
||||
scrapDialog.visible = true
|
||||
}
|
||||
|
||||
const resetScrapDialog = () => {
|
||||
scrapDialog.row = null
|
||||
scrapDialog.submitting = false
|
||||
scrapDialog.form.scrap_qty = 0
|
||||
scrapDialog.form.reason = ''
|
||||
}
|
||||
|
||||
const submitScrap = async () => {
|
||||
if (scrapDialog.submitting) return
|
||||
|
||||
const max = Number(scrapDialog.row?.remaining_qty || 0)
|
||||
const qty = Number(scrapDialog.form.scrap_qty || 0)
|
||||
const reason = (scrapDialog.form.reason || '').trim()
|
||||
|
||||
if (!qty || qty <= 0) {
|
||||
ElMessage.warning('请填写本次报废数量')
|
||||
return
|
||||
}
|
||||
if (qty > max) {
|
||||
ElMessage.warning(`报废数量不能超过在管数量 ${max}`)
|
||||
return
|
||||
}
|
||||
if (!reason) {
|
||||
ElMessage.warning('请填写报废原因')
|
||||
return
|
||||
}
|
||||
|
||||
scrapDialog.submitting = true
|
||||
try {
|
||||
const res = await scrapDefective(scrapDialog.row.id, {
|
||||
scrap_qty: qty,
|
||||
reason,
|
||||
})
|
||||
ElMessage.success(res?.msg || '报废成功')
|
||||
scrapDialog.visible = false
|
||||
await fetchData()
|
||||
} catch (e) {
|
||||
console.error('[报废销毁] 失败', e)
|
||||
} finally {
|
||||
scrapDialog.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.dlg-text {
|
||||
color: #303133;
|
||||
}
|
||||
.dlg-strong {
|
||||
color: #E6A23C;
|
||||
font-weight: bold;
|
||||
}
|
||||
.dlg-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.dlg-danger {
|
||||
color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user