20260706002

This commit is contained in:
xiaojibeier
2026-07-06 17:05:09 +08:00
parent fcf6aa2df6
commit a602334039
4 changed files with 532 additions and 803 deletions
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@
<div class="gt-wh-panel" v-loading="panelLoading" element-loading-text="加载中…">
<div v-if="showSchemaHint" class="gt-wh-hint">
<el-icon><InfoFilled /></el-icon>
<span>仓库管理为系统固定结构左侧选 RDC上方横向切换费用票据填写提成 / 利润内部零售渠道的率与额</span>
<span>仓库管理左侧选 RDC右侧按费用票据分组填写提成 / 利润各渠道的率与额</span>
</div>
<template v-if="!rdcOptions.length">
@@ -22,43 +22,38 @@
<span v-if="rdcDisplaySub(rdc)" class="gt-wh-rdc-nav-sub">{{ rdcDisplaySub(rdc) }}</span>
</button>
</nav>
<div class="gt-wh-main">
<div v-if="showLegacyRdcHint" class="gt-wh-legacy-hint">
当前 RDC 存在历史格式数据未按票据分桶请选择票据后重新填写并保存以迁移到新结构
</div>
<div class="gt-wh-workspace">
<template v-if="!expenseBills.length">
<el-empty :image-size="64" description="暂无费用票据" class="gt-wh-empty" />
</template>
<template v-else-if="!activeBillCode">
<el-empty :image-size="64" description="请选择费用票据" class="gt-wh-empty" />
</template>
<template v-else>
<el-tabs v-model="activeBillCode" class="gt-wh-bill-tabs">
<el-tab-pane
<div v-if="showLegacyRdcHint" class="gt-wh-legacy-hint gt-wh-legacy-hint--workspace">
当前 RDC 存在历史格式数据未按票据分桶请重新填写并保存以迁移到新结构
</div>
<div class="gt-wh-bills-scroll">
<div
v-for="bill in expenseBills"
:key="bill.bill_code"
:name="normBillCode(bill.bill_code)"
:key="normBillCode(bill.bill_code)"
class="gt-wh-bill-group"
>
<template #label>
<span class="gt-wh-bill-tab" :title="billTabTitle(bill)">
<span class="gt-wh-bill-tab-name">{{ billTabParts(bill).name }}</span>
<span v-if="billTabParts(bill).tax" class="gt-wh-bill-tab-tax">{{ billTabParts(bill).tax }}%</span>
</span>
</template>
</el-tab-pane>
</el-tabs>
<div class="gt-wh-form-body">
<div class="gt-wh-bill-group-title">
<span class="gt-wh-bill-group-name">{{ billTabParts(bill).name }}</span>
<span v-if="billTabParts(bill).tax" class="gt-wh-bill-group-tax">{{ billTabParts(bill).tax }}%</span>
</div>
<div class="gt-wh-bill-group-body">
<div class="gt-wh-sections-row">
<div
v-for="sec in WAREHOUSE_SECTIONS"
:key="sec.key"
:key="`${normBillCode(bill.bill_code)}-${sec.key}`"
class="gt-wh-section-block"
:class="sectionBlockClass(sec.key)"
>
<div class="gt-wh-section-title">{{ sec.label }}</div>
<div class="gt-wh-channel-grid">
<div
v-for="ch in WAREHOUSE_CHANNELS"
:key="ch.key"
:key="`${normBillCode(bill.bill_code)}-${sec.key}-${ch.key}`"
class="gt-wh-channel-card"
>
<div class="gt-wh-channel-title">{{ ch.label }}</div>
@@ -66,27 +61,31 @@
<div class="gt-wh-metric">
<span class="gt-wh-metric-label"></span>
<el-input-number
:model-value="metricValue(sec.key, ch.key, 'rate')"
:model-value="metricValue(sec.key, ch.key, 'rate', bill.bill_code)"
:disabled="readonly"
:controls="false"
placeholder="—"
size="small"
class="gt-wh-metric-input"
@update:model-value="(v) => setMetric(sec.key, ch.key, 'rate', v)"
@update:model-value="(v) => setMetric(sec.key, ch.key, 'rate', v, bill.bill_code)"
/>
<span class="gt-wh-metric-suffix">%</span>
</div>
<div class="gt-wh-metric">
<span class="gt-wh-metric-label">额</span>
<el-input-number
:model-value="metricValue(sec.key, ch.key, 'amount')"
:model-value="metricValue(sec.key, ch.key, 'amount', bill.bill_code)"
:disabled="readonly"
:controls="false"
placeholder=""
size="small"
class="gt-wh-metric-input"
@update:model-value="(v) => setMetric(sec.key, ch.key, 'amount', v)"
@update:model-value="(v) => setMetric(sec.key, ch.key, 'amount', v, bill.bill_code)"
/>
<span class="gt-wh-metric-suffix"></span>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -120,9 +119,7 @@ import {
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
readonly: { type: Boolean, default: false },
/** 全局页签管理:展示结构说明 */
showSchemaHint: { type: Boolean, default: false },
/** 物料 /goods/view 返回的 rdcs;公共页签、物料分类留空即可 */
rdcList: { type: Array, default: () => [] },
})
@@ -136,7 +133,6 @@ const billsLoaded = ref(false)
/** @type {import('vue').Ref<object[]>} */
const allBills = ref([])
const activeRdcCode = ref('')
const activeBillCode = ref('')
const panelLoading = computed(() => billLoading.value)
@@ -147,7 +143,6 @@ const bucket = computed({
set: (v) => emit('update:modelValue', v),
})
/** 仓库管理仅使用费用票据 type=4 */
const expenseBills = computed(() => {
return allBills.value.filter((b) => {
if (b?.is_valid === false || b?.is_valid === 0 || b?.is_valid === '0') return false
@@ -161,6 +156,12 @@ const showLegacyRdcHint = computed(() => {
return isLegacyRdcMetricsBucket(rdc)
})
function sectionBlockClass(sectionKey) {
if (sectionKey === 'commission') return 'is-commission'
if (sectionKey === 'profit') return 'is-profit'
return ''
}
function rdcDisplayName(rdc) {
const name = String(rdc?.name || '').trim()
const code = String(rdc?.code || '').trim()
@@ -179,11 +180,6 @@ function billTabParts(bill) {
return expenseBillTabParts(bill)
}
function billTabTitle(bill) {
const { name, tax } = billTabParts(bill)
return tax ? `${name} · ${tax}%` : name
}
function normBillCode(code) {
return String(code ?? '').trim()
}
@@ -192,26 +188,18 @@ function normRdcCode(code) {
return String(code ?? '').trim()
}
function pickInitialBillCode() {
const list = expenseBills.value
if (!list.length) {
activeBillCode.value = ''
return
function ensureAllBillBucketsForRdc(rdcCode) {
const rdc = normRdcCode(rdcCode)
if (!rdc || props.readonly) return
for (const bill of expenseBills.value) {
ensureBillBucket(rdc, bill.bill_code)
}
const cur = normBillCode(activeBillCode.value)
if (cur && list.some((b) => normBillCode(b.bill_code) === cur)) {
activeBillCode.value = cur
return
}
activeBillCode.value = normBillCode(list[0].bill_code)
}
function syncActiveWarehouseSelection() {
pickInitialBillCode()
function syncActiveRdcBuckets() {
const rdc = normRdcCode(activeRdcCode.value)
const bill = normBillCode(activeBillCode.value)
if (!rdc || !bill) return
if (!props.readonly) ensureBillBucket(rdc, bill)
if (!rdc || !expenseBills.value.length) return
if (!props.readonly) ensureAllBillBucketsForRdc(rdc)
}
function selectRdc(code) {
@@ -219,12 +207,11 @@ function selectRdc(code) {
activeRdcCode.value = c
if (!c || props.readonly) return
const next = { ...bucket.value }
const rdc = next[c]
if (!rdc) {
if (!next[c]) {
next[c] = {}
bucket.value = next
}
syncActiveWarehouseSelection()
syncActiveRdcBuckets()
}
function ensureBillBucket(rdcCode, billCode) {
@@ -246,25 +233,25 @@ function ensureBillBucket(rdcCode, billCode) {
return next[rdc][bill]
}
function currentBillBucket() {
const rdc = bucket.value[normRdcCode(activeRdcCode.value)]
const bill = normBillCode(activeBillCode.value)
function billBucketFor(rdcCode, billCode) {
const rdc = bucket.value[normRdcCode(rdcCode)]
const bill = normBillCode(billCode)
if (!rdc || !bill) return null
if (isLegacyRdcMetricsBucket(rdc)) return rdc
return rdc[bill] || null
}
function metricValue(sectionKey, channelKey, metricKey) {
const b = currentBillBucket()
function metricValue(sectionKey, channelKey, metricKey, billCode) {
const b = billBucketFor(activeRdcCode.value, billCode)
const v = b?.[sectionKey]?.[channelKey]?.[metricKey]
return v === null || v === undefined ? null : Number(v)
}
function setMetric(sectionKey, channelKey, metricKey, raw) {
function setMetric(sectionKey, channelKey, metricKey, raw, billCode) {
const rdcCode = normRdcCode(activeRdcCode.value)
const billCode = normBillCode(activeBillCode.value)
if (props.readonly || !rdcCode || !billCode) return
const billBucket = ensureBillBucket(rdcCode, billCode)
const bill = normBillCode(billCode)
if (props.readonly || !rdcCode || !bill) return
const billBucket = ensureBillBucket(rdcCode, bill)
if (!billBucket) return
if (!billBucket[sectionKey]) billBucket[sectionKey] = createEmptyRdcBucket()[sectionKey]
if (!billBucket[sectionKey][channelKey]) {
@@ -281,17 +268,16 @@ async function loadBillList() {
const res = await request.post('/api/bill/listed', { page: 1, limit: 500, type: FINANCE_BILL_TYPE_EXPENSE })
const list = Array.isArray(res?.data) ? res.data : (Array.isArray(res?.data?.list) ? res.data.list : [])
allBills.value = list.filter((b) => normalizeFinanceBillType(b?.type) === FINANCE_BILL_TYPE_EXPENSE)
syncActiveWarehouseSelection()
syncActiveRdcBuckets()
} catch {
allBills.value = []
activeBillCode.value = ''
} finally {
billLoading.value = false
}
}
watch(expenseBills, () => {
syncActiveWarehouseSelection()
syncActiveRdcBuckets()
})
function pickInitialRdcCode(opts) {
@@ -314,7 +300,7 @@ watch(
if (nextRdc !== normRdcCode(activeRdcCode.value)) {
selectRdc(nextRdc)
} else {
syncActiveWarehouseSelection()
syncActiveRdcBuckets()
}
if (!billsLoaded.value) {
billsLoaded.value = true
@@ -324,10 +310,6 @@ watch(
{ immediate: true },
)
watch(activeBillCode, () => {
syncActiveWarehouseSelection()
})
watch(
() => props.modelValue,
() => {
@@ -364,12 +346,6 @@ defineExpose({
border: 1px solid #d9ecff;
border-radius: 6px;
}
.gt-wh-hint code {
font-size: 11px;
padding: 0 4px;
background: #ecf5ff;
border-radius: 3px;
}
.gt-wh-legacy-hint {
padding: 8px 12px;
font-size: 12px;
@@ -379,6 +355,10 @@ defineExpose({
border: 1px solid #faecd8;
border-radius: 6px;
}
.gt-wh-legacy-hint--workspace {
flex-shrink: 0;
margin: 10px 12px 0;
}
.gt-wh-split {
display: flex;
align-items: stretch;
@@ -389,7 +369,7 @@ defineExpose({
background: #fff;
}
.gt-wh-rdc-nav {
flex: 0 0 132px;
flex: 0 0 168px;
align-self: stretch;
min-height: 0;
border-right: 1px solid #ebeef5;
@@ -436,44 +416,53 @@ defineExpose({
color: #909399;
word-break: break-all;
}
.gt-wh-main {
.gt-wh-workspace {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 14px 14px;
overflow-y: auto;
}
.gt-wh-bill-tabs :deep(.el-tabs__header) {
margin-bottom: 0;
}
.gt-wh-bill-tabs :deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
.gt-wh-bill-tabs :deep(.el-tabs__item) {
height: 38px;
padding: 0 14px;
}
.gt-wh-bill-tab {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 168px;
vertical-align: middle;
}
.gt-wh-bill-tab-name {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
line-height: 1.2;
}
.gt-wh-bill-tab-tax {
.gt-wh-bills-scroll {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 10px 10px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.gt-wh-bill-group {
flex-shrink: 0;
display: flex;
flex-direction: column;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 8px;
overflow: hidden;
}
.gt-wh-bill-group-title {
flex-shrink: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 8px 14px 6px;
font-size: 12px;
font-weight: 600;
color: #8b9ab0;
letter-spacing: 0.5px;
border-bottom: 1px solid #ebeef5;
background: #fafbfc;
}
.gt-wh-bill-group-name {
color: #606266;
font-size: 13px;
}
.gt-wh-bill-group-tax {
padding: 0 6px;
height: 18px;
line-height: 18px;
@@ -482,36 +471,32 @@ defineExpose({
font-weight: 600;
background: #fef0f0;
color: #f56c6c;
transition: background 0.15s, color 0.15s;
}
.gt-wh-bill-tabs :deep(.el-tabs__item.is-active) .gt-wh-bill-tab-name {
font-weight: 600;
.gt-wh-bill-group-body {
padding: 12px;
}
.gt-wh-bill-tabs :deep(.el-tabs__item.is-active) .gt-wh-bill-tab-tax {
background: #f56c6c;
color: #fff;
}
.gt-wh-bill-tabs :deep(.el-tabs__item:hover) .gt-wh-bill-tab-tax {
background: #fde2e2;
}
.gt-wh-bill-tabs :deep(.el-tabs__item.is-active:hover) .gt-wh-bill-tab-tax {
background: #f56c6c;
color: #fff;
}
.gt-wh-form-body {
display: flex;
flex-direction: column;
gap: 14px;
.gt-wh-sections-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
align-items: start;
}
.gt-wh-section-block {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
padding: 10px 10px 12px;
border-radius: 8px;
border: 1px solid transparent;
}
.gt-wh-section-block + .gt-wh-section-block {
margin-top: 4px;
padding-top: 14px;
border-top: 1px solid #ebeef5;
.gt-wh-section-block.is-commission {
background: #f7fafd;
border-color: #e6eef8;
}
.gt-wh-section-block.is-profit {
background: #f8fbf6;
border-color: #e5efe5;
}
.gt-wh-section-title {
font-size: 13px;
@@ -519,56 +504,50 @@ defineExpose({
color: #303133;
padding-left: 2px;
}
.gt-wh-section-block.is-commission .gt-wh-section-title {
color: #4a6fa5;
}
.gt-wh-section-block.is-profit .gt-wh-section-title {
color: #5a8f5a;
}
.gt-wh-channel-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 900px) {
.gt-wh-split {
flex-direction: column;
}
.gt-wh-rdc-nav {
flex: 0 0 auto;
max-height: 140px;
border-right: none;
border-bottom: 1px solid #ebeef5;
}
.gt-wh-channel-grid {
grid-template-columns: 1fr;
}
gap: 10px;
}
.gt-wh-channel-card {
padding: 12px;
padding: 10px 8px;
border: 1px solid #ebeef5;
border-radius: 8px;
border-radius: 6px;
background: #fff;
min-width: 0;
}
.gt-wh-channel-title {
font-size: 13px;
font-size: 12px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
padding-bottom: 8px;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px dashed #ebeef5;
}
.gt-wh-metrics {
display: flex;
flex-direction: column;
gap: 10px;
gap: 8px;
}
.gt-wh-metric {
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
}
.gt-wh-metric-label {
flex: 0 0 20px;
flex: 0 0 18px;
font-size: 12px;
color: #909399;
}
.gt-wh-metric-input {
flex: 1;
min-width: 0;
width: 100%;
}
.gt-wh-metric-input :deep(.el-input__inner) {
@@ -582,4 +561,21 @@ defineExpose({
.gt-wh-empty {
padding: 24px 0;
}
@media (max-width: 900px) {
.gt-wh-split {
flex-direction: column;
}
.gt-wh-rdc-nav {
flex: 0 0 auto;
max-height: 120px;
border-right: none;
border-bottom: 1px solid #ebeef5;
}
.gt-wh-sections-row {
grid-template-columns: 1fr;
}
.gt-wh-channel-grid {
grid-template-columns: 1fr;
}
}
</style>
+7
View File
@@ -89,6 +89,10 @@
.wops-flex-table.is-cols-pr-cost {
--wops-cols: 7fr 10.5fr 10.5fr 7fr 8fr 8fr 6fr 9fr 6fr 8fr;
}
/* 物料成本围栏(11 列):内部交易相关表头加宽,配合 88vw 弹窗单行显示 */
.wops-flex-table.is-cols-cost-fence {
--wops-cols: 8fr 9fr 10fr 7fr 6fr 9fr 10fr 8fr 11fr 7fr 5fr;
}
.wops-flex-scroll {
flex: 1;
@@ -197,6 +201,7 @@
.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-cost-fence .wops-flex-row,
.wops-flex-table.is-cols-collect-whp .wops-flex-row,
.wops-flex-table.is-cols-logs-whp .wops-flex-row,
.wops-flex-table.is-cols-orders-whp .wops-flex-row,
@@ -220,6 +225,8 @@
.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-cost-fence .wops-col--num,
.wops-flex-table.is-cols-cost-fence .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-collect-whp .wops-col--num,
.wops-flex-table.is-cols-collect-whp .wops-flex-head .wops-col--num,
.wops-flex-table.is-cols-logs-whp .wops-col--num,
+38 -9
View File
@@ -414,7 +414,7 @@
<el-dialog
v-model="materialDialogVisible"
:title="batchSelMode ? `批量设置选配(${selectedRows.length} 条物料)` : (materialDialogMode === 'add' ? '新增物料' : String(materialEditingId || '修改物料'))"
width="72vw"
width="88vw"
:close-on-click-modal="false"
destroy-on-close
append-to-body
@@ -504,7 +504,7 @@
</div>
<template v-for="(cat, catIdx) in materialForm.categories" :key="`${cat.category_code}-${catIdx}`">
<div
v-show="activeCatCode === cat.category_code && mlActiveNavKey !== ML_NAV_WAREHOUSE"
v-show="activeCatCode === cat.category_code && mlActiveNavKey !== ML_NAV_WAREHOUSE && mlActiveNavKey !== ML_NAV_COST"
class="mf-cat-pane"
:class="{ 'mf-cat-pane--fill': mlActiveNavKey === ML_NAV_BASIC }"
>
@@ -2579,10 +2579,25 @@ const windowW = ref(window.innerWidth)
const isMobile = computed(() => windowW.value <= 600)
window.addEventListener('resize', () => { windowW.value = window.innerWidth })
// 72vw padding/ 80px 240px1920525607
/** 物料弹窗宽度(须与 template width 一致) */
const MF_DIALOG_VW = 0.88
/** 弹窗 inset + 左侧导航 + 页签区内边距 + 字段网格内边距 */
const MF_GRID_INSET = 244
/** 每列最小宽度(1920×88vw 右侧字段区 → 6 列) */
const MF_COL_MIN_PX = 215
const MF_GRID_GAP = 16
function mfGridAvailW() {
return Math.max(320, windowW.value * MF_DIALOG_VW - MF_GRID_INSET)
}
function mfGridColPx(cols = mfColCount.value) {
return Math.max(160, Math.floor((mfGridAvailW() - (cols - 1) * MF_GRID_GAP) / cols))
}
// 88vw 1920625608
const mfColCount = computed(() => {
const bodyW = windowW.value * 0.72 - 80
return Math.max(2, Math.min(8, Math.floor(bodyW / 240)))
return Math.max(2, Math.min(8, Math.floor(mfGridAvailW() / MF_COL_MIN_PX)))
})
// MaterialCategory.fieldGridSpan
@@ -2591,7 +2606,7 @@ function mfFieldSpan(field) {
if (field.type === 'table' || field.type === 'textarea') return 'mf-field-full'
if (field.type === 'attachment') return 'mf-field-span2'
const cols = mfColCount.value
const colPx = Math.max(160, Math.floor((windowW.value * 0.72 - 80 - (cols - 1) * 16) / cols))
const colPx = mfGridColPx(cols)
if (field.type === 'radio') {
const opts = field.options || []
if (opts.length <= 1) return ''
@@ -2616,7 +2631,7 @@ function mfFieldSpan(field) {
/** 适配项占列数:名称较长时占两列 */
function adpItemSpan(name) {
const cols = mfColCount.value
const colPx = Math.max(160, Math.floor((windowW.value * 0.72 - 80 - (cols - 1) * 16) / cols))
const colPx = mfGridColPx(cols)
const len = String(name || '').length
// 14px/ 8px/
const estPx = len * 12 + 36 // 36 = + gap
@@ -2650,7 +2665,7 @@ function _collectCategoryWarehouseFlatFields(categoryCode, warehouseCode) {
/** 选配查看态:每个控件在 mf-fields-grid 中的占列规则,与 mfFieldSpan 一致 */
function mfSelCompGridClass(comp) {
const cols = mfColCount.value
const colPx = Math.max(160, Math.floor((windowW.value * 0.72 - 80 - (cols - 1) * 16) / cols))
const colPx = mfGridColPx(cols)
const type = comp?.type
const opts = comp?.options || []
if (type === 'radio') {
@@ -10891,6 +10906,20 @@ provide('mlTabPaneCtx', {
padding: 8px 12px 12px;
box-sizing: border-box;
}
.mf-nav-pane--cost-api {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
padding: 8px 12px 12px;
box-sizing: border-box;
}
.mf-right-scroll:has(.mf-nav-pane--cost-api) {
display: flex;
flex-direction: column;
overflow: hidden;
}
.mf-nav-pane--basic {
padding-top: 4px;
}
@@ -11059,7 +11088,7 @@ provide('mlTabPaneCtx', {
.mf-fields-grid {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 默认 4 列,JS 会覆盖 */
grid-template-columns: repeat(6, 1fr); /* 默认 6 列,JS 会覆盖 */
gap: 10px 16px;
align-items: start;
padding: 12px 14px;