✨ 新增毛利分析报表及提成设置
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import request, { download } from '@/utils/request'
|
||||
|
||||
export function listProfitSummary(params) {
|
||||
return request({
|
||||
url: '/report/profit/summary',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function listProfitOrders(params) {
|
||||
return request({
|
||||
url: '/report/profit/orders',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function listProfitProducts(params) {
|
||||
return request({
|
||||
url: '/report/profit/products',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function exportProfitOrders(params) {
|
||||
return download('/report/profit/orders/export', params, '订单毛利报表.xlsx')
|
||||
}
|
||||
|
||||
export function exportProfitProducts(params) {
|
||||
return download('/report/profit/products/export', params, '产品毛利报表.xlsx')
|
||||
}
|
||||
|
||||
export function listProfitCommissionRates() {
|
||||
return request({
|
||||
url: '/report/profit/commission-rates',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function saveProfitCommissionRate(data) {
|
||||
return request({
|
||||
url: '/report/profit/commission-rates',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProfitCommissionRate(rateId) {
|
||||
return request({
|
||||
url: '/report/profit/commission-rates/' + rateId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
<template>
|
||||
<div class="app-container profit-report">
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :model="query" label-width="88px" @submit.prevent>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="计费日期" class="filter-item">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
class="filter-control"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="业务员编码" class="filter-item">
|
||||
<el-input v-model="query.belongSalerCode" clearable placeholder="输入业务员编码" class="filter-control" @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="产品代码" class="filter-item">
|
||||
<el-input v-model="query.productCode" clearable placeholder="输入产品代码" class="filter-control" @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="订单号" class="filter-item">
|
||||
<el-input
|
||||
v-model="query.orderNo"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="vertical"
|
||||
clearable
|
||||
placeholder="输入订单号,多个值请换行"
|
||||
class="filter-control multi-line-filter"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="客户代码" class="filter-item">
|
||||
<el-input
|
||||
v-model="query.customerCode"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="vertical"
|
||||
clearable
|
||||
placeholder="输入客户代码,多个值请换行"
|
||||
class="filter-control multi-line-filter"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="客户名称" class="filter-item">
|
||||
<el-input
|
||||
v-model="query.customerName"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="vertical"
|
||||
clearable
|
||||
placeholder="输入客户名称,多个值请换行"
|
||||
class="filter-control multi-line-filter"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8" class="filter-actions-col">
|
||||
<div class="filter-item filter-actions">
|
||||
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div v-if="summaryList.length" class="summary-list">
|
||||
<el-card v-for="summary in summaryList" :key="summary.currency" shadow="never" class="summary-card">
|
||||
<template #header>
|
||||
<span>{{ summary.currency || '未标币种' }} 汇总</span>
|
||||
<span class="summary-subtitle">订单 {{ summary.orderCount || 0 }} 条</span>
|
||||
</template>
|
||||
<el-row :gutter="12" class="summary-grid">
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线价</div><div class="metric-value">{{ money(summary.redlineAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">产品价</div><div class="metric-value">{{ money(summary.costAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">销售价</div><div class="metric-value">{{ money(summary.salesAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线毛利</div><div class="metric-value">{{ money(summary.redlineGrossProfit) }} <small>{{ rate(summary.redlineGrossMarginRate) }}</small></div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">销售毛利</div><div class="metric-value profit-value">{{ money(summary.salesGrossProfit) }} <small>{{ rate(summary.salesGrossMarginRate) }}</small></div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">提成金额</div><div class="metric-value">{{ money(summary.commissionAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">价格缺失订单</div><div class="metric-value">{{ summary.incompleteOrderCount || 0 }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">提成未计算订单</div><div class="metric-value">{{ summary.commissionMissingOrderCount || 0 }}</div></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="table-card">
|
||||
<div class="table-heading">
|
||||
<el-tabs v-model="dimension" @tab-change="handleDimensionChange">
|
||||
<el-tab-pane label="产品维度" name="product" />
|
||||
<el-tab-pane label="订单维度" name="order" />
|
||||
</el-tabs>
|
||||
<div class="table-toolbar">
|
||||
<el-button
|
||||
icon="Setting"
|
||||
v-hasPermi="['report:profit:commission:list']"
|
||||
@click="openCommissionRates"
|
||||
>提成比例设置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['report:profit:export']"
|
||||
:loading="exporting"
|
||||
@click="handleExport"
|
||||
>导出当前维度</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-text-drag-copy="true" class="report-table-copy-area">
|
||||
<el-table :key="dimension" v-loading="loading" :data="rows" border stripe :row-key="rowKey">
|
||||
<template v-if="dimension === 'product'">
|
||||
<el-table-column prop="productCode" label="产品代码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="countryCode" label="国家代码" width="100" align="center" />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="right">
|
||||
<template #default="{ row }"><span class="success-value order-count-link" :class="{ 'is-disabled': !row.orderCount }" @click.stop="openOrderList(row, 'ALL', '订单数')">{{ row.orderCount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="commissionAmount" label="提成金额" min-width="120" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="commissionMissingOrderCount" label="提成未计算订单" width="150" align="right">
|
||||
<template #default="{ row }"><span class="success-value order-count-link" :class="{ 'is-disabled': !row.commissionMissingOrderCount }" @click.stop="openOrderList(row, 'COMMISSION_MISSING', '提成未计算订单')">{{ row.commissionMissingOrderCount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="incompleteOrderCount" label="价格缺失订单" width="130" align="right">
|
||||
<template #default="{ row }"><span class="success-value order-count-link" :class="{ 'is-disabled': !row.incompleteOrderCount }" @click.stop="openOrderList(row, 'INCOMPLETE', '价格缺失订单')">{{ row.incompleteOrderCount }}</span></template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table-column prop="bizDate" label="计费时间" width="170" align="center"><template #default="{ row }">{{ parseTime(row.bizDate) }}</template></el-table-column>
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="customerCode" label="客户代码" width="120" />
|
||||
<el-table-column prop="customerName" label="客户名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="belongSalerCode" label="业务员编码" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="belongSaler" label="业务员" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="productCode" label="产品代码" width="120" />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="commissionRate" label="提成比例" width="110" align="right"><template #default="{ row }">{{ commissionRateFormat(row.commissionRate) }}</template></el-table-column>
|
||||
<el-table-column prop="commissionAmount" label="提成金额" min-width="120" align="right"><template #default="{ row }">{{ money(row.commissionAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="arBillStatus" label="应收状态" width="110" align="center">
|
||||
<template #default="{ row }">{{ arBillStatusLabel(row.arBillStatus) }}</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<template #empty><el-empty description="暂无符合条件的数据" /></template>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="pageNum"
|
||||
v-model:limit="pageSize"
|
||||
:total="total"
|
||||
@pagination="loadRows"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="orderListDialogVisible" :title="orderListTitle" width="1280px" append-to-body>
|
||||
<el-table v-loading="orderListLoading" :data="orderListRows" border stripe>
|
||||
<el-table-column prop="bizDate" label="计费时间" width="170" align="center"><template #default="{ row }">{{ parseTime(row.bizDate) }}</template></el-table-column>
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="customerCode" label="客户代码" width="120" />
|
||||
<el-table-column prop="customerName" label="客户名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="belongSalerCode" label="业务员编码" width="130" />
|
||||
<el-table-column prop="productCode" label="产品代码" width="120" />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" width="110" />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" width="110" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" width="110" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" width="110" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="arBillStatus" label="应收状态" width="100" align="center"><template #default="{ row }">{{ arBillStatusLabel(row.arBillStatus) }}</template></el-table-column>
|
||||
<template #empty><el-empty description="暂无符合条件的订单" /></template>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="orderListTotal > 0"
|
||||
v-model:page="orderListPageNum"
|
||||
v-model:limit="orderListPageSize"
|
||||
:total="orderListTotal"
|
||||
@pagination="loadOrderList"
|
||||
/>
|
||||
<template #footer><el-button @click="orderListDialogVisible = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="commissionDialogVisible" title="业务员固定提成比例" width="760px" append-to-body>
|
||||
<div class="commission-toolbar">
|
||||
<span class="commission-tip">按业务员编码配置,比例输入百分数,例如 5 表示 5%。</span>
|
||||
<el-button type="primary" plain icon="Plus" v-hasPermi="['report:profit:commission:save']" @click="openRateForm()">新增比例</el-button>
|
||||
</div>
|
||||
<el-table v-loading="commissionLoading" :data="commissionRates" border stripe>
|
||||
<el-table-column prop="belongSalerCode" label="业务员编码" min-width="150" />
|
||||
<el-table-column prop="commissionRate" label="提成比例" width="130" align="right"><template #default="{ row }">{{ commissionRateFormat(row.commissionRate) }}</template></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90" align="center"><template #default="{ row }">{{ row.status === '0' ? '正常' : '停用' }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="130" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['report:profit:commission:save']" @click="openRateForm(row)">编辑</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['report:profit:commission:remove']" @click="handleDeleteRate(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="尚未配置业务员提成比例" /></template>
|
||||
</el-table>
|
||||
<template #footer><el-button @click="commissionDialogVisible = false">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="rateFormVisible" :title="rateForm.rateId ? '编辑提成比例' : '新增提成比例'" width="520px" append-to-body>
|
||||
<el-form ref="rateFormRef" :model="rateForm" :rules="rateRules" label-width="110px">
|
||||
<el-form-item label="业务员编码" prop="belongSalerCode"><el-input v-model="rateForm.belongSalerCode" maxlength="64" :disabled="!!rateForm.rateId" /></el-form-item>
|
||||
<el-form-item label="提成比例(%)" prop="commissionRate"><el-input-number v-model="rateForm.commissionRate" :min="0" :max="100" :precision="4" :step="0.01" controls-position="right" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="状态" prop="status"><el-radio-group v-model="rateForm.status"><el-radio value="0">正常</el-radio><el-radio value="1">停用</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="备注" prop="remark"><el-input v-model="rateForm.remark" type="textarea" :rows="2" maxlength="500" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="rateSaving" @click="submitRateForm">保存</el-button>
|
||||
<el-button @click="rateFormVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ProfitReport">
|
||||
import { deleteProfitCommissionRate, exportProfitOrders, exportProfitProducts, listProfitCommissionRates, listProfitOrders, listProfitProducts, listProfitSummary, saveProfitCommissionRate } from '@/api/report/profit'
|
||||
import { parseTime } from '@/utils/bms'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const dimension = ref('product')
|
||||
const multiValueFilterLimit = 2000
|
||||
const dateRange = ref([formatLocalDate(-30), formatLocalDate(0)])
|
||||
const query = reactive({
|
||||
orderNo: '',
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
belongSalerCode: '',
|
||||
productCode: ''
|
||||
})
|
||||
const summaryList = ref([])
|
||||
const rows = ref([])
|
||||
let rowsRequestVersion = 0
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const pageNum = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const orderListDialogVisible = ref(false)
|
||||
const orderListTitle = ref('订单列表')
|
||||
const orderListLoading = ref(false)
|
||||
const orderListRows = ref([])
|
||||
const orderListTotal = ref(0)
|
||||
const orderListPageNum = ref(1)
|
||||
const orderListPageSize = ref(10)
|
||||
const orderListContext = ref(null)
|
||||
let orderListRequestVersion = 0
|
||||
const commissionDialogVisible = ref(false)
|
||||
const commissionLoading = ref(false)
|
||||
const commissionRates = ref([])
|
||||
const rateFormVisible = ref(false)
|
||||
const rateSaving = ref(false)
|
||||
const rateFormRef = ref()
|
||||
const rateForm = reactive(createRateForm())
|
||||
const rateRules = {
|
||||
belongSalerCode: [{ required: true, message: '请输入业务员编码', trigger: 'blur' }],
|
||||
commissionRate: [{ required: true, message: '请输入提成比例', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function createRateForm(row = {}) {
|
||||
return {
|
||||
rateId: row.rateId || undefined,
|
||||
belongSalerCode: row.belongSalerCode || '',
|
||||
belongSaler: row.belongSaler || '',
|
||||
commissionRate: row.commissionRate === 0 ? 0 : (row.commissionRate || undefined),
|
||||
status: row.status || '0',
|
||||
remark: row.remark || ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatLocalDate(offset) {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + offset)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function buildQuery(paged = true) {
|
||||
const params = { ...query }
|
||||
if (dateRange.value?.[0]) params.beginBizDate = dateRange.value[0] + ' 00:00:00'
|
||||
if (dateRange.value?.[1]) params.endBizDate = dateRange.value[1] + ' 23:59:59'
|
||||
if (paged) {
|
||||
params.pageNum = pageNum.value
|
||||
params.pageSize = pageSize.value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function validateMultiValueFilters(showMessage = true) {
|
||||
const filters = [
|
||||
['orderNo', '订单号'],
|
||||
['customerCode', '客户代码'],
|
||||
['customerName', '客户名称']
|
||||
]
|
||||
for (const [key, label] of filters) {
|
||||
const values = new Set(String(query[key] || '').split(/[\s,,;;]+/).map(value => value.trim()).filter(Boolean))
|
||||
if (values.size > multiValueFilterLimit) {
|
||||
if (showMessage) ElMessage.warning(`${label}最多输入 ${multiValueFilterLimit} 个不重复值`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function loadSummary() {
|
||||
if (!validateMultiValueFilters(false)) return Promise.resolve()
|
||||
return listProfitSummary(buildQuery(false)).then(res => {
|
||||
summaryList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
function loadRows() {
|
||||
if (!validateMultiValueFilters()) return Promise.resolve()
|
||||
const requestVersion = ++rowsRequestVersion
|
||||
const requestDimension = dimension.value
|
||||
loading.value = true
|
||||
const api = requestDimension === 'product' ? listProfitProducts : listProfitOrders
|
||||
return api(buildQuery()).then(res => {
|
||||
if (requestVersion !== rowsRequestVersion || requestDimension !== dimension.value) return
|
||||
rows.value = res.rows || []
|
||||
total.value = res.total || 0
|
||||
}).finally(() => {
|
||||
if (requestVersion === rowsRequestVersion) loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!validateMultiValueFilters()) return
|
||||
pageNum.value = 1
|
||||
Promise.all([loadSummary(), loadRows()])
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
Object.keys(query).forEach(key => { query[key] = '' })
|
||||
dateRange.value = [formatLocalDate(-30), formatLocalDate(0)]
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleDimensionChange() {
|
||||
pageNum.value = 1
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
loadRows()
|
||||
}
|
||||
|
||||
function openOrderList(row, detailType, label) {
|
||||
if (!validateMultiValueFilters()) return
|
||||
const count = detailType === 'ALL' ? row.orderCount : detailType === 'COMMISSION_MISSING' ? row.commissionMissingOrderCount : row.incompleteOrderCount
|
||||
if (!count) return
|
||||
orderListTitle.value = `${label}订单列表`
|
||||
orderListContext.value = {
|
||||
productDrilldown: true,
|
||||
detailType,
|
||||
productCode: row.productCode,
|
||||
countryCode: row.countryCode,
|
||||
currency: row.currency
|
||||
}
|
||||
orderListPageNum.value = 1
|
||||
orderListTotal.value = 0
|
||||
orderListRows.value = []
|
||||
orderListDialogVisible.value = true
|
||||
loadOrderList()
|
||||
}
|
||||
|
||||
function loadOrderList() {
|
||||
if (!orderListContext.value) return
|
||||
if (!validateMultiValueFilters()) return
|
||||
const requestVersion = ++orderListRequestVersion
|
||||
orderListLoading.value = true
|
||||
const params = {
|
||||
...buildQuery(false),
|
||||
...orderListContext.value,
|
||||
pageNum: orderListPageNum.value,
|
||||
pageSize: orderListPageSize.value
|
||||
}
|
||||
listProfitOrders(params).then(res => {
|
||||
if (requestVersion !== orderListRequestVersion) return
|
||||
orderListRows.value = res.rows || []
|
||||
orderListTotal.value = res.total || 0
|
||||
}).finally(() => {
|
||||
if (requestVersion === orderListRequestVersion) orderListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function rowKey(row) {
|
||||
return dimension.value === 'product'
|
||||
? [row.productCode, row.countryCode, row.currency].join('|')
|
||||
: row.orderNo
|
||||
}
|
||||
|
||||
function money(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number.toFixed(2) : '-'
|
||||
}
|
||||
|
||||
function rate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number.toFixed(2) + '%' : '-'
|
||||
}
|
||||
|
||||
function commissionRateFormat(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number.toFixed(4) + '%' : '-'
|
||||
}
|
||||
|
||||
function arBillStatusLabel(value) {
|
||||
const labels = {
|
||||
UNBILLED: '未出账',
|
||||
PARTIAL: '部分出账',
|
||||
BILLED: '已出账'
|
||||
}
|
||||
return labels[value] || value || '-'
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
if (!validateMultiValueFilters()) return
|
||||
exporting.value = true
|
||||
const exporter = dimension.value === 'product' ? exportProfitProducts : exportProfitOrders
|
||||
exporter(buildQuery(false)).finally(() => { exporting.value = false })
|
||||
}
|
||||
|
||||
function openCommissionRates() {
|
||||
commissionDialogVisible.value = true
|
||||
commissionLoading.value = true
|
||||
listProfitCommissionRates().then(res => {
|
||||
commissionRates.value = res.data || []
|
||||
}).finally(() => {
|
||||
commissionLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function openRateForm(row) {
|
||||
Object.assign(rateForm, createRateForm(row))
|
||||
rateFormVisible.value = true
|
||||
nextTick(() => rateFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function submitRateForm() {
|
||||
rateFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
rateSaving.value = true
|
||||
saveProfitCommissionRate({ ...rateForm }).then(() => {
|
||||
ElMessage.success('提成比例已保存')
|
||||
rateFormVisible.value = false
|
||||
openCommissionRates()
|
||||
Promise.all([loadSummary(), loadRows()])
|
||||
}).finally(() => {
|
||||
rateSaving.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleDeleteRate(row) {
|
||||
ElMessageBox.confirm(`确定删除业务员 ${row.belongSalerCode} 的提成比例吗?`, '提示', { type: 'warning' }).then(() => {
|
||||
return deleteProfitCommissionRate(row.rateId)
|
||||
}).then(() => {
|
||||
ElMessage.success('提成比例已删除')
|
||||
openCommissionRates()
|
||||
Promise.all([loadSummary(), loadRows()])
|
||||
}).catch(error => {
|
||||
if (error !== 'cancel' && error !== 'close' && error?.action !== 'cancel' && error?.action !== 'close') {
|
||||
ElMessage.error(error?.message || '删除提成比例失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleSearch()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-card,
|
||||
.table-card {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.search-card :deep(.el-card__body) {
|
||||
padding: 18px 20px 8px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-item :deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-actions-col {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(560px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-card :deep(.el-card__header) {
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-card :deep(.el-card__body) {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
margin-bottom: -12px;
|
||||
}
|
||||
|
||||
.summary-grid :deep(.el-col) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-subtitle {
|
||||
float: right;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
margin-bottom: 5px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
min-height: 68px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.metric-value small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.profit-value {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.success-value {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.order-count-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-count-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.order-count-link.is-disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.table-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.table-heading :deep(.el-tabs) {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.table-heading :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-heading :deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.commission-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.commission-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.summary-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.search-card :deep(.el-card__body),
|
||||
.summary-card :deep(.el-card__body) {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.table-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user