feat(scrap): 前端报废申请 / 报废审批页 + API(对齐出库版式)
- api/scrap.ts 新增 submitScrapRequest/checkScrapApproval/getScrapApprovals/approveScrapRequest/executeScrapByRequest - apply: 选库存(带 source_table+stock_id)/报废数量/原因/按需审批人提交 - approval: 顶部审批状态筛选+刷新、展开明细、列名对齐出库、分页含 sizes、按单执行弹窗 - 路由挂载 /scrap/apply、/scrap/approval
This commit is contained in:
@ -26,3 +26,78 @@ export function getScrapRecords(params: any) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 报废申请/审批/执行(新流程,镜像出/借库)
|
||||
// ==========================================
|
||||
|
||||
// 4. 提交报废申请
|
||||
export function submitScrapRequest(data: {
|
||||
items: Array<{
|
||||
source_table: string
|
||||
stock_id: number
|
||||
scrap_qty: number
|
||||
name?: string
|
||||
spec_model?: string
|
||||
sku?: string
|
||||
location?: string
|
||||
batch_number?: string
|
||||
}>
|
||||
remark?: string
|
||||
approver_id?: number | null
|
||||
allowed_approvers?: Array<{ type: string; value: any }>
|
||||
}) {
|
||||
return request({
|
||||
url: '/v1/scrap/request',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 提交前预检:所选物料是否需要审批
|
||||
export function checkScrapApproval(data: { items: Array<{ name?: string; spec_model?: string; base_id?: number }> }) {
|
||||
return request({
|
||||
url: '/v1/scrap/request/check-approval',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 6. 报废申请列表(scope: mine | pending | executable | all)
|
||||
export function getScrapApprovals(params: {
|
||||
scope?: 'mine' | 'pending' | 'executable' | 'all'
|
||||
status?: number | ''
|
||||
page?: number
|
||||
limit?: number
|
||||
}) {
|
||||
return request({
|
||||
url: '/v1/scrap/request',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 7. 报废申请详情
|
||||
export function getScrapApprovalDetail(id: number) {
|
||||
return request({
|
||||
url: `/v1/scrap/request/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 8. 审批(通过/驳回)
|
||||
export function approveScrapRequest(id: number, data: { action: 'approve' | 'reject'; reject_reason?: string }) {
|
||||
return request({
|
||||
url: `/v1/scrap/request/${id}/approve`,
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 9. 按单报废执行(扣减库存)
|
||||
export function executeScrapByRequest(id: number) {
|
||||
return request({
|
||||
url: `/v1/scrap/request/${id}/execute`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
@ -262,6 +262,18 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'ScrapCreate',
|
||||
component: () => import('@/views/operation/scrap/create.vue'),
|
||||
meta: { title: '新建报废' }
|
||||
},
|
||||
{
|
||||
path: 'apply',
|
||||
name: 'ScrapApply',
|
||||
component: () => import('@/views/operation/scrap/apply/index.vue'),
|
||||
meta: { title: '报废申请' }
|
||||
},
|
||||
{
|
||||
path: 'approval',
|
||||
name: 'ScrapApproval',
|
||||
component: () => import('@/views/operation/scrap/approval/index.vue'),
|
||||
meta: { title: '报废审批' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
214
inventory-web/src/views/operation/scrap/apply/index.vue
Normal file
214
inventory-web/src/views/operation/scrap/apply/index.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="always">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">报废申请</span>
|
||||
<div>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="按名称/规格/SKU 搜索库存"
|
||||
style="width: 260px; margin-right: 10px;"
|
||||
clearable
|
||||
@keyup.enter="loadStock"
|
||||
@clear="loadStock"
|
||||
/>
|
||||
<el-button type="primary" @click="loadStock">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="stockList" border height="480" v-loading="loading" @selection-change="onSelect">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="typeLabel" label="类型" width="90" align="center" />
|
||||
<el-table-column prop="name" label="物料名称" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="规格型号" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.standard || row.spec_model || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="warehouse_location" label="库位" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="批次/序列号" width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.batch_number || row.serial_number || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用库存" width="110" align="right">
|
||||
<template #default="{ row }">{{ Number(row.available_quantity || 0) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top:12px; text-align:right;">
|
||||
<span style="margin-right:12px;color:#909399;">已选 {{ selected.length }} 条</span>
|
||||
<el-button type="primary" :disabled="selected.length===0" @click="openSubmitDialog">提交报废申请</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 提交报废申请 -->
|
||||
<el-dialog v-model="dialogVisible" title="提交报废申请" width="760px" destroy-on-close :close-on-click-modal="false">
|
||||
<el-table :data="cart" border size="small" max-height="300">
|
||||
<el-table-column prop="name" label="物料" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="规格" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.spec_model || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="location" label="库位" width="110" />
|
||||
<el-table-column label="批次" width="120">
|
||||
<template #default="{ row }">{{ row.batch_number || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.available_qty }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本次报废" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.scrap_qty" :min="1" :max="row.available_qty" :precision="0" :controls="false" style="width:100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-form label-width="100px" style="margin-top:14px;">
|
||||
<el-form-item v-if="approvalVisible" label="指定审批人">
|
||||
<div v-if="approvalText" style="color:#E6A23C; font-size:12px; line-height:1.4; margin-bottom:4px;">{{ approvalText }}</div>
|
||||
<el-select v-model="approverId" placeholder="请选择审批人" style="width:100%" filterable>
|
||||
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报废原因" required>
|
||||
<el-input v-model="remark" type="textarea" :rows="2" placeholder="请填写报废原因(必填)" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">确认提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getStockList } from '@/api/inbound/stock'
|
||||
import { getApproversList } from '@/api/auth'
|
||||
import { submitScrapRequest, checkScrapApproval } from '@/api/scrap'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const stockList = ref<any[]>([])
|
||||
const selected = ref<any[]>([])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const cart = ref<any[]>([])
|
||||
const remark = ref('')
|
||||
const approverId = ref<number | null>(null)
|
||||
const approvers = ref<any[]>([])
|
||||
const approvalVisible = ref(false)
|
||||
const approvalText = ref('')
|
||||
|
||||
const isKeeper = () => (userStore.role || '').toUpperCase() === 'WAREHOUSE_MGR'
|
||||
|
||||
const loadStock = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getStockList({ page: 1, pageSize: 200, keyword: keyword.value.trim() })
|
||||
const items = res?.data?.items || res?.data?.list || res?.data || []
|
||||
stockList.value = items
|
||||
} catch (e) {
|
||||
ElMessage.error('加载库存失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
loadStock()
|
||||
|
||||
const onSelect = (rows: any[]) => { selected.value = rows }
|
||||
|
||||
const loadApprovers = async () => {
|
||||
try {
|
||||
const res: any = await getApproversList()
|
||||
approvers.value = res.data || []
|
||||
} catch (e) { approvers.value = [] }
|
||||
}
|
||||
|
||||
const openSubmitDialog = async () => {
|
||||
// 构造购物车(保留 source_table + stock_id 精准定位 + 快照字段)
|
||||
cart.value = selected.value.map((r: any) => ({
|
||||
source_table: r.source_table,
|
||||
stock_id: r.id,
|
||||
base_id: r.base_id,
|
||||
name: r.name || r.material_name || '',
|
||||
spec_model: r.standard || r.spec_model || '',
|
||||
sku: r.sku || '',
|
||||
location: r.warehouse_location || '',
|
||||
batch_number: r.batch_number || r.serial_number || '',
|
||||
available_qty: Number(r.available_quantity || 0),
|
||||
scrap_qty: 1,
|
||||
}))
|
||||
|
||||
remark.value = ''
|
||||
approverId.value = null
|
||||
approvalText.value = ''
|
||||
// 库管代建默认显示审批人
|
||||
approvalVisible.value = isKeeper()
|
||||
if (approvalVisible.value) approvalText.value = '库管代建报废申请需审批,请选择审批人'
|
||||
loadApprovers()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!remark.value.trim()) {
|
||||
return ElMessage.warning('请填写报废原因')
|
||||
}
|
||||
const items = cart.value.map(it => ({
|
||||
source_table: it.source_table,
|
||||
stock_id: it.stock_id,
|
||||
scrap_qty: Number(it.scrap_qty || 0),
|
||||
name: it.name,
|
||||
spec_model: it.spec_model,
|
||||
sku: it.sku,
|
||||
location: it.location,
|
||||
batch_number: it.batch_number,
|
||||
}))
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// ★ 预检:库管代建一律需审批;否则命中需审批物料才需审批
|
||||
const isK = isKeeper()
|
||||
let needApproval = isK
|
||||
let flagged: any[] = []
|
||||
try {
|
||||
const chk: any = await checkScrapApproval({ items })
|
||||
needApproval = needApproval || !!chk?.data?.need_approval
|
||||
flagged = chk?.data?.materials || []
|
||||
} catch (e) { flagged = [] }
|
||||
approvalVisible.value = needApproval
|
||||
approvalText.value = isK && flagged.length === 0
|
||||
? '库管代建报废申请需审批,请选择审批人'
|
||||
: flagged.map((m: any) => `${m.name}(${m.spec_model || '-'})`).join(';')
|
||||
|
||||
if (needApproval && !approverId.value) {
|
||||
return ElMessage.warning(`${approvalText.value || '该申请需审批'}。请先选择审批人`)
|
||||
}
|
||||
|
||||
await submitScrapRequest({
|
||||
items,
|
||||
remark: remark.value.trim(),
|
||||
approver_id: needApproval ? approverId.value : null,
|
||||
})
|
||||
ElMessage.success('报废申请已提交(含需审批物料时将进入审批,否则直接待库管执行)')
|
||||
dialogVisible.value = false
|
||||
selected.value = []
|
||||
loadStock()
|
||||
} catch (err: any) {
|
||||
// 拦截器已按后端 data.msg 弹业务错误,HTTP 错误不重复
|
||||
if (!err?.response) ElMessage.error('网络异常,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.title { font-size: 18px; font-weight: bold; }
|
||||
</style>
|
||||
342
inventory-web/src/views/operation/scrap/approval/index.vue
Normal file
342
inventory-web/src/views/operation/scrap/approval/index.vue
Normal file
@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="filter-container">
|
||||
<span style="font-weight: bold; font-size: 15px; margin-right: 8px;">审批状态:</span>
|
||||
<el-radio-group v-model="filterStatus" size="default" @change="handleStatusChange">
|
||||
<el-radio-button label="">全部</el-radio-button>
|
||||
<el-radio-button :label="0">待审批</el-radio-button>
|
||||
<el-radio-button :label="1">已通过</el-radio-button>
|
||||
<el-radio-button :label="2">已驳回</el-radio-button>
|
||||
<el-radio-button :label="3">已执行</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" :icon="Refresh" @click="fetchData">刷新</el-button>
|
||||
<el-radio-group v-model="scope" size="default" style="margin-left:12px;" @change="handleStatusChange">
|
||||
<el-radio-button label="pending">待我审批</el-radio-button>
|
||||
<el-radio-button label="executable">待执行</el-radio-button>
|
||||
<el-radio-button label="mine">我提交的</el-radio-button>
|
||||
<el-radio-button label="all">全部单</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
border
|
||||
stripe
|
||||
style="margin-top: 16px;"
|
||||
row-key="id"
|
||||
:expand-row-keys="expandedRows"
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<!-- 展开行:本次报废对应的“选单内容”(冻结库存行) -->
|
||||
<el-table-column type="expand" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="padding: 12px 24px; background: #f5f7fa;">
|
||||
<p style="margin: 0 0 10px 0; font-weight: bold; font-size: 13px; color: #606266;">
|
||||
报废明细(共 {{ row.items?.length || 0 }} 项)
|
||||
</p>
|
||||
<el-table v-if="row.items?.length" :data="row.items" border size="small" style="width: 100%;">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="来源" width="90" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<el-tag size="small">{{ sourceLabel(item.source_table) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="location" label="库位" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="batch_number" label="批次/序列号" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="报废数量" width="100" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<span style="color: #F56C6C; font-weight: bold;">{{ item.scrap_qty ?? '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无报废明细" :image-size="60" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="request_no" label="申请单号" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="toggleExpand(row)">
|
||||
{{ row.request_no }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="申请人" width="140">
|
||||
<template #default="{ row }">{{ row.applicant_name || getApplicantName(row.applicant_id) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remark" label="申请原因" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="报废种类" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info">{{ row.items?.length || 0 }} 项</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="申请时间" width="170" />
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="审批信息" width="180">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 1 || row.status === 3">
|
||||
<span style="color: #67C23A;">{{ row.actual_approver_name || getApproverName(row.actual_approver_id) }}</span>
|
||||
<br />
|
||||
<span style="font-size: 12px; color: #909399;">{{ row.approved_at || '' }}</span>
|
||||
</template>
|
||||
<template v-else-if="row.status === 2">
|
||||
<span style="color: #F56C6C;">已驳回</span>
|
||||
<el-tooltip v-if="row.reject_reason" :content="row.reject_reason" placement="top">
|
||||
<el-icon style="margin-left: 4px; cursor: pointer;"><Warning /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span v-else style="color: #c0c4cc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 0">
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('scrap_approval:operation') || userStore.role === 'SUPER_ADMIN'"
|
||||
type="success" size="small" :loading="row._approving" @click="handleApprove(row)"
|
||||
>通过</el-button>
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('scrap_approval:operation') || userStore.role === 'SUPER_ADMIN'"
|
||||
type="danger" size="small" :loading="row._approving" @click="openRejectDialog(row)"
|
||||
>驳回</el-button>
|
||||
</template>
|
||||
<!-- ★ 已通过(待执行):持有 scrap_execute 者可执行按单报废 -->
|
||||
<template v-else-if="row.status === 1">
|
||||
<el-button
|
||||
v-if="canExecute"
|
||||
type="warning" plain size="small" :loading="row._executing" @click="openExecute(row)"
|
||||
>执行报废</el-button>
|
||||
</template>
|
||||
<span v-else style="color: #c0c4cc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 16px; justify-content: flex-end; display: flex;"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<!-- 驳回原因 Dialog -->
|
||||
<el-dialog v-model="rejectDialogVisible" title="驳回报废申请" width="480px" destroy-on-close>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="申请单号">
|
||||
<span style="font-weight: bold; color: #409EFF;">{{ currentRejectRow?.request_no }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="驳回原因" required>
|
||||
<el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请填写驳回原因" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="rejectDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="rejectLoading" @click="confirmReject">确认驳回</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 按单报废执行 Dialog(严格按选单内容扣减) -->
|
||||
<el-dialog v-model="executeVisible" title="按单报废(确认扣减库存)" width="760px" destroy-on-close>
|
||||
<el-alert
|
||||
type="warning" :closable="false" show-icon
|
||||
title="确认后将严格按下列“选单明细”扣减对应库存行的可用数量并写入报废流水,不可撤销。"
|
||||
style="margin-bottom:12px;"
|
||||
/>
|
||||
<el-table :data="execItems" border size="small" max-height="340">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="来源" width="90" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<el-tag size="small">{{ sourceLabel(item.source_table) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="location" label="库位" width="110" />
|
||||
<el-table-column prop="batch_number" label="批次/序列号" width="130" />
|
||||
<el-table-column label="报废数量" width="100" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<span style="color:#F56C6C;font-weight:bold;">{{ item.scrap_qty }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="executeVisible = false">取消</el-button>
|
||||
<el-button type="warning" :loading="executing" @click="confirmExecute">确认执行报废</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Refresh, Warning } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getScrapApprovals, approveScrapRequest, executeScrapByRequest } from '@/api/scrap'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// --- 状态 ---
|
||||
const list = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filterStatus = ref<number | ''>(0) // 默认筛选待审批
|
||||
const scope = ref<'pending' | 'executable' | 'mine' | 'all'>('mine')
|
||||
const expandedRows = ref<string[]>([])
|
||||
|
||||
// 驳回 Dialog
|
||||
const rejectDialogVisible = ref(false)
|
||||
const currentRejectRow = ref<any>(null)
|
||||
const rejectReason = ref('')
|
||||
const rejectLoading = ref(false)
|
||||
|
||||
// 执行 Dialog
|
||||
const executeVisible = ref(false)
|
||||
const executeRow = ref<any>(null)
|
||||
const execItems = ref<any[]>([])
|
||||
const executing = ref(false)
|
||||
|
||||
const userNameCache = ref<Record<number, string>>({})
|
||||
|
||||
// --- 权限 ---
|
||||
const canExecute = computed(() =>
|
||||
userStore.role === 'SUPER_ADMIN' || userStore.hasPermission('scrap_execute')
|
||||
)
|
||||
|
||||
// --- 工具 ---
|
||||
const statusText = (status: number) => {
|
||||
const map: Record<number, string> = { 0: '待审批', 1: '已通过', 2: '已驳回', 3: '已执行' }
|
||||
return map[status] ?? '-'
|
||||
}
|
||||
const statusTagType = (status: number) => {
|
||||
const map: Record<number, string> = { 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' }
|
||||
return map[status] ?? 'info'
|
||||
}
|
||||
const sourceLabel = (st: string) => ({ stock_buy: '采购件', stock_semi: '半成品', stock_product: '成品' } as any)[st] || st || '-'
|
||||
const getApplicantName = (id: number | null) => (id ? (userNameCache.value[id] ?? `用户 #${id}`) : '-')
|
||||
const getApproverName = (id: number | null) => (id ? (userNameCache.value[id] ?? `用户 #${id}`) : '-')
|
||||
|
||||
// --- 展开行 ---
|
||||
const toggleExpand = (row: any) => {
|
||||
const idx = expandedRows.value.indexOf(row.id)
|
||||
if (idx > -1) expandedRows.value.splice(idx, 1)
|
||||
else expandedRows.value.push(row.id)
|
||||
}
|
||||
const handleExpandChange = () => { /* 由 expandedRows 控制 */ }
|
||||
|
||||
// --- 数据获取 ---
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = { page: page.value, limit: pageSize.value, scope: scope.value }
|
||||
if (filterStatus.value !== '') params.status = filterStatus.value
|
||||
const res: any = await getScrapApprovals(params)
|
||||
const records = res.data?.items || []
|
||||
records.forEach((r: any) => {
|
||||
if (r.applicant_id && r.applicant_name) userNameCache.value[r.applicant_id] = r.applicant_name
|
||||
if (r.actual_approver_id && r.actual_approver_name) userNameCache.value[r.actual_approver_id] = r.actual_approver_name
|
||||
r._approving = false
|
||||
r._executing = false
|
||||
})
|
||||
list.value = records
|
||||
total.value = res.data?.total || records.length || 0
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.msg || '加载报废审批列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- 筛选 / 分页 ---
|
||||
const handleStatusChange = () => { page.value = 1; expandedRows.value = []; fetchData() }
|
||||
const handlePageChange = (p: number) => { page.value = p; fetchData() }
|
||||
const handleSizeChange = (s: number) => { pageSize.value = s; page.value = 1; fetchData() }
|
||||
|
||||
// --- 审批操作 ---
|
||||
const handleApprove = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要通过报废申请单 【${row.request_no}】 吗?`, '审批确认',
|
||||
{ confirmButtonText: '确定通过', cancelButtonText: '取消', type: 'info' })
|
||||
} catch { return }
|
||||
row._approving = true
|
||||
try {
|
||||
await approveScrapRequest(row.id, { action: 'approve' })
|
||||
ElMessage.success(`申请单 ${row.request_no} 已通过`)
|
||||
await fetchData()
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.msg || '审批操作失败')
|
||||
} finally { row._approving = false }
|
||||
}
|
||||
|
||||
const openRejectDialog = (row: any) => {
|
||||
currentRejectRow.value = row
|
||||
rejectReason.value = ''
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
const confirmReject = async () => {
|
||||
const reason = rejectReason.value.trim()
|
||||
if (!reason) return ElMessage.warning('请填写驳回原因')
|
||||
rejectLoading.value = true
|
||||
try {
|
||||
await approveScrapRequest(currentRejectRow.value.id, { action: 'reject', reject_reason: reason })
|
||||
ElMessage.success(`申请单 ${currentRejectRow.value.request_no} 已驳回`)
|
||||
rejectDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.msg || '驳回操作失败')
|
||||
} finally { rejectLoading.value = false }
|
||||
}
|
||||
|
||||
// --- 按单报废执行(严格按选单明细) ---
|
||||
const openExecute = (row: any) => {
|
||||
executeRow.value = row
|
||||
execItems.value = row.items || []
|
||||
executeVisible.value = true
|
||||
}
|
||||
const confirmExecute = async () => {
|
||||
if (!executeRow.value) return
|
||||
executing.value = true
|
||||
try {
|
||||
await executeScrapByRequest(executeRow.value.id)
|
||||
ElMessage.success('已执行报废并扣减库存')
|
||||
executeVisible.value = false
|
||||
await fetchData()
|
||||
} catch (err: any) {
|
||||
// 拦截器已弹后端业务错误;无响应才算网络异常
|
||||
if (!err?.response) ElMessage.error('网络异常,请重试')
|
||||
} finally { executing.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchData() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container { padding: 20px; }
|
||||
.filter-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user