Merge branch 'test' into feauture-20261104

# Conflicts:
#	bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcOrderBillMapper.java
This commit is contained in:
liuminghui
2026-11-09 15:37:55 +08:00
46 changed files with 2238 additions and 83 deletions
+15
View File
@@ -0,0 +1,15 @@
import request from '@/utils/request'
export const listArProfitBills = params => request({ url: '/report/ar-profit/bills', method: 'get', params })
export const listArProfitBillDetails = (billId, params) => request({ url: `/report/ar-profit/bills/${billId}/details`, method: 'get', params })
export const listArProfitSummary = params => request({ url: '/report/ar-profit/summary', method: 'get', params })
export const listArProfitSalesOptions = params => request({ url: '/report/ar-profit/sales-options', method: 'get', params })
export const listArProfitAreaRules = () => request({ url: '/report/ar-profit/area-rules', method: 'get' })
export const listArProfitFees = detailId => request({ url: `/report/ar-profit/orders/${detailId}/fees`, method: 'get' })
export const listArProfitRates = () => request({ url: '/report/ar-profit/rates', method: 'get' })
export const addArProfitRate = data => request({ url: '/report/ar-profit/rates', method: 'post', data })
export const listArProfitMissingRates = params => request({ url: '/report/ar-profit/writeoffs/missing-rates', method: 'get', params })
export const confirmArProfitMissingRate = data => request({ url: '/report/ar-profit/writeoffs/confirm-rate', method: 'post', data })
export const listArProfitCommissions = params => request({ url: '/report/ar-profit/commissions', method: 'get', params })
export const listArProfitCommissionSummary = params => request({ url: '/report/ar-profit/commissions/summary', method: 'get', params })
export const listArProfitWriteoffRates = billId => request({ url: `/report/ar-profit/commissions/${billId}/writeoffs`, method: 'get' })
@@ -73,8 +73,8 @@
<el-table-column :prop="dateColumnProp" :label="dateColumnLabel" width="170" :formatter="formatDateColumn" />
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
<el-table-column prop="countryName" label="国家/地区" min-width="110" show-overflow-tooltip />
<el-table-column prop="actualWeight" label="实际重(kg)" width="105" align="right" :formatter="formatNumberColumn" />
<el-table-column prop="chargeWeight" label="计费重(kg)" width="105" align="right" :formatter="formatNumberColumn" />
<el-table-column prop="actualWeight" label="实际重(kg)" width="105" align="right" :formatter="formatWeightColumn" />
<el-table-column prop="chargeWeight" label="计费重(kg)" width="105" align="right" :formatter="formatWeightColumn" />
<el-table-column prop="incomeTotal" label="应收金额" width="110" align="right" :formatter="formatNumberColumn" />
<el-table-column prop="currency" label="币种" width="75" />
</el-table>
@@ -541,6 +541,7 @@ function date(value) {
}
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
function formatDateColumn(_, __, value) { return date(value) }
function formatWeightColumn(_, __, value) { return value == null || value === '' ? '-' : value }
function formatNumberColumn(_, __, value) { return Number(value || 0).toFixed(2) }
function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${Date.now()}-${Math.random().toString(16).slice(2)}` }
</script>
+13 -13
View File
@@ -626,19 +626,6 @@ const searchColumns = [
collapseTagsTooltip: true
}
},
{
label: '\u8ba1\u8d39\u5931\u8d25',
prop: 'billingFailStatuses',
component: 'select',
options: billingFailStatusOptions,
placeholder: '\u8bf7\u9009\u62e9',
props: {
multiple: true,
clearable: true,
collapseTags: true,
collapseTagsTooltip: true
}
},
{
label: '计费时间',
prop: 'bizDateRange',
@@ -659,6 +646,19 @@ const searchColumns = [
clearable: true,
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
}
},
{
label: '\u8ba1\u8d39\u5931\u8d25',
prop: 'billingFailStatuses',
component: 'select',
options: billingFailStatusOptions,
placeholder: '\u8bf7\u9009\u62e9',
props: {
multiple: true,
clearable: true,
collapseTags: true,
collapseTagsTooltip: true
}
}
]
@@ -448,8 +448,8 @@ function onCreateVersion() {
function onRemoveVersion(row) {
if (!row || row.psvId == null) return
const msg = isEffectVersion(row)
? "确认软删除已生效版本(生效时间:" + (row.effectiveTime || '未设置') + ")?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?其明细与附加费将一并删除。"
? "确认删除该版本(生效时间:" + (row.effectiveTime || '未设置') + ")?若订单已引用该版本则仅软删版本记录;若无订单引用则整版本硬删。产品与明细均会物理删除,且上一版将自动恢复生效。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?若订单已引用该版本则仅软删版本记录;若无订单引用则整版本硬删。产品与明细均会物理删除。"
proxy.$modal.confirm(msg).then(() => {
removingPsvId.value = row.psvId
proxy.$modal.loading("正在删除,请稍候...")
@@ -36,23 +36,23 @@ export function createExclusiveModule(api) {
exclusiveQuote: true,
onlyImportFresh: true,
routes: Object.freeze({
list: '/exclusive/quote',
create: '/exclusive/quoteCreate',
detail: '/exclusive/quoteDetail'
list: '/customer/quote',
create: '/customer/quoteCreate',
detail: '/customer/quoteDetail'
}),
labels: Object.freeze({
entity: '专属报价',
entityCode: '报价编码',
entityName: '报价名称',
createButton: '新建报价',
saveProgressTitle: '正在保存专属报价',
savedSuccess: '专属报价已保存',
noActiveVersion: '该专属报价暂无生效版本',
disableConfirm: '停用后该专属报价不再参与计费命中,确认停用【{name}】?',
enableConfirm: '启用后该专属报价将重新参与计费命中,确认启用【{name}】?',
basicEditTitle: '修改专属报价基本信息',
keywordPlaceholder: '报价编码 / 名称',
detailTitleFallback: '专属报价详情'
entity: '专属套餐',
entityCode: '套餐编码',
entityName: '套餐名称',
createButton: '新建专属套餐',
saveProgressTitle: '正在保存专属套餐',
savedSuccess: '专属套餐已保存',
noActiveVersion: '该专属套餐暂无生效版本',
disableConfirm: '停用后该专属套餐不再参与计费命中,确认停用【{name}】?',
enableConfirm: '启用后该专属套餐将重新参与计费命中,确认启用【{name}】?',
basicEditTitle: '修改专属套餐基本信息',
keywordPlaceholder: '套餐编码 / 名称',
detailTitleFallback: '专属套餐详情'
})
})
}
+2 -2
View File
@@ -465,8 +465,8 @@ function onCreateVersion() {
function onRemoveVersion(row) {
if (!row || row.psvId == null) return
const msg = isEffectVersion(row)
? "确认软删除已生效版本(生效时间:" + (row.effectiveTime || '未设置') + ")?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?其明细与附加费将一并删除。"
? "确认删除该版本(生效时间:" + (row.effectiveTime || '未设置') + ")?若订单已引用该版本则仅软删版本记录;若无订单引用则整版本硬删。产品与明细均会物理删除,且上一版将自动恢复生效。"
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?若订单已引用该版本则仅软删版本记录;若无订单引用则整版本硬删。产品与明细均会物理删除。"
proxy.$modal.confirm(msg).then(() => {
removingPsvId.value = row.psvId
proxy.$modal.loading("正在删除,请稍候...")
@@ -0,0 +1,283 @@
<template>
<div class="app-container ar-profit-report">
<el-card shadow="never">
<el-form :model="query" inline @submit.prevent>
<el-form-item label="账单日期">
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item label="客户">
<customer-select v-model="query.customerId" :fetch-options="listArBillCustomerOptions"
placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" />
</el-form-item>
<el-form-item label="销售员">
<el-select v-model="query.salesUserId" placeholder="姓名或工号搜索" clearable filterable remote
:remote-method="searchSales" :loading="salesLoading" style="width: 180px"
@input="handleSalesInput">
<el-option v-for="person in salesOptions" :key="person.userId" :label="person.name || person.code || String(person.userId)" :value="person.userId">
<span>{{ person.name || person.code || person.userId }}</span>
<span v-if="person.code" class="sales-code">{{ person.code }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="账单号"><el-input v-model="query.billNo" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="订单号"><el-input v-model="query.orderNo" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部状态" clearable style="width: 140px">
<el-option label="已确认" value="CONFIRMED" /><el-option label="部分核销" value="PARTIAL_WRITTEN_OFF" /><el-option label="已核销" value="WRITTEN_OFF" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" icon="Search" @click="search">搜索</el-button><el-button icon="Refresh" @click="reset">重置</el-button></el-form-item>
</el-form>
</el-card>
<el-card v-for="summary in summaryCards" :key="summary.currency || 'empty'" shadow="never" class="summary-card">
<template #header>{{ summary.currency ? summary.currency + ' 汇总' : '汇总' }}</template>
<div class="metrics">
<div>账单数 <b>{{ summary.billCount ?? '-' }}</b></div>
<div>销售价 <b>{{ money(summary.salesAmount) }}</b></div><div>产品价 <b>{{ money(summary.productAmount) }}</b></div>
<div>销售毛利 <b>{{ money(summary.salesGrossProfit) }}</b></div>
<div>销售毛利率 <b>{{ rate(summary.salesGrossMarginRate) }}</b></div>
<div>账单已核销 <b>{{ money(summary.writtenOffAmount) }}</b></div><div>账单未核销 <b>{{ money(summary.unwrittenAmount) }}</b></div>
<div>毛利待核对账单 <b>{{ summary.invalidBillCount ?? '-' }}</b></div>
</div>
<div class="summary-note">广州产品价按红线价展示,深圳产品价按产品价展示;待核对账单不计入产品价、毛利及毛利率汇总。</div>
<div v-if="query.orderNo" class="summary-note">订单号用于查找账单,金额按完整账单统计。</div>
</el-card>
<el-card shadow="never" class="data-card">
<div class="heading">
<span class="table-title">账单列表</span>
<div>
<el-button v-hasPermi="['report:arProfit:export']" icon="Download" @click="exportRows">导出账单</el-button>
<el-button v-hasPermi="['report:arProfit:commission']" @click="openCommission()">提成管理</el-button>
</div>
</div>
<el-table v-loading="loading" :data="rows" border stripe>
<el-table-column prop="billNo" label="账单号" min-width="170" show-overflow-tooltip />
<el-table-column prop="billDate" label="账单日期" width="115" />
<el-table-column prop="customerCode" label="客户代码" width="110" />
<el-table-column prop="customerName" label="客户名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="salesUserName" label="销售员" width="110" />
<el-table-column label="归属来源" width="110"><template #default="{ row }">{{ row.snapshotSource === 'NEW' ? '生成时快照' : row.snapshotSource === 'BACKFILLED' ? '历史补录' : '-' }}</template></el-table-column>
<el-table-column prop="currency" label="币种" width="80" />
<el-table-column label="销售价" min-width="110" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
<el-table-column label="产品价" min-width="110" align="right"><template #default="{ row }">{{ money(row.productAmount) }}</template></el-table-column>
<el-table-column label="销售毛利" min-width="110" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
<el-table-column label="销售毛利率" width="110" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
<el-table-column label="已核销" min-width="110" align="right"><template #default="{ row }">{{ money(row.writtenOffAmount) }}</template></el-table-column>
<el-table-column label="未核销" min-width="110" align="right"><template #default="{ row }">{{ money(row.unwrittenAmount) }}</template></el-table-column>
<el-table-column label="状态" width="110"><template #default="{ row }">{{ statusLabel(row.status) }}</template></el-table-column>
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openBillDetails(row)">账单明细</el-button><el-button v-hasPermi="['report:arProfit:commission']" link type="primary" @click="openBillCommission(row)">查看提成</el-button><el-button v-hasPermi="['report:arProfit:commission']" link type="primary" @click="openWriteoffRates(row)">核销比例</el-button></template></el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" @pagination="loadRows" />
</el-card>
<el-dialog v-model="detailsVisible" :title="'账单明细:' + detailBillNo" width="900px" append-to-body>
<el-table :data="details" border v-loading="detailsLoading">
<el-table-column prop="orderNo" label="订单号" min-width="160" />
<el-table-column prop="productName" label="产品名称" min-width="150" />
<el-table-column prop="currency" label="币种" width="80" />
<el-table-column label="销售价" align="right"><template #default="{ row }">{{ money(row.totalAmount) }}</template></el-table-column>
<el-table-column label="操作" width="100"><template #default="{ row }"><el-button link type="primary" @click="openFees(row)">费用明细</el-button></template></el-table-column>
</el-table>
<pagination v-show="detailsTotal > 0" v-model:page="detailsPage" v-model:limit="detailsSize" :total="detailsTotal" @pagination="loadBillDetails" />
</el-dialog>
<el-dialog v-model="feeVisible" title="本次出账费用明细" width="800px" append-to-body>
<el-table :data="fees" border v-loading="feeLoading">
<el-table-column prop="feeName" label="费用名称" min-width="140" /><el-table-column prop="currency" label="币种" width="80" />
<el-table-column label="销售价" align="right"><template #default="{ row }">{{ money(row.amount) }}</template></el-table-column>
<el-table-column label="产品价" align="right"><template #default="{ row }">{{ money(feeProductAmount(row)) }}</template></el-table-column>
</el-table>
</el-dialog>
<el-dialog v-model="commissionVisible" title="提成管理" width="1100px" append-to-body>
<el-tabs v-model="commissionTab" @tab-change="loadCommissionTab">
<el-tab-pane label="正式提成" name="results">
<el-alert v-if="commissionBillId" :title="'当前查看账单:' + commissionBillNo" type="info" :closable="false" show-icon>
<el-button link type="primary" @click="clearCommissionBill">查看全部账单</el-button>
</el-alert>
<div v-if="commissionBillId" class="summary-note">账单全部核销后生成正式提成;存在待处理明细时不展示整单提成合计。</div>
<el-button icon="Download" style="margin-bottom: 12px" @click="exportCommissions">导出提成明细</el-button>
<el-checkbox v-model="includeReversed" style="margin: 0 0 12px 12px" @change="loadCommissionTab">显示已冲回记录</el-checkbox>
<el-table :data="commissionSummary" border size="small" style="margin-bottom: 16px">
<el-table-column prop="salesUserName" label="销售员" /><el-table-column prop="currency" label="币种" width="80" />
<el-table-column prop="orderCount" label="订单明细数" /><el-table-column prop="pendingCount" label="待处理订单数" />
<el-table-column label="正式提成合计" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
</el-table>
<el-table :data="results" border size="small" v-loading="commissionLoading">
<el-table-column prop="billNo" label="账单号" min-width="160" /><el-table-column prop="orderNo" label="订单号" min-width="150" />
<el-table-column prop="salesUserName" label="销售员" width="110" /><el-table-column prop="currency" label="币种" width="75" />
<el-table-column label="销售毛利" width="110" align="right"><template #default="{ row }">{{ money(row.grossProfit) }}</template></el-table-column>
<el-table-column label="正式提成" width="110" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
<el-table-column label="状态" min-width="120"><template #default="{ row }">{{ resultLabel(row.resultStatus) }}</template></el-table-column>
<el-table-column v-if="includeReversed" prop="reversedBy" label="冲回人" width="110" />
<el-table-column v-if="includeReversed" label="冲回时间" width="170"><template #default="{ row }">{{ row.reversedTime ? parseTime(row.reversedTime) : '-' }}</template></el-table-column>
<el-table-column label="依据" width="100"><template #default="{ row }"><el-button link type="primary" @click="openWriteoffRates(row)">核销比例</el-button></template></el-table-column>
</el-table>
<pagination v-show="commissionTotal > 0" v-model:page="commissionPage" v-model:limit="commissionSize" :total="commissionTotal" @pagination="loadCommissionTab" />
</el-tab-pane>
<el-tab-pane label="提成比例" name="rates">
<el-form inline><el-form-item label="销售员"><user-select v-model="rateForm.salesUserId" placeholder="选择销售员" style="width: 200px" /></el-form-item>
<el-form-item label="比例(%)"><el-input-number v-model="rateForm.rate" :min="0" :max="100" :precision="4" /></el-form-item>
<el-form-item><el-button type="primary" @click="saveRate">立即生效</el-button></el-form-item>
</el-form>
<el-table :data="rates" border size="small" height="400" v-loading="commissionLoading">
<el-table-column prop="salesUserName" label="销售员" /><el-table-column prop="salesUserCode" label="销售员编码" />
<el-table-column prop="rate" label="比例(%)" /><el-table-column prop="effectiveAt" label="生效时间" /><el-table-column prop="createBy" label="操作人" />
</el-table>
</el-tab-pane>
<el-tab-pane label="比例待确认" name="missing">
<el-table :data="missingRates" border size="small" v-loading="commissionLoading">
<el-table-column prop="billNo" label="账单号" min-width="150" /><el-table-column prop="customerName" label="客户" min-width="140" />
<el-table-column prop="salesUserName" label="销售员" width="110" /><el-table-column prop="writeoffAmount" label="核销金额" width="110" />
<el-table-column prop="writeoffDate" label="核销日期" width="165"><template #default="{ row }">{{ parseTime(row.writeoffDate, '{y}-{m}-{d}') }}</template></el-table-column>
<el-table-column label="操作" width="140"><template #default="{ row }"><el-button v-if="row.rate === null || row.rate === undefined" link type="primary" @click="openConfirm(row)">确认比例</el-button><span v-else>比例异常,请核对</span></template></el-table-column>
</el-table>
<pagination v-show="missingTotal > 0" v-model:page="missingPage" v-model:limit="missingSize" :total="missingTotal" @pagination="loadCommissionTab" />
</el-tab-pane>
</el-tabs>
</el-dialog>
<el-dialog v-model="confirmVisible" title="确认历史核销比例" width="480px" append-to-body>
<el-form label-width="100px"><el-form-item label="账单号">{{ confirmForm.billNo }}</el-form-item>
<el-form-item label="比例(%)"><el-input-number v-model="confirmForm.rate" :min="0" :max="100" :precision="4" /></el-form-item>
<el-form-item label="核对说明"><el-input v-model="confirmForm.remark" type="textarea" maxlength="500" /></el-form-item>
</el-form>
<template #footer><el-button @click="confirmVisible = false">取消</el-button><el-button type="primary" @click="confirmRate">确认</el-button></template>
</el-dialog>
<el-dialog v-model="writeoffRatesVisible" title="账单核销与提成比例" width="950px" append-to-body>
<el-table :data="writeoffRates" border size="small" v-loading="writeoffRatesLoading">
<el-table-column prop="writeoffDate" label="核销日期" width="130"><template #default="{ row }">{{ parseTime(row.writeoffDate, '{y}-{m}-{d}') }}</template></el-table-column>
<el-table-column prop="writeoffAmount" label="核销金额" width="120" /><el-table-column prop="rate" label="比例(%)" width="110" />
<el-table-column label="状态" width="100"><template #default="{ row }">{{ row.status === 'EFFECTIVE' ? '有效' : '已反核销' }}</template></el-table-column>
<el-table-column prop="confirmedBy" label="补录人" width="110" /><el-table-column prop="remark" label="核对说明" min-width="180" show-overflow-tooltip />
</el-table>
</el-dialog>
</div>
</template>
<script setup name="ArProfitReport">
import { listArBillCustomerOptions } from '@/api/ar/bill'
import { addArProfitRate, confirmArProfitMissingRate, listArProfitAreaRules, listArProfitBillDetails, listArProfitBills, listArProfitCommissions, listArProfitCommissionSummary, listArProfitFees, listArProfitMissingRates, listArProfitRates, listArProfitSalesOptions, listArProfitSummary, listArProfitWriteoffRates } from '@/api/report/arProfit'
import { parseTime } from '@/utils/bms'
import { ElMessage } from 'element-plus'
const { proxy } = getCurrentInstance()
const query = reactive({ customerId: null, salesUserId: null, billNo: '', orderNo: '', status: '' })
const salesOptions = ref([])
const salesLoading = ref(false)
let salesSearchVersion = 0
const areaRules = ref({})
const dateRange = ref([])
const pageNum = ref(1), pageSize = ref(10), total = ref(0), rows = ref([]), loading = ref(false)
const summaries = ref([]), summaryLoaded = ref(false)
const detailsVisible = ref(false), detailsLoading = ref(false), details = ref([]), detailsTotal = ref(0)
const detailBillId = ref(null), detailBillNo = ref(''), detailsPage = ref(1), detailsSize = ref(10)
let detailsVersion = 0
const feeVisible = ref(false), feeLoading = ref(false), feeArea = ref(''), fees = ref([])
const summaryCards = computed(() => !summaryLoaded.value ? [] : summaries.value.length ? summaries.value : [{}])
let requestVersion = 0
let commissionVersion = 0
const commissionVisible = ref(false), commissionTab = ref('results'), commissionLoading = ref(false)
const commissionBillId = ref(null), commissionBillNo = ref('')
const results = ref([]), commissionTotal = ref(0), commissionPage = ref(1), commissionSize = ref(10)
const commissionSummary = ref([])
const includeReversed = ref(false)
const rates = ref([]), rateForm = reactive({ salesUserId: null, rate: null })
const missingRates = ref([]), missingTotal = ref(0), missingPage = ref(1), missingSize = ref(10)
const confirmVisible = ref(false), confirmForm = reactive({ writeoffId: null, billNo: '', rate: null, remark: '' })
const writeoffRatesVisible = ref(false), writeoffRatesLoading = ref(false), writeoffRates = ref([])
function params() { return { ...query, beginBillDate: dateRange.value?.[0], endBillDate: dateRange.value?.[1] } }
function search() { pageNum.value = 1; loadRows(); loadSummary() }
function reset() { Object.assign(query, { customerId: null, salesUserId: null, billNo: '', orderNo: '', status: '' }); dateRange.value = []; search() }
function handleSalesInput() {
salesSearchVersion++
salesOptions.value = []
salesLoading.value = false
}
async function searchSales(keyword) {
const version = ++salesSearchVersion
salesOptions.value = []
salesLoading.value = true
try {
const res = await listArProfitSalesOptions({ keyword: keyword?.trim() || '' })
if (version !== salesSearchVersion) return
salesOptions.value = res.data || []
} finally {
if (version === salesSearchVersion) salesLoading.value = false
}
}
function loadRows() {
const version = ++requestVersion
loading.value = true
rows.value = []
total.value = 0
listArProfitBills({ ...params(), pageNum: pageNum.value, pageSize: pageSize.value }).then(res => {
if (version === requestVersion) { rows.value = res.rows || []; total.value = res.total || 0 }
}).finally(() => { if (version === requestVersion) loading.value = false })
}
let summaryVersion = 0
function loadSummary() {
const version = ++summaryVersion
summaryLoaded.value = false
listArProfitSummary(params()).then(res => { if (version === summaryVersion) { summaries.value = res.data || []; summaryLoaded.value = true } })
}
function openBillDetails(row) { detailBillId.value = row.arBillId; detailBillNo.value = row.billNo; feeArea.value = row.customerArea; detailsPage.value = 1; detailsVisible.value = true; loadBillDetails() }
async function loadBillDetails() {
const version = ++detailsVersion
detailsLoading.value = true
details.value = []
detailsTotal.value = 0
try { const res = await listArProfitBillDetails(detailBillId.value, { pageNum: detailsPage.value, pageSize: detailsSize.value }); if (version === detailsVersion) { details.value = res.rows || []; detailsTotal.value = res.total || 0 } }
finally { if (version === detailsVersion) detailsLoading.value = false }
}
async function openFees(row) { feeVisible.value = true; feeLoading.value = true; fees.value = []; try { fees.value = (await listArProfitFees(row.detailId)).data || [] } finally { feeLoading.value = false } }
function exportRows() { proxy.download('/report/ar-profit/export', params(), `应收账单毛利分析_${Date.now()}.xlsx`) }
function exportCommissions() { proxy.download('/report/ar-profit/commissions/export', { billId: commissionBillId.value }, `应收账单提成明细_${Date.now()}.xlsx`) }
function money(v) { return v === null || v === undefined || v === '' || !Number.isFinite(Number(v)) ? '-' : Number(v).toFixed(2) }
function rate(v) { return v === null || v === undefined || v === '' || !Number.isFinite(Number(v)) ? '-' : Number(v).toFixed(2) + '%' }
function feeProductAmount(row) {
if (areaRules.value[feeArea.value] === 'GUANGZHOU') return row.redlineAmount
if (areaRules.value[feeArea.value] === 'SHENZHEN') return row.productAmount
return null
}
function statusLabel(v) { return ({ CONFIRMED: '已确认', PARTIAL_WRITTEN_OFF: '部分核销', WRITTEN_OFF: '已核销' })[v] || v || '-' }
function resultLabel(v) { return ({ READY: '已计算', MISSING_RATE: '比例待确认', MISSING_PRICE: '价格缺失', MISSING_SNAPSHOT: '归属或区域缺失', WRITEOFF_MISMATCH: '核销金额不一致', REVERSED: '已冲回' })[v] || v || '-' }
function openCommission() { commissionBillId.value = null; commissionBillNo.value = ''; commissionPage.value = 1; commissionVisible.value = true; loadCommissionTab() }
function openBillCommission(row) { commissionBillId.value = row.arBillId; commissionBillNo.value = row.billNo; commissionPage.value = 1; commissionTab.value = 'results'; commissionVisible.value = true; loadCommissionTab() }
function clearCommissionBill() { commissionBillId.value = null; commissionBillNo.value = ''; commissionPage.value = 1; loadCommissionTab() }
async function loadCommissionTab() {
const version = ++commissionVersion
const tab = commissionTab.value
const billId = commissionBillId.value
commissionLoading.value = true
if (tab === 'results') { results.value = []; commissionSummary.value = []; commissionTotal.value = 0 }
try {
if (tab === 'rates') { const r = await listArProfitRates(); if (version === commissionVersion) rates.value = r.data || [] }
else if (tab === 'missing') { const r = await listArProfitMissingRates({ pageNum: missingPage.value, pageSize: missingSize.value }); if (version === commissionVersion) { missingRates.value = r.rows || []; missingTotal.value = r.total || 0 } }
else { const [r, summary] = await Promise.all([listArProfitCommissions({ billId, pageNum: commissionPage.value, pageSize: commissionSize.value, includeReversed: includeReversed.value }), listArProfitCommissionSummary({ billId })]); if (version === commissionVersion) { results.value = r.rows || []; commissionTotal.value = r.total || 0; commissionSummary.value = summary.data || [] } }
} finally { if (version === commissionVersion) commissionLoading.value = false }
}
async function saveRate() {
if (!rateForm.salesUserId || rateForm.rate === null) return ElMessage.warning('请选择销售员并填写提成比例')
await addArProfitRate(rateForm); ElMessage.success('提成比例已保存'); loadCommissionTab()
}
function openConfirm(row) { Object.assign(confirmForm, { writeoffId: row.writeoffId, billNo: row.billNo, rate: null, remark: '' }); confirmVisible.value = true }
async function openWriteoffRates(row) { writeoffRatesVisible.value = true; writeoffRatesLoading.value = true; writeoffRates.value = []; try { writeoffRates.value = (await listArProfitWriteoffRates(row.arBillId)).data || [] } finally { writeoffRatesLoading.value = false } }
async function confirmRate() {
if (confirmForm.rate === null || !confirmForm.remark.trim()) return ElMessage.warning('请填写比例及核对说明')
await confirmArProfitMissingRate(confirmForm); confirmVisible.value = false; ElMessage.success('已确认提成比例'); loadCommissionTab()
}
listArProfitAreaRules().then(res => { areaRules.value = res.data || {} })
search()
</script>
<style scoped>
.summary-card, .data-card { margin-top: 14px; }
.metrics { display: flex; flex-wrap: wrap; gap: 14px; }
.metrics > div { min-width: 150px; padding: 10px; background: var(--el-fill-color-lighter); color: var(--el-text-color-secondary); }
.metrics b { display: block; margin-top: 5px; color: var(--el-text-color-primary); }
.summary-note { margin-top: 10px; color: var(--el-text-color-secondary); font-size: 12px; }
.heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.table-title { font-size: 16px; font-weight: 500; }
.sales-code { float: right; margin-left: 16px; color: var(--el-text-color-secondary); font-size: 12px; }
</style>