20260706001

This commit is contained in:
xiaojibeier
2026-07-06 12:46:54 +08:00
parent 67d1da71a8
commit fcf6aa2df6
10 changed files with 938 additions and 38 deletions
+23 -6
View File
@@ -5,6 +5,7 @@ import { loadGoodsInScenes, sceneValuesObjectFromRaw } from '@/utils/rfSceneGood
import {
pickGoodsCategoryEntry,
pickGoodsDisplayName,
pickGoodsNameFromListedRow,
resolveGoodsCategoryCode,
} from '@/utils/goodsCategorys'
@@ -571,7 +572,15 @@ function parseStableSupply(v) {
/** @param {unknown} row */
export function normalizeCollectionGoodsRow(row) {
if (!row || typeof row !== 'object') return null
const r = row
const embedded = row.raw && typeof row.raw === 'object' ? row.raw : null
const r = embedded
? {
...embedded,
...row,
goods_categorys: row.goods_categorys ?? embedded.goods_categorys,
goods: row.goods ?? embedded.goods,
}
: row
const nested = r.goods && typeof r.goods === 'object' ? r.goods : null
const goods_code = String(
r.goods_code
@@ -600,7 +609,8 @@ export function normalizeCollectionGoodsRow(row) {
).trim()
const name = pickGoodsDisplayName(r, category_code, { allowTopLevelName: true })
|| String(
r.goods_name
r.name
|| r.goods_name
|| r.goodsName
|| r.product_name
|| r.material_name
@@ -671,15 +681,22 @@ export function pickCollectionGoodsPreferredTax(row) {
export function pickCollectionGoodsPickFields(row) {
const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null)
if (!normalized) return null
const raw = normalized.raw || row
const sourceRow = normalized.raw || row
const goods_code = String(normalized.goods_code || '').trim()
if (!goods_code) return null
const payRaw = raw?.payment_price ?? ''
const payRaw = sourceRow?.payment_price ?? row?.payment_price ?? ''
const payment_price = payRaw !== '' && payRaw != null ? Number(payRaw) : null
const bill_tax = pickCollectionGoodsPreferredTax(row)
const bill_tax = pickCollectionGoodsPreferredTax(sourceRow)
const nameCandidates = [
String(row?.name || '').trim(),
String(normalized.name || '').trim(),
pickGoodsNameFromListedRow(sourceRow),
pickGoodsNameFromListedRow(row),
].filter(Boolean)
const name = nameCandidates.find((n) => n !== goods_code) || nameCandidates[0] || goods_code
return {
goods_code,
name: String(normalized.name || goods_code).trim(),
name,
payment_price: Number.isFinite(payment_price) ? payment_price : null,
bill_tax,
raw: normalized,
+17
View File
@@ -186,6 +186,23 @@ export async function allocAttrListed(body = {}) {
}
}
/** POST /api/warehouse/alloc/attr/search — 按商品/RDC/FDC 检索配货属性编码 */
export async function allocAttrSearch(body = {}) {
const payload = buildListedPayload({
goods_code: str(body.goods_code) || undefined,
rdc_warehouse_code: str(body.rdc_warehouse_code) || undefined,
fdc_warehouse_code: str(body.fdc_warehouse_code) || undefined,
page: body.page,
limit: body.limit,
})
const res = await request.post('/api/warehouse/alloc/attr/search', payload)
const { list, total } = unwrapListed(res)
return {
list: list.map(normalizeAllocAttrRow).filter(Boolean),
total,
}
}
/** POST /api/warehouse/alloc/logs/listed */
export async function allocLogsListed(body = {}) {
const res = await request.post('/api/warehouse/alloc/logs/listed', buildListedPayload(body))
+78 -1
View File
@@ -198,6 +198,83 @@ export async function wasteLogsAdd(body) {
return res?.data ?? res
}
function normalizeWasteShareRow(raw) {
if (!raw || typeof raw !== 'object') return null
const wh = pickRdcFdcPair(raw)
const goods = pickGoodsFields(raw)
const goods_code = goods.goods_code
if (!goods_code && !wh.rdc_warehouse_code) return null
const waste_count = num(
raw.waste_count
?? raw.rdc_waste_count
?? raw.rdc_waste
?? raw.waste_num,
)
const rdc_in_count = num(
raw.rdc_in_count
?? raw.total_rdc_in
?? raw.rdc_inbound_count
?? raw.in_count,
)
const share_total_amount = num(
raw.total_amount
?? raw.share_total_amount
?? raw.share_amount,
)
return {
...raw,
...wh,
...goods,
goods_display_name: pickGoodsNameFromListedRow(raw) || goods.goods_name,
waste_count,
rdc_in_count,
ratio: num(raw.ratio ?? raw.share_ratio ?? raw.waste_ratio),
pre_price: num(raw.pre_price ?? raw.price ?? raw.cost_price),
unit_amount: num(raw.unit_amount ?? raw.share_unit_amount ?? raw.unit_share_amount),
share_total_amount,
share_time: str(raw.share_time ?? raw.amortize_time ?? raw.ctime ?? raw.create_time),
day_id: raw.day_id != null && raw.day_id !== '' ? Number(raw.day_id) : undefined,
}
}
/** POST /api/warehouse/waste/share/listed */
export async function wasteShareListed(body = {}) {
const payload = buildListedPayload({
goods_code: str(body.goods_code) || undefined,
rdc_warehouse_code: str(body.rdc_warehouse_code) || undefined,
day_id: body.day_id != null && body.day_id !== '' ? Number(body.day_id) : undefined,
page: body.page,
limit: body.limit,
})
const res = await request.post('/api/warehouse/waste/share/listed', payload)
const { list, total } = unwrapListed(res)
return {
list: list.map(normalizeWasteShareRow).filter(Boolean),
total,
}
}
/** POST /api/warehouse/waste/amortize */
export async function wasteAmortize(body) {
const payload = {
start_time: str(body?.start_time),
end_time: str(body?.end_time),
}
if (!payload.start_time || !payload.end_time) throw new Error('请选择分摊时间段')
const res = await request.post('/api/warehouse/waste/amortize', payload, { skipErrorNotify: true })
return res?.data ?? res
}
export function isBenignWasteAmortizeError(err) {
const msg = pickApiErrorMessage(err, '')
return !msg || /没有|已分摊|无需分摊|无可分摊|暂无|为空/.test(msg)
}
export function pickApiErrorMessage(err, fallback = '操作失败') {
return String(err?.data?.message || err?.message || fallback).trim() || fallback
return String(
err?.data?.message
?? err?.data?.data?.message
?? err?.message
?? fallback,
).trim() || fallback
}
+2 -2
View File
@@ -1,4 +1,4 @@
{
"buildId": "20260703007",
"generatedAt": "2026-07-03T09:28:11.726Z"
"buildId": "20260706001",
"generatedAt": "2026-07-06T04:43:39.914Z"
}
+40
View File
@@ -47,9 +47,49 @@ export const APP_VERSION = '0.7.029'
/** @type {ChangelogDay[]} */
export const APP_CHANGELOG = [
{
date: '2026-07-06',
items: [
{
icon: 'mdi:cart-outline',
kind: 'fix',
text: '「采购管理 → 新增采购订单」从统筹商品添加至明细后,物料列正确展示「名称(编码)」;修复二次归一化丢失 `goods_categorys` 物料名、明细仅显示编码的问题。',
},
{
icon: 'mdi:package-variant-closed-minus',
kind: 'change',
text: '「仓库管理 → 损耗管理 → 新增记录」配货属性编码旁增加「搜索」按钮,弹窗内按物料编码、RDC/FDC 仓查询 `alloc/attr/search` 后点选;主表单展示编码及「物料 · RDC · FDC」说明。',
},
],
},
{
date: '2026-07-05',
items: [
{
icon: 'mdi:cog-outline',
kind: 'change',
text: '系统工程调整:变更说明按日归档,便于发版对照;仓库管理相关 API 封装与字段归一化逻辑补充,提升与后端联调时的数据一致性。',
},
],
},
{
date: '2026-07-04',
items: [
{
icon: 'mdi:tune-variant',
kind: 'change',
text: '系统交互与展示优化:仓库运维及分摊预估类页面统一「进页跑批 / 手动查询 / 重新跑批」职责划分;列表 loading 与空态提示对齐;数字列间距、除物料外仅展示名称等展示规则在各模块间进一步收敛。',
},
],
},
{
date: '2026-07-03',
items: [
{
icon: 'mdi:package-variant-closed-minus',
kind: 'add',
text: '「仓库管理 → 损耗分摊预估」新增页面(`WasteShareListed.vue`,路由 `/warehouse/waste-share`):进页自动 `waste/amortize` 跑批后拉 `waste/share/listed`;筛选分摊时间段、物料编码、RDC 仓库编码;展示损耗数、RDC 入库、占比、定价成本、分摊总额、单件分摊(`is-cols-share-waste`9 列)。',
},
{
icon: 'mdi:help-circle-outline',
kind: 'change',
+10
View File
@@ -505,6 +505,16 @@ export const asyncRoutes = [
powerKey: 'warehouse',
},
},
{
path: 'waste-share',
name: 'WarehouseWasteShare',
component: () => import('@/views/base/WasteShareListed.vue'),
meta: {
title: '损耗分摊预估',
icon: 'mdi:package-variant-closed-minus',
powerKey: 'warehouse',
},
},
],
},
/*
+7
View File
@@ -81,6 +81,10 @@
.wops-flex-table.is-cols-share-deploy {
--wops-cols: 14fr 11fr 11fr 5fr 5fr 5fr 4fr 5fr 8fr;
}
/* 损耗分摊预估(9 列) */
.wops-flex-table.is-cols-share-waste {
--wops-cols: 14fr 11fr 5fr 5fr 4fr 5fr 5fr 5fr 8fr;
}
/* 公关费用(10 列):费用编码略窄,集团 / 组织各加半份 */
.wops-flex-table.is-cols-pr-cost {
--wops-cols: 7fr 10.5fr 10.5fr 7fr 8fr 8fr 6fr 9fr 6fr 8fr;
@@ -191,6 +195,7 @@
.wops-flex-table.is-cols-share-pr .wops-flex-row,
.wops-flex-table.is-cols-share-alloc .wops-flex-row,
.wops-flex-table.is-cols-share-deploy .wops-flex-row,
.wops-flex-table.is-cols-share-waste .wops-flex-row,
.wops-flex-table.is-cols-pr-cost .wops-flex-row,
.wops-flex-table.is-cols-collect-whp .wops-flex-row,
.wops-flex-table.is-cols-logs-whp .wops-flex-row,
@@ -211,6 +216,8 @@
.wops-flex-table.is-cols-share-alloc .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-share-deploy .wops-col--num,
.wops-flex-table.is-cols-share-deploy .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-share-waste .wops-col--num,
.wops-flex-table.is-cols-share-waste .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-pr-cost .wops-col--num,
.wops-flex-table.is-cols-pr-cost .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-collect-whp .wops-col--num,
+419
View File
@@ -0,0 +1,419 @@
<template>
<div class="wsl-page">
<header class="wsl-header">
<div class="wsl-header-main">
<h2 class="wsl-title">损耗分摊预估</h2>
<span class="wsl-total">{{ !loading && queried ? `${total}` : '' }}</span>
</div>
</header>
<div class="wsl-filters">
<el-date-picker
v-model="batchRange.start_time"
type="date"
placeholder="分摊开始日期"
value-format="YYYY-MM-DD"
size="small"
clearable
class="wsl-filter-date"
/>
<el-date-picker
v-model="batchRange.end_time"
type="date"
placeholder="分摊结束日期"
value-format="YYYY-MM-DD"
size="small"
clearable
class="wsl-filter-date"
/>
<el-input
v-model="filters.goods_code"
placeholder="物料编码"
clearable
size="small"
class="wsl-filter-item"
@keyup.enter="doSearch"
/>
<el-input
v-model="filters.rdc_warehouse_code"
placeholder="RDC仓库编码"
clearable
size="small"
class="wsl-filter-item"
@keyup.enter="doSearch"
/>
<el-button type="primary" size="small" @click="doSearch">查询</el-button>
<el-button size="small" @click="resetFilters">重置</el-button>
<el-button type="warning" size="small" @click="runBatchAndLoad">重新跑批</el-button>
</div>
<div v-loading="loading" :element-loading-text="loadingText" class="wsl-body">
<div class="wops-flex-table is-cols-share-waste">
<div class="wops-flex-scroll">
<div class="wops-flex-row wops-flex-head">
<div
v-for="col in SHARE_COLUMNS"
:key="col.key"
class="wops-col wsl-head-col"
:class="[colHeadClass(col.key), colHeadColorClass(col.key)]"
>
{{ col.label }}
</div>
</div>
<div v-if="!loading && queried && !rows.length" class="wops-empty-wrap">
<p class="wops-empty">暂无分摊结果</p>
</div>
<div
v-for="(row, idx) in rows"
:key="shareRowKey(row, idx)"
class="wops-flex-row wops-flex-data"
:class="{ 'is-stripe': idx % 2 === 1 }"
>
<WarehouseOpsNameCode
:name="pickShareGoodsName(row)"
:code="row.goods_code"
/>
<div class="wops-col wops-col--wh">
<span class="wops-name wops-ellipsis">{{ row.rdc_warehouse_name || '—' }}</span>
</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.waste_count) }}</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.rdc_in_count) }}</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.ratio, 4) }}</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.pre_price) }}</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.share_total_amount) }}</div>
<div class="wops-col wops-col--num">{{ fmtNum(row.unit_amount) }}</div>
<div class="wops-col wops-col--time">
<span class="wops-ellipsis">{{ formatOpsLogTime(row.share_time || row.ctime) }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="queried && total > 0" class="wsl-footer">
<el-pagination
v-model:current-page="page"
v-model:page-size="limit"
:total="total"
:page-sizes="[20, 50]"
layout="total, sizes, prev, pager, next, jumper"
background
small
@current-change="loadPage"
@size-change="onSizeChange"
/>
</div>
</div>
</template>
<script setup>
import { onActivated, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import WarehouseOpsNameCode from '@/components/warehouse/WarehouseOpsNameCode.vue'
import { formatOpsLogTime } from '@/utils/warehouseOpsDisplay'
import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys'
import {
isBenignWasteAmortizeError,
pickApiErrorMessage,
wasteAmortize,
wasteShareListed,
} from '@/api/warehouseWaste'
defineOptions({ name: 'WasteShareListed' })
const SHARE_COLUMNS = [
{ key: 'goods_name', label: '物料' },
{ key: 'rdc_warehouse_name', label: 'RDC仓库' },
{ key: 'waste_count', label: '损耗数' },
{ key: 'rdc_in_count', label: 'RDC入库' },
{ key: 'ratio', label: '占比' },
{ key: 'pre_price', label: '定价成本' },
{ key: 'share_total_amount', label: '分摊总额' },
{ key: 'unit_amount', label: '单件分摊' },
{ key: 'share_time', label: '分摊时间' },
]
const NUM_KEYS = new Set([
'waste_count',
'rdc_in_count',
'ratio',
'pre_price',
'share_total_amount',
'unit_amount',
])
function colHeadClass(key) {
if (NUM_KEYS.has(key)) return 'wops-col--num'
if (key === 'share_time') return 'wops-col--time'
if (key === 'rdc_warehouse_name') return 'wops-col--wh'
if (key === 'goods_name') return 'wops-col--goods'
return 'wops-col--plain'
}
function colHeadColorClass(key) {
const map = {
goods_name: 'wsl-head-col--goods',
rdc_warehouse_name: 'wsl-head-col--rdc-wh',
waste_count: 'wsl-head-col--waste',
rdc_in_count: 'wsl-head-col--in',
ratio: 'wsl-head-col--ratio',
pre_price: 'wsl-head-col--price',
share_total_amount: 'wsl-head-col--share-total',
unit_amount: 'wsl-head-col--unit-amt',
share_time: 'wsl-head-col--time',
}
return map[key] || 'wsl-head-col--plain'
}
const loading = ref(false)
const loadingText = ref('加载中…')
const queried = ref(false)
const rows = ref([])
const total = ref(0)
const page = ref(1)
const limit = ref(20)
const filters = reactive({
goods_code: '',
rdc_warehouse_code: '',
})
function buildQueryBody() {
return {
page: page.value,
limit: limit.value,
goods_code: String(filters.goods_code || '').trim() || undefined,
rdc_warehouse_code: String(filters.rdc_warehouse_code || '').trim() || undefined,
}
}
function pad2(n) {
return String(n).padStart(2, '0')
}
function fmtDate(d) {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
}
function defaultBatchRange() {
const now = new Date()
const start = new Date(now.getFullYear(), 0, 1)
return { start_time: fmtDate(start), end_time: fmtDate(now) }
}
const batchRange = reactive(defaultBatchRange())
function pickShareGoodsName(row) {
const label = String(row?.goods_display_name ?? '').trim()
if (label) return label
return pickGoodsNameFromListedRow(row) || ''
}
function fmtNum(v, digits = 2) {
if (v == null || v === '') return '—'
const n = Number(v)
return Number.isFinite(n) ? n.toFixed(digits) : String(v)
}
function shareRowKey(row, idx) {
return [
row?.goods_code ?? '',
row?.rdc_warehouse_code ?? '',
row?.share_time ?? row?.ctime ?? '',
row?.day_id ?? '',
idx,
].join(':')
}
async function fetchShareList() {
loadingText.value = '加载分摊结果…'
const res = await wasteShareListed(buildQueryBody())
rows.value = res.list
total.value = res.total
queried.value = true
}
function loadPage() {
if (loading.value) return
search()
}
function onSizeChange() {
page.value = 1
search()
}
async function runBatchAndLoad() {
const start_time = String(batchRange.start_time || '').trim()
const end_time = String(batchRange.end_time || '').trim()
if (!start_time || !end_time) {
ElMessage.warning('请选择分摊时间段(开始日期、结束日期)')
return
}
if (start_time > end_time) {
ElMessage.warning('开始日期不能晚于结束日期')
return
}
page.value = 1
loading.value = true
loadingText.value = '正在跑批中…'
try {
let amortizeHint = ''
try {
await wasteAmortize({ start_time, end_time })
} catch (e) {
if (!isBenignWasteAmortizeError(e)) {
throw e
}
amortizeHint = pickApiErrorMessage(e, '')
}
await fetchShareList()
if (amortizeHint) {
ElMessage.warning(amortizeHint)
} else if (rows.value.length) {
ElMessage.success(`跑批完成,分摊结果 ${total.value}`)
}
} catch (e) {
ElMessage.error(pickApiErrorMessage(e, '跑批失败'))
} finally {
loading.value = false
}
}
async function search() {
loading.value = true
loadingText.value = '加载中…'
try {
await fetchShareList()
} catch (e) {
ElMessage.error(pickApiErrorMessage(e, '查询失败'))
} finally {
loading.value = false
}
}
function doSearch() {
page.value = 1
search()
}
function resetFilters() {
Object.assign(batchRange, defaultBatchRange())
filters.goods_code = ''
filters.rdc_warehouse_code = ''
page.value = 1
search()
}
async function initPage() {
await runBatchAndLoad()
}
onMounted(initPage)
onActivated(initPage)
</script>
<style scoped>
.wsl-page {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 16px;
box-sizing: border-box;
}
.wsl-header {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
margin-bottom: 12px;
}
.wsl-header-main {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 12px;
}
.wsl-title {
font-size: calc(16px + var(--wh-fs-delta, 0px));
font-weight: 600;
color: #303133;
margin: 0;
}
.wsl-total {
font-size: 13px;
color: #909399;
min-width: 60px;
display: inline-block;
}
.wsl-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex-shrink: 0;
margin-bottom: 12px;
}
.wsl-filter-item {
width: 180px;
}
.wsl-filter-date {
width: 150px;
}
.wsl-body {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wsl-body :deep(.wops-flex-head) {
border-bottom: none;
align-items: flex-end;
padding-bottom: 0;
}
.wsl-body :deep(.wsl-head-col) {
border-bottom: 2px solid;
padding-bottom: 7px;
box-sizing: border-box;
}
.wsl-body :deep(.wsl-head-col--goods) { border-bottom-color: #409eff; }
.wsl-body :deep(.wsl-head-col--rdc-wh) { border-bottom-color: #67c23a; }
.wsl-body :deep(.wsl-head-col--waste) { border-bottom-color: #e6a23c; }
.wsl-body :deep(.wsl-head-col--in) { border-bottom-color: #36cfc9; }
.wsl-body :deep(.wsl-head-col--ratio) { border-bottom-color: #6b8cba; }
.wsl-body :deep(.wsl-head-col--price) { border-bottom-color: #909399; }
.wsl-body :deep(.wsl-head-col--share-total) { border-bottom-color: #722ed1; }
.wsl-body :deep(.wsl-head-col--unit-amt) { border-bottom-color: #13c2c2; }
.wsl-body :deep(.wsl-head-col--time) { border-bottom-color: #606266; }
.wsl-body :deep(.wops-col--wh) {
justify-content: flex-start;
padding-left: 12px;
}
.wsl-body :deep(.wsl-head-col--time) {
text-align: center;
}
.wsl-body :deep(.wops-col--time) {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.wsl-body :deep(.wops-col--time .wops-ellipsis) {
display: block;
width: 100%;
text-align: center;
}
.wsl-footer {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
</style>
+17 -4
View File
@@ -481,8 +481,8 @@
<div v-for="(g, idx) in orderForm.goods" :key="g.goods_code + '-' + idx" class="whp-order-goods-row">
<span class="whp-og-seq">{{ idx + 1 }}</span>
<div class="whp-og-code">
<span class="whp-og-code-label" :title="g._name ? g._name + '' + g.goods_code + '' : g.goods_code">
{{ g._name ? g._name + '' + g.goods_code + '' : g.goods_code }}
<span class="whp-og-code-label" :title="formatOrderGoodsLabel(g)">
{{ formatOrderGoodsLabel(g) }}
</span>
</div>
<div class="whp-og-tax">
@@ -631,6 +631,7 @@ import {
expenseBillTabParts,
normalizeFinanceBillType,
} from '@/utils/financeBillTypes'
import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys'
defineOptions({ name: 'WhPurchase' })
@@ -760,6 +761,13 @@ function fmtOrderAmount(v) {
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
}
function formatOrderGoodsLabel(g) {
const code = String(g?.goods_code || '').trim()
const name = String(g?._name || g?.goods_name || '').trim()
if (name && name !== code) return `${name}${code}`
return code || '—'
}
function resolveBillTaxRate(billCode) {
const code = normBillCode(billCode)
if (!code) return 0
@@ -1181,13 +1189,18 @@ async function onOrderPurchaseOrgChange() {
await refreshOrderCollectionOptions()
}
function onOrderCatalogAdd({ goods_code, name, payment_price, bill_tax }) {
function onOrderCatalogAdd({ goods_code, name, payment_price, bill_tax, raw }) {
const code = String(goods_code || '').trim()
if (!code) return
if (orderForm.goods.some((g) => String(g.goods_code || '').trim() === code)) return
const row = emptyOrderGoodsRow()
row.goods_code = code
row._name = String(name || '').trim()
const source = raw?.raw || raw
let displayName = String(name || '').trim()
if (!displayName || displayName === code) {
displayName = pickGoodsNameFromListedRow(source) || displayName
}
row._name = displayName !== code ? displayName : ''
if (payment_price != null && Number.isFinite(payment_price)) {
row.purchase_amount = payment_price
}
+325 -25
View File
@@ -135,25 +135,18 @@
append-to-body
destroy-on-close
:close-on-click-modal="false"
@opened="loadAllocAttrOptions"
>
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="120px" size="small">
<el-form-item label="配货属性编码" prop="alloc_attr_code">
<el-select
v-model="addForm.alloc_attr_code"
placeholder="选择配货属性编码"
filterable
clearable
:loading="allocAttrLoading"
style="width:100%"
>
<el-option
v-for="opt in allocAttrOptions"
:key="opt.alloc_attr_code"
:label="opt.label"
:value="opt.alloc_attr_code"
<div class="whw-attr-code-row">
<el-input
v-model="addForm.alloc_attr_code"
placeholder="请点击搜索选择"
readonly
/>
</el-select>
<el-button type="warning" plain @click="openAllocAttrPickDlg">搜索</el-button>
</div>
<p v-if="addFormAllocAttrLabel" class="whw-attr-picked-label">{{ addFormAllocAttrLabel }}</p>
</el-form-item>
<el-form-item label="RDC损耗数量" prop="rdc_count">
<el-input-number v-model="addForm.rdc_count" :min="0" :controls="false" class="whw-form-num" />
@@ -170,6 +163,106 @@
<el-button type="primary" :loading="addSaving" @click="submitAdd">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="allocAttrPickVisible"
title="搜索配货属性"
width="760px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
class="whw-attr-pick-dlg"
@opened="onAllocAttrPickOpened"
>
<div class="whw-attr-pick-filters">
<el-input
v-model="allocAttrQuery.goods_code"
placeholder="物料编码"
clearable
size="small"
class="whw-attr-pick-filter"
@keyup.enter="searchAllocAttrPick(true)"
/>
<el-select
v-model="allocAttrQuery.rdc_warehouse_code"
placeholder="RDC仓"
filterable
clearable
size="small"
class="whw-attr-pick-filter"
>
<el-option
v-for="w in warehouseOptions"
:key="'pick-rdc-' + w.warehouse_code"
:label="w.label"
:value="w.warehouse_code"
/>
</el-select>
<el-select
v-model="allocAttrQuery.fdc_warehouse_code"
placeholder="FDC仓"
filterable
clearable
size="small"
class="whw-attr-pick-filter"
>
<el-option
v-for="w in warehouseOptions"
:key="'pick-fdc-' + w.warehouse_code"
:label="w.label"
:value="w.warehouse_code"
/>
</el-select>
<el-button type="primary" size="small" :loading="allocAttrLoading" @click="searchAllocAttrPick(true)">查询</el-button>
</div>
<div class="whw-attr-pick-panel">
<div v-loading="allocAttrLoading" element-loading-text="搜索中…" class="whw-attr-pick-body">
<div
v-if="!allocAttrLoading && allocAttrPickQueried && !allocAttrPickRows.length"
class="whw-attr-pick-empty-wrap"
>
<p class="whw-attr-pick-empty">未找到匹配的配货属性</p>
</div>
<div v-else class="whw-attr-pick-scroll">
<button
v-for="opt in allocAttrPickRows"
:key="opt.alloc_attr_code"
type="button"
class="whw-attr-pick-item"
:class="{ 'is-active': allocAttrPickSelected === opt.alloc_attr_code }"
@click="selectAllocAttrPick(opt)"
>
<span class="whw-attr-pick-code">{{ opt.alloc_attr_code }}</span>
<span class="whw-attr-pick-meta">
<span class="whw-attr-pick-tag">物料</span>
<span>{{ opt.goodsText }}</span>
<template v-if="opt.whText">
<span class="whw-attr-pick-sep">·</span>
<span>{{ opt.whText }}</span>
</template>
</span>
</button>
</div>
</div>
<div v-if="allocAttrPickQueried && allocAttrPickTotal > 0" class="whw-attr-pick-pager">
<el-pagination
v-model:current-page="allocAttrPickPage"
:page-size="ALLOC_ATTR_PICK_PAGE_SIZE"
:total="allocAttrPickTotal"
layout="total, prev, pager, next"
small
background
@current-change="loadAllocAttrPickPage"
/>
</div>
</div>
<template #footer>
<el-button @click="allocAttrPickVisible = false">取消</el-button>
<el-button type="primary" :disabled="!allocAttrPickSelected" @click="confirmAllocAttrPick">确定</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -178,7 +271,7 @@ import { onMounted, reactive, ref } from 'vue'
import WarehouseOpsNameCode from '@/components/warehouse/WarehouseOpsNameCode.vue'
import { ElMessage } from 'element-plus'
import { warehouseListed } from '@/api/warehouse'
import { allocAttrListed, pickAllocAttrCode } from '@/api/warehouseAlloc'
import { allocAttrSearch, pickAllocAttrCode } from '@/api/warehouseAlloc'
import {
pickApiErrorMessage,
wasteCollectListed,
@@ -189,6 +282,7 @@ import {
enrichWarehouseOpsRows,
formatOpsLogTime,
} from '@/utils/warehouseOpsDisplay'
import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys'
defineOptions({ name: 'WhWaste' })
@@ -201,8 +295,21 @@ const page = ref(1)
const limit = ref(20)
const warehouseOptions = ref([])
const allocAttrOptions = ref([])
const allocAttrLoading = ref(false)
const allocAttrPickVisible = ref(false)
const allocAttrPickQueried = ref(false)
const allocAttrPickRows = ref([])
const allocAttrPickSelected = ref('')
const allocAttrPickSelectedOption = ref(null)
const allocAttrPickPage = ref(1)
const allocAttrPickTotal = ref(0)
const ALLOC_ATTR_PICK_PAGE_SIZE = 50
const addFormAllocAttrLabel = ref('')
const allocAttrQuery = reactive({
goods_code: '',
rdc_warehouse_code: '',
fdc_warehouse_code: '',
})
const filters = reactive({
alloc_attr_code: '',
@@ -284,27 +391,52 @@ function onViewModeChange() {
loadData()
}
function allocAttrHint(row) {
function formatPickGoodsText(row) {
const code = String(row?.goods_code || '').trim()
const name = String(
row?.goods_name
|| pickGoodsNameFromListedRow(row)
|| '',
).trim()
if (name && name !== code) return `${name}${code}`
return code || '—'
}
function formatPickWhText(row) {
const parts = [
row?.goods_name || row?.goods_code,
row?.rdc_warehouse_name || row?.rdc_warehouse_code,
row?.fdc_warehouse_name || row?.fdc_warehouse_code,
].filter(Boolean)
return parts.join(' · ')
}
function allocAttrHint(row) {
const parts = [formatPickGoodsText(row), formatPickWhText(row)].filter((v) => v && v !== '—')
return parts.join(' · ')
}
function mapAllocAttrOption(row) {
const alloc_attr_code = pickAllocAttrCode(row)
if (!alloc_attr_code) return null
const goodsText = formatPickGoodsText(row)
const whText = formatPickWhText(row)
const hint = allocAttrHint(row)
const label = hint ? `${alloc_attr_code}${hint}` : alloc_attr_code
return { alloc_attr_code, label }
return { alloc_attr_code, label, hint, goodsText, whText }
}
async function loadAllocAttrOptions() {
async function searchAllocAttrPick(resetPage = false) {
if (resetPage) allocAttrPickPage.value = 1
allocAttrLoading.value = true
allocAttrPickQueried.value = true
try {
const res = await allocAttrListed({ page: 1, limit: 50 })
const res = await allocAttrSearch({
goods_code: String(allocAttrQuery.goods_code || '').trim() || undefined,
rdc_warehouse_code: String(allocAttrQuery.rdc_warehouse_code || '').trim() || undefined,
fdc_warehouse_code: String(allocAttrQuery.fdc_warehouse_code || '').trim() || undefined,
page: allocAttrPickPage.value,
limit: ALLOC_ATTR_PICK_PAGE_SIZE,
})
const enriched = enrichWarehouseOpsRows(res.list || [], {
warehouseOptions: warehouseOptions.value,
})
@@ -314,14 +446,64 @@ async function loadAllocAttrOptions() {
if (!opt || map.has(opt.alloc_attr_code)) continue
map.set(opt.alloc_attr_code, opt)
}
allocAttrOptions.value = [...map.values()]
} catch {
allocAttrOptions.value = []
allocAttrPickRows.value = [...map.values()]
allocAttrPickTotal.value = res.total || allocAttrPickRows.value.length
} catch (e) {
allocAttrPickRows.value = []
allocAttrPickTotal.value = 0
ElMessage.error(pickApiErrorMessage(e, '搜索失败'))
} finally {
allocAttrLoading.value = false
}
}
function loadAllocAttrPickPage() {
void searchAllocAttrPick(false)
}
function selectAllocAttrPick(opt) {
allocAttrPickSelected.value = opt?.alloc_attr_code || ''
allocAttrPickSelectedOption.value = opt || null
}
function resetAllocAttrPick() {
allocAttrQuery.goods_code = ''
allocAttrQuery.rdc_warehouse_code = ''
allocAttrQuery.fdc_warehouse_code = ''
allocAttrPickRows.value = []
allocAttrPickSelected.value = ''
allocAttrPickSelectedOption.value = null
allocAttrPickQueried.value = false
allocAttrPickPage.value = 1
allocAttrPickTotal.value = 0
}
function openAllocAttrPickDlg() {
resetAllocAttrPick()
allocAttrPickSelected.value = String(addForm.alloc_attr_code || '').trim()
allocAttrPickVisible.value = true
}
function onAllocAttrPickOpened() {
void searchAllocAttrPick(true)
}
function confirmAllocAttrPick() {
const code = String(allocAttrPickSelected.value || '').trim()
if (!code) return
const hit = allocAttrPickSelectedOption.value
|| allocAttrPickRows.value.find((opt) => opt.alloc_attr_code === code)
addForm.alloc_attr_code = code
addFormAllocAttrLabel.value = hit?.label || code
allocAttrPickVisible.value = false
addFormRef.value?.validateField?.('alloc_attr_code')
}
function resetAllocAttrSearch() {
resetAllocAttrPick()
addFormAllocAttrLabel.value = ''
}
async function loadWarehouses() {
try {
const list = await warehouseListed({})
@@ -341,9 +523,13 @@ async function loadWarehouses() {
function openAddDlg() {
const preset = filters.alloc_attr_code || ''
resetAllocAttrSearch()
Object.assign(addForm, emptyAddForm(), {
alloc_attr_code: preset,
})
if (preset) {
addFormAllocAttrLabel.value = preset
}
addDlgVisible.value = true
}
@@ -494,6 +680,120 @@ onMounted(async () => {
.whw-form-num {
width: 100%;
}
.whw-attr-code-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.whw-attr-code-row :deep(.el-input) {
flex: 1;
min-width: 0;
}
.whw-attr-picked-label {
margin: 6px 0 0;
font-size: 12px;
color: #606266;
line-height: 1.4;
word-break: break-all;
}
.whw-attr-pick-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.whw-attr-pick-filter {
width: 180px;
}
.whw-attr-pick-panel {
display: flex;
flex-direction: column;
gap: 10px;
}
.whw-attr-pick-body {
height: 320px;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.whw-attr-pick-scroll {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
scrollbar-gutter: stable;
}
.whw-attr-pick-pager {
display: flex;
justify-content: flex-end;
}
.whw-attr-pick-empty-wrap {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}
.whw-attr-pick-empty {
margin: 0;
padding: 0 16px;
text-align: center;
color: #909399;
font-size: 13px;
}
.whw-attr-pick-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
width: 100%;
padding: 10px 12px;
border: none;
border-bottom: 1px solid #f0f2f5;
background: #fff;
text-align: left;
cursor: pointer;
transition: background .12s;
}
.whw-attr-pick-item:last-child {
border-bottom: none;
}
.whw-attr-pick-item:hover {
background: #f5f7fa;
}
.whw-attr-pick-item.is-active {
background: #ecf5ff;
}
.whw-attr-pick-code {
font-size: 13px;
font-weight: 600;
color: #303133;
font-family: ui-monospace, monospace;
}
.whw-attr-pick-code::before {
content: '配货属性 ';
font-family: inherit;
font-weight: 500;
color: #909399;
}
.whw-attr-pick-meta {
font-size: 12px;
color: #606266;
line-height: 1.4;
}
.whw-attr-pick-tag {
color: #909399;
margin-right: 4px;
}
.whw-attr-pick-sep {
margin: 0 4px;
color: #c0c4cc;
}
.whw-form-num :deep(.el-input__inner) {
text-align: left;
}