问题 ---- 主列表只有「物料数 N 种」(种类数),具体数量藏在展开行的明细表里, 不展开就看不到。 改动 ---- · 移除「类型」列(type_label); · 新增「数量」列,紧跟申请单号,橙色加粗; · 新增 totalQty() 汇总单据总数量。 totalQty 只认 quantity 字段,无需按 type 分支 —— 因为聚合端点已做字段 归一化(报废的 scrap_qty → quantity)。这正是当初做归一化的价值。 数值经 Number(sum.toFixed(4)) 处理,去掉 1.0000 这类无意义的小数尾巴。 顺带移除已无引用的 typeTagType()。
254 lines
9.9 KiB
Vue
254 lines
9.9 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="title">我的申请单</span>
|
||
<span class="subtitle">这里汇总你提交过的全部申请(出库 / 借库 / 报废)</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 筛选 -->
|
||
<div class="filter-container">
|
||
<span class="filter-label">类型:</span>
|
||
<el-radio-group v-model="filterType" @change="handleFilterChange">
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="outbound">出库</el-radio-button>
|
||
<el-radio-button label="borrow">借库</el-radio-button>
|
||
<el-radio-button label="scrap">报废</el-radio-button>
|
||
</el-radio-group>
|
||
|
||
<span class="filter-label" style="margin-left:16px;">状态:</span>
|
||
<el-radio-group v-model="filterStatus" @change="handleFilterChange">
|
||
<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-button :label="4">已撤回</el-radio-button>
|
||
</el-radio-group>
|
||
|
||
<el-button type="primary" :icon="Refresh" :loading="loading" @click="fetchData">刷新</el-button>
|
||
</div>
|
||
|
||
<el-table :data="list" v-loading="loading" border stripe style="width:100%; margin-top:16px">
|
||
<el-table-column type="expand">
|
||
<template #default="{ row }">
|
||
<div class="expand-box">
|
||
<p class="expand-title">物料明细(共 {{ row.items?.length || 0 }} 项)</p>
|
||
<el-table :data="row.items || []" border size="small" style="width:100%">
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="spec_model" label="规格型号" min-width="130" show-overflow-tooltip />
|
||
<el-table-column prop="sku" label="SKU" width="130" show-overflow-tooltip />
|
||
<el-table-column prop="warehouse_location" label="库位" width="120" show-overflow-tooltip />
|
||
<!-- ★ 数量字段已由后端归一化(报废的 scrap_qty → quantity) -->
|
||
<el-table-column prop="quantity" label="数量" width="90" align="right" />
|
||
</el-table>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="request_no" label="申请单号" min-width="190" show-overflow-tooltip />
|
||
|
||
<!-- ★ 数量前置并直接展示:原先只显示「N 种」(种类数),具体数量
|
||
藏在展开行的明细表里,主列表看不到。 -->
|
||
<el-table-column label="数量" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<span style="color: #E6A23C; font-weight: bold; font-size: 15px;">
|
||
{{ totalQty(row) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="物料数" width="80" align="center">
|
||
<template #default="{ row }">{{ row.items?.length || 0 }} 种</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="remark" label="申请原因" min-width="150" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="驳回原因" min-width="120" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.status === 2" style="color:#F56C6C;">{{ row.reject_reason || '-' }}</span>
|
||
<span v-else style="color:#c0c4cc;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="提交时间" width="160" align="center">
|
||
<template #default="{ row }">
|
||
<span>{{ (row.created_at || '').substring(0, 16) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<!-- ★ 仅「待审批(0)」与「已通过(1)」可撤回 —— 两者都已预占库存但尚未执行 -->
|
||
<el-button
|
||
v-if="row.status === 0 || row.status === 1"
|
||
type="warning" link size="small"
|
||
:loading="row._withdrawing"
|
||
@click="handleWithdraw(row)"
|
||
>撤回</el-button>
|
||
<span v-else style="color:#c0c4cc;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-empty v-if="!loading && list.length === 0" description="暂无申请单" :image-size="70" />
|
||
|
||
<el-pagination
|
||
v-if="total > 0"
|
||
background
|
||
style="margin-top:16px; justify-content:flex-end; display:flex;"
|
||
layout="total, prev, pager, next"
|
||
:total="total"
|
||
:page-size="pageSize"
|
||
:current-page="page"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Refresh } from '@element-plus/icons-vue'
|
||
import { getMyRequests } from '@/api/outbound'
|
||
import request from '@/utils/request'
|
||
|
||
// ============================================================================
|
||
// 我的申请单(跨模块聚合)
|
||
//
|
||
// 数据来自专用聚合端点 /my-requests:服务端硬编码 applicant_id = 当前用户,
|
||
// 因此本页对任何登录用户可用,且从设计上不可能看到他人单据。
|
||
//
|
||
// 撤回是**写操作**,保留在各模块自己的端点(三者释放逻辑不同),
|
||
// 后端在每条记录上回传 withdraw_endpoint,前端按此分发 —— 无需在此
|
||
// 硬编码三个模块的 URL 映射。
|
||
// ============================================================================
|
||
|
||
const list = ref<any[]>([])
|
||
const loading = ref(false)
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const pageSize = ref(20)
|
||
const filterType = ref<'all' | 'outbound' | 'borrow' | 'scrap'>('all')
|
||
const filterStatus = ref<number | ''>('')
|
||
|
||
const statusText = (s: number) => {
|
||
const map: Record<number, string> = {
|
||
0: '待审批', 1: '已通过', 2: '已驳回', 3: '已完成', 4: '已撤回',
|
||
}
|
||
return map[s] ?? '-'
|
||
}
|
||
// 状态配色:
|
||
// 0 待审批 → 蓝(primary,待处理)
|
||
// 1 已通过 → 绿(success,正向结果)
|
||
// 2 已驳回 → 红(danger)
|
||
// 3 已完成 → 灰(info,终态、不再需要关注)
|
||
// 4 已撤回 → 黄(warning,需留意但非错误)
|
||
const statusTagType = (s: number) => {
|
||
const map: Record<number, string> = {
|
||
0: 'primary', 1: 'success', 2: 'danger', 3: 'info', 4: 'warning',
|
||
}
|
||
return map[s] ?? 'info'
|
||
}
|
||
// 单据的总数量 = 各明细数量之和
|
||
//
|
||
// 数量字段已由后端归一化(报废的 scrap_qty → quantity),此处只认 quantity,
|
||
// 无需按 type 分支处理 —— 这正是聚合端点做字段归一化的意义。
|
||
const totalQty = (row: any): string => {
|
||
const items = row?.items || []
|
||
if (!items.length) return '-'
|
||
const sum = items.reduce((acc: number, it: any) => acc + (Number(it.quantity) || 0), 0)
|
||
// 去掉无意义的小数尾巴(1.0000 → 1)
|
||
return String(Number(sum.toFixed(4)))
|
||
}
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const res: any = await getMyRequests({
|
||
page: page.value,
|
||
limit: pageSize.value,
|
||
type: filterType.value,
|
||
status: filterStatus.value,
|
||
})
|
||
const items = res?.data?.items || []
|
||
list.value = items.map((r: any) => ({ ...r, _withdrawing: false }))
|
||
total.value = res?.data?.total || 0
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.msg || '加载我的申请单失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleFilterChange = () => {
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
const handlePageChange = (p: number) => {
|
||
page.value = p
|
||
fetchData()
|
||
}
|
||
|
||
const handleWithdraw = async (row: any) => {
|
||
if (!row.withdraw_endpoint) {
|
||
return ElMessage.error('该类型暂不支持撤回')
|
||
}
|
||
const itemCount = row.items?.length || 0
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定撤回【${row.type_label}】${row.request_no} 吗?\n\n` +
|
||
`撤回后该单将作废,其预占的 ${itemCount} 项物料库存会立即释放,` +
|
||
`可供其它申请使用。\n此操作不可恢复。`,
|
||
'⚠️ 撤回确认',
|
||
{ confirmButtonText: '确认撤回', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch (e) {
|
||
return
|
||
}
|
||
|
||
row._withdrawing = true
|
||
try {
|
||
// ★ 按后端回传的端点分发,前端不硬编码三模块 URL 映射
|
||
const res: any = await request({ url: row.withdraw_endpoint, method: 'post' })
|
||
ElMessage.success(res?.msg || `申请单 ${row.request_no} 已撤回`)
|
||
await fetchData()
|
||
} catch (err: any) {
|
||
ElMessage.error(err?.msg || err?.response?.data?.msg || '撤回失败')
|
||
} finally {
|
||
row._withdrawing = false
|
||
}
|
||
}
|
||
|
||
onMounted(fetchData)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.card-header { display: flex; align-items: baseline; gap: 10px; }
|
||
.title { font-size: 17px; font-weight: bold; }
|
||
.subtitle { font-size: 12px; color: #909399; }
|
||
|
||
.filter-container {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.filter-label { font-size: 14px; color: #606266; font-weight: 500; }
|
||
|
||
.expand-box { padding: 12px 24px; background: #f5f7fa; }
|
||
.expand-title { margin: 0 0 10px 0; font-weight: bold; font-size: 13px; color: #606266; }
|
||
</style>
|