This commit is contained in:
xiaojibeier
2026-07-19 00:39:51 +08:00
parent cd204a7808
commit 5eab17d624
7 changed files with 603 additions and 77 deletions
+129 -4
View File
@@ -1,5 +1,10 @@
import request from '@/utils/request'
import { orgTaxTypeLabel } from '@/constants/orgTaxType'
import {
ORG_TAX_TYPE_GENERAL,
ORG_TAX_TYPE_RECEIPT,
ORG_TAX_TYPE_SMALL,
orgTaxTypeLabel,
} from '@/constants/orgTaxType'
import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys'
function str(v) {
@@ -26,6 +31,18 @@ export const PRICING_TAXPAYER_BY_ORG = {
export const PRICING_TAXPAYER_DEFAULT = 'GENERAL'
/** 成本围栏 Tab key → 保存接口 org_type(1 一般 / 2 小规模 / 3 收据) */
export const PRICING_TAXPAYER_ORG_TYPE = {
GENERAL: ORG_TAX_TYPE_GENERAL,
SMALL: ORG_TAX_TYPE_SMALL,
INDIVIDUAL: ORG_TAX_TYPE_RECEIPT,
}
export function pricingTaxpayerTypeToOrgType(taxpayerType) {
const key = str(taxpayerType).toUpperCase()
return PRICING_TAXPAYER_ORG_TYPE[key] ?? null
}
const PRICING_TAXPAYER_KEYS = new Set(PRICING_TAXPAYER_TABS.map((t) => t.key))
/** 接口 data.list */
@@ -41,6 +58,81 @@ function unwrapPricingTableList(res) {
return []
}
/** 成本价上限:0 或未设视为无勾选(后端默认 0) */
export function normalizePricingTableCostPriceBound(value) {
if (value == null || value === '') return null
const n = Number(value)
if (!Number.isFinite(n) || n <= 0) return null
return n
}
/** 纳税身份 Tab → 定价表 / RDC 成本价上限字段 */
export const PRICING_COST_PRICE_BOUND_FIELDS = {
GENERAL: 'general_cost_price_bound',
SMALL: 'small_cost_price_bound',
INDIVIDUAL: 'individual_cost_price_bound',
}
export function emptyPricingTableCostPriceBounds() {
return {
GENERAL: null,
SMALL: null,
INDIVIDUAL: null,
}
}
function pricingTableCostPriceBoundSources(res, list = []) {
const envelope = res?.data ?? res
const payload = envelope?.data ?? envelope
const sources = [payload, envelope, res].filter((s) => s && typeof s === 'object' && !Array.isArray(s))
for (const row of list) {
if (row && typeof row === 'object') sources.push(row)
}
return sources
}
/** 定价表响应中的 RDC 物料成本价上限(三种纳税身份各一) */
export function pickPricingTableCostPriceBounds(res, list = [], { taxpayer_type = '' } = {}) {
const bounds = emptyPricingTableCostPriceBounds()
const sources = pricingTableCostPriceBoundSources(res, list)
for (const [key, field] of Object.entries(PRICING_COST_PRICE_BOUND_FIELDS)) {
for (const src of sources) {
const n = pickMoney(src, field)
if (n != null) {
bounds[key] = normalizePricingTableCostPriceBound(n)
break
}
}
}
const legacyKey = str(taxpayer_type).toUpperCase()
if (legacyKey && PRICING_TAXPAYER_KEYS.has(legacyKey) && bounds[legacyKey] == null) {
for (const src of sources) {
const n = pickMoney(src, 'cost_price_bound')
if (n != null) {
bounds[legacyKey] = normalizePricingTableCostPriceBound(n)
break
}
}
}
return bounds
}
/** 指定纳税身份的成本价上限 */
export function pickPricingTableCostPriceBoundForType(bounds, taxpayerType) {
const key = str(taxpayerType).toUpperCase()
if (!key || !PRICING_TAXPAYER_KEYS.has(key)) return null
return normalizePricingTableCostPriceBound(bounds?.[key])
}
/** @deprecated 请用 pickPricingTableCostPriceBounds + pickPricingTableCostPriceBoundForType */
export function pickPricingTableCostPriceBound(res, list = [], taxpayerType = '') {
const bounds = pickPricingTableCostPriceBounds(res, list, { taxpayer_type: taxpayerType })
return pickPricingTableCostPriceBoundForType(bounds, taxpayerType)
}
function pickText(raw, ...keys) {
for (const k of keys) {
const v = raw[k]
@@ -92,6 +184,7 @@ export function normalizePricingTableRow(raw, idx = 0) {
const supplier_org_type = pickRate(raw, 'supplier_org_type')
const supplier_org_type_label = orgTaxTypeLabel(supplier_org_type) || null
const goods_name = pickGoodsNameFromListedRow(raw) || pickText(raw, 'goods_name')
const pre_price = pickMoney(raw, 'pre_price')
const bill_type = pickText(raw, 'bill_name', 'bill_type')
const input_tax_rate = pickRate(raw, 'bill_tax', 'input_tax_rate')
const source_company_name = pickText(raw, 'receive_company_name', 'source_company_name')
@@ -121,11 +214,20 @@ export function normalizePricingTableRow(raw, idx = 0) {
)
const fence_value = pickMoney(raw, 'fence_price', 'fence_value')
const bloc = pickText(raw, 'supplier_bloc_code')
const supplier_company_code = pickText(raw, 'supplier_company_code', 'company_code')
const supplier_company_code = pickPricingTableRowSupplierCompanyCode(raw)
const supply_collection_code = pickText(raw, 'supply_collection_code', 'collection_code')
const explicitKey = pickText(raw, 'key', 'id')
const rowKey = explicitKey ?? [
bloc || 'bloc',
supplier_company_code || `sup-${idx}`,
receive_company_code || `rcv-${idx}`,
source_warehouse_code || '',
supply_collection_code || '',
].filter(Boolean).join('-')
return {
key: pickText(raw, 'key', 'id') ?? `${bloc || 'bloc'}-${supplier_company_code || idx}`,
key: rowKey,
supplier_bloc_name,
supplier_company_name,
supplier_company_display: formatCompanyNameWithOrgType(supplier_company_name, supplier_org_type_label),
@@ -134,6 +236,7 @@ export function normalizePricingTableRow(raw, idx = 0) {
supplier_company_code,
supply_collection_code,
goods_name,
pre_price,
bill_type,
input_tax_rate,
source_company_name,
@@ -150,9 +253,22 @@ export function normalizePricingTableRow(raw, idx = 0) {
source_rdc_name: source_warehouse_name,
source_rdc_code: source_warehouse_code,
fence_value,
raw,
}
}
/** 定价表行 → 供货组织编码(围栏值 ? debug 请求用) */
export function pickPricingTableRowSupplierCompanyCode(row) {
const raw = row?.raw && typeof row.raw === 'object' ? row.raw : row
if (!raw || typeof raw !== 'object') return ''
return pickText(
raw,
'supplier_company_code',
'supply_company_code',
'supplier_org_code',
)
}
/**
* 组装 POST /api/pricing/table 请求体
* @param {object} body
@@ -197,9 +313,18 @@ export function buildPricingTablePayload(body = {}, opts = {}) {
export async function pricingTableQuery(body = {}) {
const payload = buildPricingTablePayload(body, { debug: false })
const res = await request.post('/api/pricing/table', payload)
return unwrapPricingTableList(res)
const list = unwrapPricingTableList(res)
const rows = list
.map((row, idx) => normalizePricingTableRow(row, idx))
.filter(Boolean)
const cost_price_bounds = pickPricingTableCostPriceBounds(res, list, {
taxpayer_type: payload.taxpayer_type,
})
return {
rows,
cost_price_bounds,
cost_price_bound: pickPricingTableCostPriceBoundForType(cost_price_bounds, payload.taxpayer_type),
}
}
/**
+23
View File
@@ -388,6 +388,29 @@ export function warehouseRdcSetZone(body) {
return request.post('/api/warehouse/rdc/set_zone', payload)
}
/** POST /api/warehouse/rdc/set_cost_price_bound — body: { warehouse_code, goods_code, org_type, cost_price_bound } */
export function warehouseRdcSetCostPriceBound(body) {
const warehouse_code = String(body?.warehouse_code ?? '').trim()
const goods_code = String(body?.goods_code ?? '').trim()
const org_type = Number(body?.org_type)
const cost_price_bound = Number(body?.cost_price_bound)
if (!warehouse_code || !goods_code) {
return Promise.reject(new Error('set_cost_price_bound 缺少 warehouse_code / goods_code'))
}
if (!Number.isFinite(org_type) || org_type < 1 || org_type > 3) {
return Promise.reject(new Error('set_cost_price_bound 缺少有效 org_type(1/2/3)'))
}
if (!Number.isFinite(cost_price_bound)) {
return Promise.reject(new Error('set_cost_price_bound 缺少有效 cost_price_bound'))
}
return request.post('/api/warehouse/rdc/set_cost_price_bound', {
warehouse_code,
goods_code,
org_type,
cost_price_bound,
})
}
/** POST /api/warehouse/rdc/match_zone — body: { warehouse_code, goods_code } */
export async function warehouseRdcMatchZone(body) {
const warehouse_code = String(body?.warehouse_code ?? '').trim()
+317 -49
View File
@@ -57,6 +57,12 @@
<div class="gt-cost-table-wrap">
<div class="wops-flex-table is-cols-cost-fence">
<div
class="wops-flex-body gt-cost-table-body"
v-loading="loading"
element-loading-text="加载中…"
element-loading-background="rgba(255, 255, 255, 0.72)"
>
<div class="wops-flex-scroll">
<div class="wops-flex-row wops-flex-head">
<div
@@ -68,53 +74,79 @@
>
{{ col.label }}<span v-if="col.unit" class="gt-cost-head-sub">({{ col.unit }})</span>
</div>
<div
class="wops-col wops-col--sel gt-cost-head-col gt-cost-head-col--sel"
aria-hidden="true"
/>
</div>
<div
class="gt-cost-table-body"
v-loading="loading"
element-loading-text="加载中…"
element-loading-background="rgba(255, 255, 255, 0.72)"
>
<div v-if="!loading && !fenceRows.length" class="wops-empty-wrap">
<p class="wops-empty">暂无围栏数据</p>
</div>
<template v-if="fenceRows.length">
<div
v-for="(row, idx) in fenceRows"
:key="row.key || idx"
class="wops-flex-row wops-flex-data"
:class="{ 'is-stripe': idx % 2 === 1 }"
:class="{
'is-stripe': idx % 2 === 1,
'is-below-selected': isFenceRowAboveBound(row),
}"
>
<div class="wops-col wops-col--wh">
<span class="wops-name wops-ellipsis">{{ row.supplier_bloc_name || '—' }}</span>
<el-tooltip
:content="fenceCellText(row.supplier_bloc_name)"
:disabled="fenceCellText(row.supplier_bloc_name) === '—'"
placement="top"
:show-after="280"
:popper-options="{ strategy: 'fixed' }"
>
<span class="wops-name wops-ellipsis">{{ fenceCellText(row.supplier_bloc_name) }}</span>
</el-tooltip>
</div>
<div class="wops-col wops-col--wh">
<span
class="wops-name wops-ellipsis"
:title="row.supplier_company_display || row.supplier_company_name || ''"
>{{ row.supplier_company_display || row.supplier_company_name || '—' }}</span>
</div>
<div class="wops-col wops-col--goods">
<span class="wops-name wops-ellipsis">{{ row.goods_name || '—' }}</span>
<el-tooltip
:content="fenceCellText(row.supplier_company_display, row.supplier_company_name)"
:disabled="fenceCellText(row.supplier_company_display, row.supplier_company_name) === '—'"
placement="top"
:show-after="280"
:popper-options="{ strategy: 'fixed' }"
>
<span class="wops-name wops-ellipsis">{{ fenceCellText(row.supplier_company_display, row.supplier_company_name) }}</span>
</el-tooltip>
</div>
<div class="wops-col wops-col--num gt-cost-col--pre-price">{{ fmtMoney(row.pre_price) }}</div>
<div class="wops-col wops-col--plain gt-cost-col--bill">
<span class="wops-ellipsis">{{ row.bill_type || '—' }}</span>
</div>
<div class="wops-col wops-col--num gt-cost-col--input-tax">{{ fmtTaxRate(row.input_tax_rate) }}</div>
<div class="wops-col wops-col--wh">
<span
class="wops-name wops-ellipsis"
:title="row.source_company_display || row.source_company_name || ''"
>{{ row.source_company_display || row.source_company_name || '—' }}</span>
<el-tooltip
:content="fenceCellText(row.source_company_display, row.source_company_name)"
:disabled="fenceCellText(row.source_company_display, row.source_company_name) === '—'"
placement="top"
:show-after="280"
:popper-options="{ strategy: 'fixed' }"
>
<span class="wops-name wops-ellipsis">{{ fenceCellText(row.source_company_display, row.source_company_name) }}</span>
</el-tooltip>
</div>
<div class="wops-col wops-col--num gt-cost-col--min-price">{{ fmtMoney(row.internal_min_price) }}</div>
<div class="wops-col wops-col--plain gt-cost-col--int-bill">
<span class="wops-ellipsis">{{ row.internal_bill_type || '—' }}</span>
</div>
<div class="wops-col wops-col--num gt-cost-col--internal-tax">{{ fmtTaxRate(row.internal_input_tax_rate) }}</div>
<div class="wops-col wops-col--wh">
<span class="wops-name wops-ellipsis">{{ row.source_warehouse_name || '—' }}</span>
<div class="wops-col wops-col--wh gt-cost-col--rdc">
<el-tooltip
:content="fenceCellText(row.source_warehouse_name)"
:disabled="fenceCellText(row.source_warehouse_name) === '—'"
placement="top"
:show-after="280"
:popper-options="{ strategy: 'fixed' }"
>
<span class="wops-name wops-ellipsis">{{ fenceCellText(row.source_warehouse_name) }}</span>
</el-tooltip>
</div>
<div class="wops-col wops-col--num gt-cost-col--fence">
<span
@@ -132,8 +164,22 @@
</span>
<span v-else class="gt-cost-fence-num">{{ fmtMoney(row.fence_value) }}</span>
</div>
<div class="wops-col wops-col--sel">
<button
type="button"
class="gt-cost-fence-sel-btn"
role="radio"
:aria-checked="isFenceRowSelected(row)"
:aria-label="`选择第 ${idx + 1} 行`"
:disabled="!canSelectFenceRow(row) || savingCostBound || loading"
@click.stop="selectFenceRow(row)"
>
<span class="gt-cost-fence-sel-box" :class="{ 'is-selected': isFenceRowSelected(row) }" />
</button>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
@@ -214,15 +260,20 @@ import {
pickCustomerCompanyCode,
pickCustomerDisplayName,
} from '@/api/customer'
import { formatOrgSaleTypeLabel } from '@/api/company'
import { warehouseRdcSetCostPriceBound } from '@/api/warehouseRdc'
import {
PRICING_TAXPAYER_DEFAULT,
PRICING_TAXPAYER_TABS,
PRICING_TRADE_DOMESTIC,
emptyPricingTableCostPriceBounds,
formatPricingTableDebug,
pickPricingTableCostPriceBoundForType,
pickPricingTableError,
pickPricingTableRowSupplierCompanyCode,
normalizePricingTableCostPriceBound,
pricingTableDebugQuery,
pricingTableQuery,
pricingTaxpayerTypeToOrgType,
} from '@/api/pricingTable'
const props = defineProps({
@@ -231,10 +282,12 @@ const props = defineProps({
rdcList: { type: Array, default: () => [] },
})
const emit = defineEmits(['fence-goods-names-change'])
const FENCE_COLUMNS = [
{ key: 'supplier_bloc_name', label: '供应商集团名称' },
{ key: 'supplier_company_name', label: '供应商组织名称' },
{ key: 'goods_name', label: '商品名称' },
{ key: 'pre_price', label: '商品预估成本', unit: '元' },
{ key: 'bill_type', label: '票据类型' },
{ key: 'input_tax_rate', label: '进项税率', unit: '%' },
{ key: 'source_company_name', label: '源采购组织名称' },
@@ -245,11 +298,10 @@ const FENCE_COLUMNS = [
{ key: 'fence_value', label: '围栏值', unit: '元' },
]
const NUM_KEYS = new Set(['input_tax_rate', 'internal_min_price', 'internal_input_tax_rate', 'fence_value'])
const NUM_KEYS = new Set(['pre_price', 'input_tax_rate', 'internal_min_price', 'internal_input_tax_rate', 'fence_value'])
function colHeadClass(key) {
if (NUM_KEYS.has(key)) return 'wops-col--num'
if (key === 'goods_name') return 'wops-col--goods'
if (key === 'supplier_bloc_name' || key === 'supplier_company_name' || key === 'source_company_name' || key === 'source_rdc') {
return 'wops-col--wh'
}
@@ -260,7 +312,7 @@ function colHeadColorClass(key) {
const map = {
supplier_bloc_name: 'gt-cost-head-col--bloc',
supplier_company_name: 'gt-cost-head-col--company',
goods_name: 'gt-cost-head-col--goods',
pre_price: 'gt-cost-head-col--pre-price',
bill_type: 'gt-cost-head-col--bill',
input_tax_rate: 'gt-cost-head-col--tax',
source_company_name: 'gt-cost-head-col--source-co',
@@ -285,6 +337,77 @@ function orgTypeTabClass(key) {
return ''
}
const FENCE_MONEY_EPS = 0.005
function fenceRowFenceNumber(row) {
const v = row?.fence_value
if (v == null || v === '') return null
const n = Number(v)
return Number.isFinite(n) ? n : null
}
function isFenceMoneyEqual(a, b) {
if (a == null || b == null) return false
return Math.abs(a - b) < FENCE_MONEY_EPS
}
function activeCostPriceBound() {
return pickPricingTableCostPriceBoundForType(costPriceBounds.value, activeTaxpayerType.value)
}
function isFenceRowSelected(row) {
const bound = activeCostPriceBound()
const fence = fenceRowFenceNumber(row)
if (bound == null) return false
return isFenceMoneyEqual(fence, bound)
}
function isFenceRowAboveBound(row) {
const bound = activeCostPriceBound()
const fence = fenceRowFenceNumber(row)
if (bound == null || fence == null) return false
return fence > bound + FENCE_MONEY_EPS
}
function canSelectFenceRow(row) {
const fence = fenceRowFenceNumber(row)
return fence != null && fence > 0
}
async function selectFenceRow(row) {
if (isFenceRowSelected(row)) return
const cost_price_bound = fenceRowFenceNumber(row)
if (!canSelectFenceRow(row)) {
ElMessage.warning('该行围栏值无效,无法设为成本价上限')
return
}
const goods_code = String(props.goodsCode || '').trim()
const warehouse_code = String(activeWarehouseCode.value || '').trim()
if (!goods_code || !warehouse_code) return
const taxpayer_type = String(activeTaxpayerType.value || '').trim()
const org_type = pricingTaxpayerTypeToOrgType(taxpayer_type)
if (!taxpayer_type || org_type == null) return
if (savingCostBound.value || loading.value) return
savingCostBound.value = true
try {
await warehouseRdcSetCostPriceBound({
warehouse_code,
goods_code,
org_type,
cost_price_bound,
})
costPriceBounds.value = {
...costPriceBounds.value,
[taxpayer_type]: normalizePricingTableCostPriceBound(cost_price_bound),
}
} catch (e) {
ElMessage.error(String(e?.data?.message || e?.message || '设置成本价上限失败').trim() || '设置成本价上限失败')
} finally {
savingCostBound.value = false
}
}
const loading = ref(false)
const loadError = ref('')
/** 当前 Tab 对应的接口 taxpayer_type 枚举 */
@@ -292,9 +415,24 @@ const activeTaxpayerType = ref(PRICING_TAXPAYER_DEFAULT)
const activeWarehouseCode = ref('')
/** @type {import('vue').Ref<object[]>} */
const fenceRows = ref([])
/** 当前售出 RDC 三种纳税身份的成本价上限(与围栏值相等的行打勾) */
const costPriceBounds = ref(emptyPricingTableCostPriceBounds())
const savingCostBound = ref(false)
/** 递增序号,避免切换售出仓 / 纳税身份时旧请求覆盖新结果 */
let fenceFetchSeq = 0
const fenceGoodsNames = computed(() => {
const names = []
const seen = new Set()
for (const row of fenceRows.value) {
const name = String(row?.goods_name ?? '').trim()
if (!name || seen.has(name)) continue
seen.add(name)
names.push(name)
}
return names
})
const calculateDebugVisible = ref(false)
const calculateDebugTitle = ref('算价明细')
const calculateDebugText = ref('')
@@ -342,6 +480,14 @@ function fmtMoney(v) {
return n.toFixed(2)
}
function fenceCellText(...candidates) {
for (const v of candidates) {
const s = String(v ?? '').trim()
if (s) return s
}
return '—'
}
function fmtTaxRate(v) {
if (v == null || v === '') return '—'
const n = Number(v)
@@ -372,7 +518,7 @@ function buildPricingTableDebugBody(row, customerCode = '') {
}
const receive_company_code = String(row?.receive_company_code || '').trim()
if (receive_company_code) body.receive_company_code = receive_company_code
const supplier_company_code = String(row?.supplier_company_code || '').trim()
const supplier_company_code = pickPricingTableRowSupplierCompanyCode(row)
if (supplier_company_code) body.supplier_company_code = supplier_company_code
const source_warehouse_code = String(row?.source_warehouse_code || row?.source_rdc_code || '').trim()
if (source_warehouse_code) body.source_warehouse_code = source_warehouse_code
@@ -483,14 +629,17 @@ async function fetchFenceTable() {
if (!goods_code) {
fenceRows.value = []
costPriceBounds.value = emptyPricingTableCostPriceBounds()
return
}
if (!warehouse_code) {
fenceRows.value = []
costPriceBounds.value = emptyPricingTableCostPriceBounds()
return
}
if (!taxpayer_type) {
fenceRows.value = []
costPriceBounds.value = emptyPricingTableCostPriceBounds()
return
}
@@ -498,7 +647,7 @@ async function fetchFenceTable() {
loading.value = true
loadError.value = ''
try {
const rows = await pricingTableQuery({
const { rows, cost_price_bounds } = await pricingTableQuery({
goods_code,
warehouse_code,
taxpayer_type,
@@ -507,6 +656,7 @@ async function fetchFenceTable() {
if (seq !== fenceFetchSeq) return
if (activeTaxpayerType.value !== taxpayer_type) return
fenceRows.value = rows
costPriceBounds.value = cost_price_bounds
} catch (e) {
if (seq !== fenceFetchSeq) return
if (activeTaxpayerType.value !== taxpayer_type) return
@@ -521,6 +671,7 @@ async function loadCost() {
const code = String(props.goodsCode || '').trim()
loadError.value = ''
fenceRows.value = []
costPriceBounds.value = emptyPricingTableCostPriceBounds()
if (!code) {
activeTaxpayerType.value = PRICING_TAXPAYER_DEFAULT
activeWarehouseCode.value = ''
@@ -539,6 +690,14 @@ watch(
{ immediate: true },
)
watch(
fenceGoodsNames,
(names) => {
emit('fence-goods-names-change', names)
},
{ immediate: true },
)
watch(
() => props.rdcList,
() => {
@@ -674,6 +833,7 @@ onBeforeUnmount(() => {
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.gt-cost-org-tabs {
flex-shrink: 0;
@@ -734,20 +894,30 @@ onBeforeUnmount(() => {
padding-right: 6px;
}
.gt-cost-table-wrap :deep(.wops-flex-table) {
flex: 1;
flex: 1 1 0;
min-height: 0;
height: 100%;
max-height: 100%;
overflow: hidden;
}
.gt-cost-table-wrap :deep(.wops-flex-body),
.gt-cost-table-body {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.gt-cost-table-wrap :deep(.wops-flex-scroll) {
flex: 1;
flex: none;
min-height: 0;
overflow: visible;
}
.gt-cost-table-wrap :deep(.wops-flex-scroll > .wops-flex-head) {
position: sticky;
top: 0;
z-index: 3;
}
.gt-cost-table-body {
flex: 1;
min-height: 120px;
position: relative;
display: flex;
flex-direction: column;
}
.gt-cost-table-wrap :deep(.wops-empty-wrap) {
flex: 1;
@@ -774,8 +944,84 @@ onBeforeUnmount(() => {
color: #b0b3b8;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--bloc) { border-bottom-color: #409eff; }
.gt-cost-table-wrap :deep(.wops-col--sel) {
padding: 0;
overflow: visible;
}
.gt-cost-fence-sel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
line-height: 0;
}
.gt-cost-fence-sel-box {
display: inline-block;
width: 14px;
height: 14px;
flex-shrink: 0;
border: 1.5px solid #c0c4cc;
border-radius: 2px;
background: #fff;
box-sizing: border-box;
transition: border-color 0.15s, background 0.15s;
position: relative;
}
.gt-cost-fence-sel-btn:hover .gt-cost-fence-sel-box {
border-color: #409eff;
}
.gt-cost-fence-sel-btn:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.gt-cost-fence-sel-btn:disabled:hover .gt-cost-fence-sel-box {
border-color: #c0c4cc;
}
.gt-cost-fence-sel-box.is-selected {
border-color: #409eff;
background: #409eff;
}
.gt-cost-fence-sel-box.is-selected::after {
content: '';
position: absolute;
left: 3px;
top: 1px;
width: 5px;
height: 8px;
border: 2px solid #fff;
border-top: none;
border-left: none;
transform: rotate(45deg);
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--sel) {
border-bottom: 2px solid #dcdfe6;
padding-left: 0;
padding-right: 0;
}
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected) {
color: #c0c4cc;
background: #f5f7fa;
}
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected .wops-name),
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected .wops-code),
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected .wops-ellipsis),
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected .gt-cost-fence-num),
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected .gt-cost-fence-help-icon) {
color: #c0c4cc;
}
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected.is-stripe) {
background: #eef0f3;
}
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected:hover),
.gt-cost-table-wrap :deep(.wops-flex-data.is-below-selected.is-stripe:hover) {
background: #eef0f3;
color: #c0c4cc;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--company) { border-bottom-color: #5470c6; }
.gt-cost-table-wrap :deep(.gt-cost-head-col--goods) { border-bottom-color: #67c23a; }
.gt-cost-table-wrap :deep(.gt-cost-head-col--pre-price) { border-bottom-color: #95d475; }
.gt-cost-table-wrap :deep(.gt-cost-head-col--bill) { border-bottom-color: #909399; }
.gt-cost-table-wrap :deep(.gt-cost-head-col--tax) { border-bottom-color: #e6a23c; }
.gt-cost-table-wrap :deep(.gt-cost-head-col--source-co) { border-bottom-color: #13c2c2; }
@@ -786,10 +1032,28 @@ onBeforeUnmount(() => {
.gt-cost-table-wrap :deep(.gt-cost-head-col--fence) { border-bottom-color: #f56c6c; }
.gt-cost-table-wrap :deep(.wops-col--wh) {
justify-content: flex-start;
align-items: center;
padding-left: 6px;
padding-right: 4px;
overflow: hidden;
}
.gt-cost-table-wrap :deep(.wops-col--goods) {
padding-left: 6px;
.gt-cost-table-wrap :deep(.wops-flex-head .wops-col--wh) {
justify-content: flex-start;
text-align: left;
}
.gt-cost-table-wrap :deep(.wops-col--wh > .el-tooltip) {
display: block;
flex: 1 1 0;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.gt-cost-table-wrap :deep(.wops-col--wh .el-tooltip__trigger) {
display: block;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--bill),
.gt-cost-table-wrap :deep(.wops-flex-head .gt-cost-head-col--bill) {
@@ -848,24 +1112,27 @@ onBeforeUnmount(() => {
justify-content: center;
text-align: center;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--int-tax),
.gt-cost-table-wrap :deep(.wops-flex-head .gt-cost-head-col--int-tax) {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
overflow: visible;
text-overflow: clip;
}
.gt-cost-table-wrap :deep(.gt-cost-col--internal-tax) {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--min-price) {
.gt-cost-table-wrap :deep(.wops-flex-head .gt-cost-head-col--pre-price),
.gt-cost-table-wrap :deep(.wops-flex-head .gt-cost-head-col--min-price) {
padding-left: 4px;
padding-right: 4px;
padding-right: 2px;
text-align: right;
overflow: visible;
text-overflow: clip;
}
.gt-cost-table-wrap :deep(.gt-cost-head-col--int-tax),
.gt-cost-table-wrap :deep(.wops-flex-head .gt-cost-head-col--int-tax) {
display: flex;
align-items: center;
justify-content: center;
padding-left: 0;
padding-right: 0;
text-align: center;
overflow: visible;
text-overflow: clip;
@@ -880,6 +1147,7 @@ onBeforeUnmount(() => {
.gt-cost-table-wrap :deep(.is-cols-cost-fence .wops-col--num) {
padding-right: 6px;
}
.gt-cost-table-wrap :deep(.is-cols-cost-fence .gt-cost-col--pre-price),
.gt-cost-table-wrap :deep(.is-cols-cost-fence .gt-cost-col--input-tax),
.gt-cost-table-wrap :deep(.is-cols-cost-fence .gt-cost-col--internal-tax),
.gt-cost-table-wrap :deep(.is-cols-cost-fence .gt-cost-col--tax-rate),
+2 -2
View File
@@ -1,4 +1,4 @@
{
"buildId": "20260717010",
"generatedAt": "2026-07-17T13:14:15.818Z"
"buildId": "20260718006",
"generatedAt": "2026-07-18T02:58:25.837Z"
}
+76 -1
View File
@@ -41,18 +41,93 @@
*/
/** 应用版本(如 0.7.040 → 侧栏 v0.7,弹窗 v0.7.040) */
export const APP_VERSION = '0.7.138'
export const APP_VERSION = '0.7.146'
/** @type {ChangelogDay[]} */
export const APP_CHANGELOG = [
{
date: '2026-07-17',
items: [
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」保存成本价上限 `POST /api/warehouse/rdc/set_cost_price_bound` 改传 `org_type`(1 一般纳税人 / 2 小规模 / 3 收据组织)+ `cost_price_bound`(该行围栏值),与 Tab 一一对应。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」成本价上限按纳税身份 Tab 拆为三字段:`general_cost_price_bound`(一般纳税人)、`small_cost_price_bound`(小规模)、`individual_cost_price_bound`(收据);勾选/置灰读当前 Tab 对应值,各 Tab 独立互不影响。',
},
{
icon: 'mdi:fence',
kind: 'add',
text: '「物料弹窗 → 成本围栏」勾选行对接 `POST /api/warehouse/rdc/set_cost_price_bound`(`warehouse_code` + `goods_code` + `org_type` + 该行围栏值);保存成功后本地更新当前 Tab 成本价上限,不重新拉列表。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」按定价表三字段成本价上限驱动勾选与置灰:仅当对应值 >0 生效(默认 0 忽略);围栏值等于上限打勾,大于上限变灰,低于不变;打开页签时若已有上限同步展示。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」单选勾选列移至表末(围栏值之后);勾选后不可取消,只能换选其他行;「供应商组织名称」「源采购组织名称」列宽恢复。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」围栏值高于成本价上限的行置灰时,组织名称等 `.wops-name` 深蓝色字段同步变灰;样式限定在本表内,不影响其他页。',
},
{
icon: 'mdi:fence',
kind: 'add',
text: '「物料弹窗 → 成本围栏」列表新增单选勾选列(方形圆角、蓝底白勾,与物料列表一致);同一时刻仅可选一行。',
},
{
icon: 'mdi:fence',
kind: 'fix',
text: '「物料弹窗 → 成本围栏」修复表头在滚动容器外、表体在内导致纵向滚动条挤窄数据区、列 grid 错位(「源采购 RDC」等列最明显);表头移入同一滚动区并 sticky 吸顶,滚动与列对齐恢复正常。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」「源采购 RDC」文本过长省略时悬停 tooltip 显示全称;表头「商品预估成本」「内部交易最低价」右对齐,「内部交易进项税率」居中。',
},
{
icon: 'mdi:fence',
kind: 'add',
text: '「物料弹窗 → 成本围栏」新增「商品预估成本」列,读 `pre_price`(元),位于「供应商组织名称」之后。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」列宽微调:「供应商组织名称」「源采购组织名称」各略收窄,让给「商品预估成本」,表头「商品预估成本」不换行。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」表格去掉「商品名称」列;当前围栏数据去重后的商品名改显于弹窗顶栏「物料编码」后(多个以顿号连接,仅成本围栏页签时展示);省出列宽加给「供应商组织名称」「源采购组织名称」。',
},
{
icon: 'mdi:fence',
kind: 'change',
text: '「物料弹窗 → 成本围栏」「供应商集团名称」「供应商组织名称」「源采购组织名称」文本过长省略时,悬停 tooltip 显示全称。',
},
{
icon: 'mdi:fence',
kind: 'fix',
text: '「物料弹窗 → 成本围栏」修复定价表行 key 仅用 bloc+供应商编码致重复、切换 Tab 后 DOM 堆叠行数虚增;表头/数据区 flex 布局对齐仓库运维列表,数据超出可视区时出现纵向滚动。',
},
{
icon: 'mdi:folder-cog-outline',
kind: 'fix',
text: '「录入商品 → 本集合商品」进项税率(专票):后端 `collection/goods/listed`(及 info 内嵌商品)的 `bills[]` 补回财务票据 `type`(1 一般纳税人法定增值税率 / 2 出口退税率 / 3 小规模征收率)与 `tax`;前端按集合推导票种 + 供货方组织类型匹配 `type=1|3` 显示税率(普票仍 0%、收据仍 /)。',
},
{
icon: 'mdi:format-list-bulleted',
kind: 'fix',
text: '「物料列表 → 物流配置」修复物流指数等常量显示 —:打开/切换物料时拉常量补全全局页签(finance/warehouse/cost/logistics)groups,不再只扫分类页签与选配。',
},
{
icon: 'mdi:truck-delivery-outline',
kind: 'fix',
+4 -3
View File
@@ -140,9 +140,9 @@
.wops-flex-table.is-cols-wh-labor-cost {
--wops-cols: 8fr 10fr 8fr 10fr 7fr 9fr 7fr 10fr 7fr;
}
/* 物料成本围栏(11 列):企业类型并入供应商/源采购组织名称(名称·类型) */
/* 物料成本围栏(12 列):末列单选;商品名称提至弹窗顶栏;企业类型并入供应商/源采购组织名称 */
.wops-flex-table.is-cols-cost-fence {
--wops-cols: 10fr 12fr 10fr minmax(3.2em, 4fr) minmax(5.4em, 5.5fr) 12fr minmax(6.4em, 7.2fr) minmax(4.2em, 5.5fr) minmax(8.5em, 9fr) 9fr minmax(6.5em, 11fr);
--wops-cols: 10fr 16fr minmax(5.5em, 8.5fr) minmax(3.2em, 4fr) minmax(5.4em, 5.5fr) 16fr minmax(6.4em, 7.2fr) minmax(4.2em, 5.5fr) minmax(8.5em, 9fr) 9fr minmax(6.5em, 11fr) minmax(28px, 2.5fr);
}
/* 特殊定价 · 批量值按票据(3 列:票据 / 超额绩效 / 好处费) */
@@ -371,7 +371,8 @@
}
.wops-col--code,
.wops-col--op {
.wops-col--op,
.wops-col--sel {
display: flex;
align-items: center;
justify-content: center;
+36 -2
View File
@@ -126,6 +126,7 @@
<GlobalTabCostPanel
:goods-code="materialEditingId || ''"
:rdc-list="materialWarehouseRdcs"
@fence-goods-names-change="onCostFenceGoodsNamesChange"
/>
</div>
</el-collapse-item>
@@ -576,6 +577,10 @@
<template v-else-if="materialDialogMode === 'add'">新增物料</template>
<template v-else>
<span class="mf-dialog-top-code-label">物料编码:</span><span class="mf-dialog-top-code-value">{{ materialEditingId }}</span>
<template v-if="mlActiveNavKey === ML_NAV_COST && costFenceGoodsNames.length">
<span class="mf-dialog-top-goods-sep">·</span>
<span class="mf-dialog-top-goods-names">{{ costFenceGoodsNames.join('、') }}</span>
</template>
</template>
</div>
<div class="form-dialog-mobile-header">
@@ -585,7 +590,9 @@
<span class="mobile-dialog-title">
<template v-if="batchSelMode">批量设置选配</template>
<template v-else-if="materialDialogMode === 'add'">新增物料</template>
<template v-else>物料编码:{{ materialEditingId }}</template>
<template v-else>
物料编码:{{ materialEditingId }}<template v-if="mlActiveNavKey === ML_NAV_COST && costFenceGoodsNames.length"> · {{ costFenceGoodsNames.join('、') }}</template>
</template>
</span>
<el-button type="primary" size="small" class="mobile-save-btn" :loading="materialDialogSubmitting" :disabled="materialForm.categories.length === 0" @click="submitMaterial">保存</el-button>
</div>
@@ -652,6 +659,7 @@
<GlobalTabCostPanel
:goods-code="materialEditingId || ''"
:rdc-list="materialWarehouseRdcs"
@fence-goods-names-change="onCostFenceGoodsNamesChange"
/>
</div>
<template v-for="(cat, catIdx) in materialForm.categories" :key="`${cat.category_code}-${catIdx}`">
@@ -6300,6 +6308,10 @@ function initSelSortables() {
// 注意:不要监听 components.length —— 拖拽 onEnd 里 splice 时会同步触发 watch,
// 在 Sortable 回调尚未结束就 destroy 实例,易导致界面卡死。
watch(materialDialogVisible, (visible) => {
if (!visible) costFenceGoodsNames.value = []
})
watch([batchSelMode, materialDialogVisible, activeCatCode], () => {
if (selEditMode.value && materialDialogVisible.value) nextTick(() => initSelSortables())
}, { flush: 'post' })
@@ -6683,6 +6695,14 @@ const ML_NAV_BASIC = '__ml_nav_basic__'
const ML_NAV_GTAB_PREFIX = '__ml_nav_gtab__:'
const mlActiveNavKey = ref(ML_NAV_FINANCE)
/** 成本围栏当前表格去重后的商品名称(供弹窗顶栏展示) */
const costFenceGoodsNames = ref([])
function onCostFenceGoodsNamesChange(names) {
costFenceGoodsNames.value = Array.isArray(names)
? names.map((n) => String(n ?? '').trim()).filter(Boolean)
: []
}
const ML_SLOT_NAV_META = [
{ key: ML_NAV_FINANCE, storageKey: 'finance', label: '财务管理' },
@@ -6913,13 +6933,16 @@ const mfAssemblyFormulaValueMap = computed(() => {
return merged
})
/** 物料编辑:预拉当前物料全部分类页签 + 选配内的 config_constant(catValueMaps 跨 Nav 引用) */
/** 物料编辑:预拉当前物料全部分类页签 + 全局页签 + 选配内的 config_constant(catValueMaps 跨 Nav 引用) */
function mlCollectMaterialConstantTabGroups() {
/** @type {object[]} */
const merged = []
const pushGroups = (groups) => {
if (Array.isArray(groups) && groups.length) merged.push(...groups)
}
for (const tab of globalTabRuntimeList.value || []) {
pushGroups(tab?.groups)
}
for (const cat of materialForm.categories) {
for (const tab of catTabListsMap.value[cat.category_code] || []) {
pushGroups(tab?.groups)
@@ -10821,6 +10844,16 @@ provide('mlTabPaneCtx', reactive({
font-size: 15px;
color: #303133;
}
.mf-dialog-top-goods-sep {
margin: 0 8px;
color: #c0c4cc;
font-weight: 400;
}
.mf-dialog-top-goods-names {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.mf-main-split {
flex: 1;
@@ -13149,6 +13182,7 @@ provide('mlTabPaneCtx', reactive({
}
.ml-compact-mf-pane--cost :deep(.gt-cost-layout) {
min-width: 720px;
min-height: 420px;
}
.ml-compact-mf-pane--basic :deep(.mf-detail-table) {
overflow-x: auto;