按钮级权限(与后端 @permission_required 用同一批权限码):
- 出库记录页「退回」按钮 -> v-permission="'outbound_return'"
- 不良品看板「修复回库」 -> v-permission="'defective_restock'"
- 不良品看板「报废销毁」 -> v-permission="'defective_scrap'"
两个处置按钮是**独立权限**,可能有人只有其一,故分别判定、分别隐藏;
两者都无时显示「无处置权限」。均保留响应式的 v-if 兜底——v-permission
只在 mounted 执行一次,而 Element Plus 表格行会重渲染,两者用同一权限码
判定一致。hasPermission() 内部已对 SUPER_ADMIN 放行,与后端装饰器的
超管旁路对齐。
另外去掉了出库记录页原本套用的 username === 'IRIS' 特例放行:退回是实物
交接的 SOP 动作,后端不认这个特例,前端也不该认,否则会出现「按钮可见但
接口 403」的错位。
数量显示格式化(新增 utils/format.ts,该文件原本是 0 字节空文件):
- formatQty() 显示用:1.0000 -> '1',2.5000 -> '2.5'
- normalizeQty() 数值用:同上但返回 number,供 el-input-number 的
v-model / :max —— 喂字符串会让步进与边界比较退化
应用于 不良品看板(退回总数、剩余待处理、两个弹窗的在管数量与输入框)、
出库记录页(已退 N 提示、退回弹窗三项数字)、退回记录页(退回数量列)。
同时移除三个 el-input-number 上的 :precision="4" —— 它会把 1 强制渲染成
1.0000,是截图上多余小数点的直接来源。数量本身仍支持小数(后端按
numeric(19,4) 收),只是不再强制补零。
表格列原本不受影响(接口返回 JSON 数字,1.0 经 JS 解析即 1),但后端有一批
运算是 float 累加,可能吐出 0.30000000000000004 这类尾巴,格式化按
numeric(19,4) 量纲先收敛到 4 位再做归一。
631 lines
22 KiB
Vue
631 lines
22 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- ★ 高级搜索:与借还记录 / 报废记录统一版式 -->
|
||
<el-form :inline="true" class="filter-form" @submit.prevent>
|
||
<el-form-item>
|
||
<CompanySelector v-model="listQuery.company" @change="fetchData" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="搜索">
|
||
<el-input
|
||
v-model="listQuery.keyword"
|
||
placeholder="输入关键词"
|
||
style="width: 240px;"
|
||
clearable
|
||
@input="debouncedSearch"
|
||
@clear="handleClearSearch"
|
||
>
|
||
<template #prepend>
|
||
<el-select v-model="listQuery.search_type" placeholder="搜索类型" style="width: 110px" @change="handleSearchImmediate">
|
||
<el-option label="全部" value="all" />
|
||
<el-option label="单号" value="no" />
|
||
<el-option label="姓名" value="name" />
|
||
<el-option label="SKU" value="sku" />
|
||
<el-option label="物料名称" value="material_name" />
|
||
<el-option label="规格型号" value="spec_model" />
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="出库日期">
|
||
<el-date-picker
|
||
v-model="listQuery.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="fetchData">查询</el-button>
|
||
<el-button @click="resetFilter">重置</el-button>
|
||
|
||
<!-- ★ 高级筛选:对齐 material/list.vue 既有模式 -->
|
||
<el-popover
|
||
v-model:visible="advancedFilterVisible"
|
||
placement="bottom"
|
||
title="高级筛选"
|
||
width="600"
|
||
trigger="manual"
|
||
>
|
||
<template #reference>
|
||
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">
|
||
高级筛选
|
||
<el-badge v-if="appliedConditions.length" :value="appliedConditions.length" style="margin-left:6px" />
|
||
</el-button>
|
||
</template>
|
||
<div class="advanced-filter">
|
||
<div
|
||
v-for="(condition, index) in advancedConditions"
|
||
:key="index"
|
||
class="condition-row"
|
||
>
|
||
<el-select v-model="condition.field" placeholder="字段" style="width:180px" :teleported="false">
|
||
<el-option v-for="f in fieldOptions" :key="f.value" :label="f.label" :value="f.value" />
|
||
</el-select>
|
||
<el-select v-model="condition.operator" placeholder="操作符" style="width:120px; margin-left:8px" :teleported="false">
|
||
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
|
||
</el-select>
|
||
<el-input v-model="condition.value" placeholder="值" style="width:180px; margin-left:8px" />
|
||
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left:8px">删除</el-button>
|
||
</div>
|
||
<div style="margin-top:12px">
|
||
<el-button type="primary" link @click="addCondition">添加条件</el-button>
|
||
<el-button type="primary" @click="applyAdvancedFilter">应用筛选</el-button>
|
||
<el-button @click="resetAdvancedFilter">重置</el-button>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" @click="$router.push('/outbound/create')">新建出库</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table
|
||
:data="list"
|
||
v-loading="loading"
|
||
border
|
||
style="width: 100%; margin-top: 20px;"
|
||
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||
>
|
||
<el-table-column type="expand">
|
||
<template #default="props">
|
||
<div style="padding: 10px 40px; background: #fafafa;">
|
||
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
|
||
<el-table :data="props.row.items" border size="small">
|
||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="150" />
|
||
|
||
<el-table-column v-if="hasColumnPermission('name')" prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||
<el-table-column v-if="hasColumnPermission('material_type')" prop="material_type" label="类型" width="120" show-overflow-tooltip />
|
||
<el-table-column v-if="hasColumnPermission('category')" prop="category" label="类别" width="120" show-overflow-tooltip />
|
||
<el-table-column v-if="hasColumnPermission('spec_model')" prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="batch_sn" label="批号/SN" min-width="120" />
|
||
<el-table-column prop="warehouse_location" label="库位" min-width="120">
|
||
<template #default="{row}">
|
||
<span v-if="row.warehouse_location">{{ row.warehouse_location }}</span>
|
||
<span v-else style="color:#ccc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('quantity')" prop="quantity" label="数量" width="100" />
|
||
<el-table-column v-if="hasColumnPermission('unit_price')" prop="unit_price" label="单价" width="120">
|
||
<template #default="{row}">¥{{ row.unit_price }}</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="hasColumnPermission('subtotal')" prop="subtotal" label="小计">
|
||
<template #default="{row}">
|
||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<!-- ★ 原单退回:良品加回库存 / 不良品转入异常待处理台账。
|
||
已退满的行(returnable_quantity <= 0)按钮置灰不可点。
|
||
|
||
★ 权限双重保障,两者用同一个权限码 outbound_return,判定一致:
|
||
· v-if="canReturn" 响应式判定。v-permission 指令只在 mounted
|
||
执行一次,而表格行会重渲染,用响应式判定兜底更稳。
|
||
· v-permission 本系统规范的按钮级指令,声明式表达权限要求,
|
||
无权限时直接把元素移出 DOM(普通领料员工完全看不到)。 -->
|
||
<el-table-column label="退回" width="150" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<template v-if="canReturn">
|
||
<el-button
|
||
v-permission="'outbound_return'"
|
||
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">
|
||
已退 {{ formatQty(row.returned_quantity) }}
|
||
</span>
|
||
</template>
|
||
<span v-else class="returned-hint">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('outbound_no')" prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
|
||
|
||
<el-table-column v-if="hasColumnPermission('outbound_time')" prop="outbound_time" label="出库时间" width="170" align="center">
|
||
<template #default="{ row }">
|
||
<span>{{ row.outbound_time ? row.outbound_time.substring(0, 16) : '' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('outbound_type')" prop="outbound_type" label="类型" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getTagType(row.outbound_type)">{{ formatType(row.outbound_type) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('total_amount')" prop="total_amount" label="总金额" width="120" align="right">
|
||
<template #default="{ row }">
|
||
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('consumer_name')" prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
|
||
|
||
<el-table-column v-if="hasColumnPermission('operator_name')" prop="operator_name" label="操作员" min-width="100" show-overflow-tooltip />
|
||
|
||
<el-table-column v-if="hasColumnPermission('remark')" prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||
|
||
<el-table-column v-if="hasColumnPermission('signature_path')" label="签名" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<div v-if="row.signature_path" class="signature-cell">
|
||
<el-image
|
||
style="width: 80px; height: 40px; border-radius: 4px; border: 1px solid #dcdfe6;"
|
||
:src="row.signature_path"
|
||
:preview-src-list="[row.signature_path]"
|
||
preview-teleported
|
||
fit="contain"
|
||
hide-on-click-modal
|
||
>
|
||
<template #error>
|
||
<div class="image-slot">
|
||
<el-icon><Picture /></el-icon>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
</div>
|
||
<span v-else style="color: #909399; font-size: 12px;">未签名</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div style="margin-top: 20px; text-align: right;">
|
||
<el-pagination
|
||
background
|
||
layout="prev, pager, next"
|
||
:total="total"
|
||
:page-size="listQuery.limit"
|
||
@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">{{ formatQty(returnDialog.row?.quantity) }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="已退回数量">
|
||
<span class="dialog-text">{{ formatQty(returnDialog.row?.returned_quantity) }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="本次可退最大">
|
||
<span class="dialog-text dialog-strong">
|
||
{{ formatQty(returnDialog.row?.returnable_quantity) }}
|
||
</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="本次退回数量" required>
|
||
<!-- 不再设 :precision="4" —— 那会把 1 显示成 1.0000。
|
||
数量本身仍支持小数(后端按 numeric(19,4) 收)。 -->
|
||
<el-input-number
|
||
v-model="returnDialog.form.return_qty"
|
||
:min="0"
|
||
:max="normalizeQty(returnDialog.row?.returnable_quantity)"
|
||
: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, computed, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getOutboundList } from '@/api/outbound'
|
||
import { returnFromOutbound } from '@/api/inbound/return'
|
||
import { formatQty, normalizeQty } from '@/utils/format'
|
||
import { Picture } from '@element-plus/icons-vue'
|
||
import { useUserStore } from '@/stores/user'
|
||
import CompanySelector from '@/components/CompanySelector.vue'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
// 退回按钮的可见性。
|
||
//
|
||
// ★ 权限码必须与后端 @permission_required('outbound_return') 逐字一致,
|
||
// 且刻意**不**套用本文件 hasColumnPermission() 里 `username === 'IRIS'`
|
||
// 那类特例放行 —— 退回是实物交接的 SOP 动作,后端不认特例,前端也不该认,
|
||
// 否则会出现「按钮可见但接口 403」的错位。
|
||
// hasPermission() 内部已对 SUPER_ADMIN 放行,与后端装饰器的超管旁路对齐。
|
||
// 权限码来源见 db_migrations/add_outbound_return_perm.sql
|
||
const canReturn = computed(() => userStore.hasPermission('outbound_return'))
|
||
|
||
// 防抖定时器
|
||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
// 防抖搜索函数
|
||
const debouncedSearch = () => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
}
|
||
debounceTimer = setTimeout(() => {
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}, 500)
|
||
}
|
||
|
||
// 清空搜索时立即触发查询
|
||
const handleClearSearch = () => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
}
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
// 列与权限Code的映射关系(数据库中的code)
|
||
const permissionMap: Record<string, string | null> = {
|
||
// 主表基础列 — 始终可见
|
||
outbound_no: null,
|
||
outbound_time: null,
|
||
outbound_type: null,
|
||
consumer_name: null,
|
||
operator_name: null,
|
||
remark: null,
|
||
signature_path: null,
|
||
// 金额列 — 需权限
|
||
total_amount: 'outbound_list:total_amount',
|
||
// 明细列 — 始终可见
|
||
sku: null,
|
||
name: null,
|
||
material_type: null,
|
||
category: null,
|
||
spec_model: null,
|
||
quantity: null,
|
||
batch_sn: null,
|
||
// 明细金额 — 需权限
|
||
unit_price: 'outbound_list:unit_price',
|
||
subtotal: 'outbound_list:subtotal',
|
||
}
|
||
|
||
// 检查列权限
|
||
const hasColumnPermission = (prop: string) => {
|
||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||
return true
|
||
}
|
||
if (!(prop in permissionMap)) {
|
||
return false // Default Deny
|
||
}
|
||
const code = permissionMap[prop]
|
||
if (code === null || code === undefined) {
|
||
return true // 基础字段,无需权限
|
||
}
|
||
return userStore.hasPermission(code)
|
||
}
|
||
|
||
const list = ref([])
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
|
||
const listQuery = reactive({
|
||
page: 1,
|
||
limit: 10,
|
||
keyword: '',
|
||
search_type: 'all',
|
||
dateRange: [],
|
||
company: '' as string,
|
||
advancedFilters: [] as any[],
|
||
})
|
||
|
||
// --- ★ 高级筛选 ---
|
||
const advancedFilterVisible = ref(false)
|
||
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
|
||
const appliedConditions = ref<any[]>([])
|
||
const fieldOptions = [
|
||
{ value: 'no', label: '单号' },
|
||
{ value: 'sku', label: 'SKU' },
|
||
{ value: 'material_name', label: '物料名称' },
|
||
{ value: 'operator', label: '操作人' },
|
||
]
|
||
const operatorOptions = [
|
||
{ value: 'contains', label: '包含' },
|
||
{ value: 'eq', label: '等于' },
|
||
{ value: 'not_contains', label: '不包含' },
|
||
{ value: 'ne', label: '不等于' },
|
||
]
|
||
|
||
const addCondition = () => {
|
||
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
||
}
|
||
const removeCondition = (index: number) => {
|
||
advancedConditions.value.splice(index, 1)
|
||
}
|
||
const applyAdvancedFilter = () => {
|
||
const valid = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
|
||
listQuery.advancedFilters = valid
|
||
appliedConditions.value = valid
|
||
advancedFilterVisible.value = false
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
const resetAdvancedFilter = () => {
|
||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||
listQuery.advancedFilters = []
|
||
appliedConditions.value = []
|
||
advancedFilterVisible.value = false
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
...listQuery,
|
||
start_date: listQuery.dateRange && listQuery.dateRange[0] ? listQuery.dateRange[0] : null,
|
||
end_date: listQuery.dateRange && listQuery.dateRange[1] ? listQuery.dateRange[1] : null,
|
||
// ★ 高级筛选:后端约定参数名为 advancedFilters,值为 JSON 字符串
|
||
advancedFilters: JSON.stringify(listQuery.advancedFilters || []),
|
||
}
|
||
|
||
const res = await getOutboundList(params)
|
||
list.value = res.data.items || []
|
||
total.value = res.data.total || 0
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handlePageChange = (val: number) => {
|
||
listQuery.page = val
|
||
fetchData()
|
||
}
|
||
|
||
// 立即查询(搜索类型切换用,取消防抖)
|
||
const handleSearchImmediate = () => {
|
||
if (debounceTimer) clearTimeout(debounceTimer)
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
const resetFilter = () => {
|
||
if (debounceTimer) clearTimeout(debounceTimer)
|
||
listQuery.keyword = ''
|
||
listQuery.search_type = 'all'
|
||
listQuery.dateRange = []
|
||
listQuery.advancedFilters = []
|
||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||
appliedConditions.value = []
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
const formatType = (type: string) => {
|
||
const map: any = {
|
||
'SALES': '销售出库',
|
||
'USE': '内部领用',
|
||
'PRODUCTION': '生产出库',
|
||
'SCRAP': '报废',
|
||
'LOSS': '盘亏出库', // ★ 补充映射:扫码出库下拉中存在「盘亏出库 LOSS」,避免出库记录显示英文
|
||
'REPAIR': '维修出库'
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
const getTagType = (type: string) => {
|
||
const map: any = {
|
||
'SALES': 'success',
|
||
'USE': 'warning',
|
||
'PRODUCTION': 'info',
|
||
'SCRAP': 'danger',
|
||
'LOSS': 'warning', // ★ 补充:盘亏出库标签样式
|
||
'REPAIR': 'info'
|
||
}
|
||
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
|
||
// 默认带入「本次可退最大」,业务上整行退回最常见;用户可再调小做部分退回
|
||
// 用 normalizeQty 抹掉后端 float 累加留下的尾数,避免输入框显示一长串小数
|
||
returnDialog.form.return_qty = normalizeQty(row?.returnable_quantity)
|
||
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 = normalizeQty(returnDialog.row?.returnable_quantity)
|
||
const qty = normalizeQty(returnDialog.form.return_qty)
|
||
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()
|
||
})
|
||
|
||
// 组件销毁前清理定时器,防止内存泄漏
|
||
onBeforeUnmount(() => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
debounceTimer = null
|
||
}
|
||
})
|
||
</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;
|
||
}
|
||
|
||
/* 高级筛选条件行 */
|
||
.condition-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.signature-cell {
|
||
display: flex;
|
||
justify-content: center;
|
||
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;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #f5f7fa;
|
||
color: #909399;
|
||
}
|
||
</style>
|