销售发货

This commit is contained in:
朱昊天
2026-05-18 17:48:43 +08:00
parent f94ddb433d
commit 264ca0e407
59 changed files with 8181 additions and 603 deletions

View File

@@ -18,6 +18,8 @@
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@tailwindcss/vite": "^4.1.11",
"@vue-office/docx": "^1.6.3",
"@vue-office/excel": "^1.7.14",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
@@ -27,6 +29,7 @@
"element-plus": "2.9.9",
"file-saver": "2.0.5",
"fuse.js": "6.6.2",
"html2canvas": "^1.4.1",
"i": "^0.3.7",
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",

View File

@@ -69,3 +69,15 @@ export function importProductData(data, updateSupport) {
data: data
})
}
// 查询产品附加属性按产品ID集合返回 Map<productId, attrs>
export function listProductAdditionByProductIds(productIds) {
return request({
url: '/api/mat/productAddition/listByProductIds',
method: 'post',
data: productIds,
headers: {
repeatSubmit: false
}
})
}

View File

@@ -47,6 +47,9 @@ export function listProductAdditionByProductIds(productIds) {
return request({
url: '/api/mat/productAddition/listByProductIds',
method: 'post',
data: productIds
data: productIds,
headers: {
repeatSubmit: false
}
})
}

View File

@@ -0,0 +1,27 @@
import request from '@/utils/request'
// 订单生产记录gear_order_production
export function listOrderProduction(query) {
return request({
url: '/oa/orderProduction/list',
method: 'get',
params: query
})
}
export function initOrderProduction(orderId) {
return request({
url: '/oa/orderProduction/init/' + orderId,
method: 'post'
})
}
export function updateOrderProduction(data) {
return request({
url: '/oa/orderProduction',
method: 'put',
data
})
}

View File

@@ -0,0 +1,54 @@
import request from '@/utils/request'
// 查询销售员列表
export function listSalesman(query) {
return request({
url: '/oa/salesman/list',
method: 'get',
params: query
})
}
// 查询销售员详情
export function getSalesman(salesmanId) {
return request({
url: '/oa/salesman/' + salesmanId,
method: 'get'
})
}
// 新增销售员
export function addSalesman(data) {
return request({
url: '/oa/salesman',
method: 'post',
data
})
}
// 修改销售员
export function updateSalesman(data) {
return request({
url: '/oa/salesman',
method: 'put',
data
})
}
// 删除销售员
export function delSalesman(salesmanIds) {
return request({
url: '/oa/salesman/' + salesmanIds,
method: 'delete'
})
}
// 查询销售员跟进客户
export function listSalesmanCustomers(salesmanId, query) {
return request({
url: '/oa/salesman/' + salesmanId + '/customers',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,111 @@
import request from '@/utils/request'
// 发货单据(独立表 gear_shipping_order
// 说明:用于订单发货/物流信息记录,不与出入库/WMS 单据绑定
// 查询发货单据列表
export function listShippingOrder(query) {
return request({
url: '/oa/shippingOrder/list',
method: 'get',
params: query
})
}
// 根据订单ID查询发货单据列表
export function listShippingOrderByOrderId(orderId) {
return request({
url: '/oa/shippingOrder/listByOrderId/' + orderId,
method: 'get'
})
}
// 查询发货单据详细
export function getShippingOrder(shippingId) {
return request({
url: '/oa/shippingOrder/' + shippingId,
method: 'get'
})
}
// 新增发货单据
export function addShippingOrder(data) {
return request({
url: '/oa/shippingOrder',
method: 'post',
data: data
})
}
// 修改发货单据
export function updateShippingOrder(data) {
return request({
url: '/oa/shippingOrder',
method: 'put',
data: data
})
}
// 删除发货单据
export function delShippingOrder(shippingId) {
return request({
url: '/oa/shippingOrder/' + shippingId,
method: 'delete'
})
}
// ================================
// 发货计划(独立表 gear_shipping_plan
// ================================
// 查询发货计划列表(分页)
export function listShippingPlan(query) {
return request({
url: '/oa/shippingPlan/list',
method: 'get',
params: query
})
}
// 查询发货计划列表(带单据数,左侧列表使用)
export function listShippingPlanWithCount(query) {
return request({
url: '/oa/shippingPlan/listWithCount',
method: 'get',
params: query
})
}
// 查询发货计划详细
export function getShippingPlan(planId) {
return request({
url: '/oa/shippingPlan/' + planId,
method: 'get'
})
}
// 新增发货计划
export function addShippingPlan(data) {
return request({
url: '/oa/shippingPlan',
method: 'post',
data: data
})
}
// 修改发货计划
export function updateShippingPlan(data) {
return request({
url: '/oa/shippingPlan',
method: 'put',
data: data
})
}
// 删除发货计划
export function delShippingPlan(planId) {
return request({
url: '/oa/shippingPlan/' + planId,
method: 'delete'
})
}

View File

@@ -53,3 +53,17 @@ export function getStockTrace(batchNo) {
}
})
}
export function sumStockQuantityByItemIds(itemType, itemIds) {
return request({
url: '/gear/stock/sumQuantityByItemIds',
method: 'post',
params: {
itemType: itemType || 'product'
},
data: itemIds || [],
headers: {
repeatSubmit: false
}
})
}

View File

@@ -88,13 +88,13 @@ const renderChart = () => {
if (!productStats[productName]) {
productStats[productName] = {
quantity: 0, // 销量
amount: 0 // 销售额(销量*含税单价)
amount: 0 // 销售额(销量*单价)
};
}
// 累加销量
productStats[productName].quantity += Number(detail.quantity || 0);
// 累加销售额
const unitPrice = Number(detail.taxPrice || 0);
const unitPrice = Number(detail.unitPrice || 0);
productStats[productName].amount += unitPrice * Number(detail.quantity || 0);
});
@@ -200,4 +200,4 @@ const handleResize = () => {
width: 100%;
height: calc(100% - 40px);
}
</style>
</style>

View File

@@ -0,0 +1,626 @@
<template>
<div class="app-container sales-order-page">
<!-- 左右两栏左侧合同列表 + 右侧详情 -->
<el-row :gutter="16">
<!-- 左侧列表区 -->
<el-col :span="6" class="left-pane">
<!-- 顶部搜索 + 操作 -->
<div class="left-toolbar">
<el-input
v-model="keyword"
clearable
placeholder="请输入关键字"
@clear="handleQuery"
@keyup.enter="handleQuery"
>
<template #append>
<el-button icon="Search" @click="handleQuery" />
</template>
</el-input>
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</div>
<!-- 列表使用现有 GearList 组件实现卡片式 -->
<klp-list
:list-data="filteredList"
list-key="contractId"
:loading="loading"
info1-field="contractTitle"
info1-max-percent="70"
info5-field="contractCode"
@item-click="handleSelect"
>
<!-- 主标题合同名称 + 合同编号 -->
<template #info1="{ item }">
<span class="list-title">{{ item.contractTitle }}</span>
<span class="list-code">{{ item.contractCode }}</span>
</template>
<!-- 副标题/乙方 -->
<template #info2="{ item }">
<div class="list-sub">
<span>供方{{ item.sellerName }}</span>
<span class="ml-8">需方{{ item.buyerName }}</span>
</div>
</template>
<!-- 状态 -->
<template #info3="{ item }">
<el-tag :type="item.status === '已生效' ? 'success' : 'info'" size="small">
{{ item.status }}
</el-tag>
</template>
<!-- 时间信息 -->
<template #info4="{ item }">
<div class="list-sub">
<span>签订时间{{ item.signDate }}</span>
<span class="ml-8">交货日期{{ item.deliveryDate }}</span>
</div>
</template>
<!-- 右侧动作置顶/导出/修改/删除雏形只做 UI -->
<template #actions="{ item }">
<div class="list-actions">
<el-button link type="primary" icon="Top" @click.stop="handlePin(item)">置顶</el-button>
<el-button link type="primary" icon="Download" @click.stop="handleExport(item)">导出</el-button>
<el-button link type="primary" icon="Edit" @click.stop="handleEdit(item)">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleDelete(item)">删除</el-button>
</div>
</template>
</klp-list>
</el-col>
<!-- 右侧详情区 -->
<el-col :span="18" class="right-pane">
<!-- 未选中时提示 -->
<div v-if="!selected" class="empty-tip">
<el-empty description="请选择左侧一个订单查看详情" />
</div>
<!-- 选中后显示详情 -->
<div v-else>
<!-- 顶部标题 + 状态选择雏形 -->
<div class="detail-header">
<div class="detail-header__title">
<span class="detail-title">产品销售合同</span>
<span class="detail-code">{{ selected.contractCode }}</span>
</div>
<el-select v-model="selected.contractStatus" size="small" style="width: 140px">
<el-option label="草稿" value="草稿" />
<el-option label="已生效" value="已生效" />
<el-option label="已作废" value="已作废" />
</el-select>
</div>
<!-- 合同基础信息预览Excel表风格展示对齐产品管理-说明书预览的观感边框+圆角+表格单元格 -->
<div class="contract-excel-preview">
<div class="contract-excel-title">
<span>合同基础信息</span>
<div class="contract-excel-title__actions">
<!-- 上传合同Excel雏形直接保存ossId串到 selected.contractExcelOssIds后续可落库到订单主表 -->
<file-upload
v-model="selected.contractExcelOssIds"
:limit="1"
:file-size="20"
:file-type="['xls', 'xlsx']"
:is-show-tip="false"
@success="handleContractExcelUploaded"
/>
</div>
</div>
<div class="contract-excel-body">
<table class="excel-table">
<colgroup>
<col style="width: 14%" />
<col style="width: 36%" />
<col style="width: 14%" />
<col style="width: 36%" />
</colgroup>
<tbody>
<tr>
<td class="excel-cell excel-cell--label">合同编号</td>
<td class="excel-cell">{{ selected.contractCode }}</td>
<td class="excel-cell excel-cell--label">合同状态</td>
<td class="excel-cell">{{ selected.contractStatus }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">供方</td>
<td class="excel-cell">{{ selected.sellerName }}</td>
<td class="excel-cell excel-cell--label">需方</td>
<td class="excel-cell">{{ selected.buyerName }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">签订时间</td>
<td class="excel-cell">{{ selected.signDate }}</td>
<td class="excel-cell excel-cell--label">交货日期</td>
<td class="excel-cell">{{ selected.deliveryDate }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">签订地点</td>
<td class="excel-cell">{{ selected.signPlace }}</td>
<td class="excel-cell excel-cell--label">备注</td>
<td class="excel-cell">{{ selected.remark || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 产品内容产品名称 + 生产厂家 -->
<div class="section">
<div class="section-title">产品内容</div>
<div class="section-body">
<div class="product-top">
<div class="product-top__left">
<span class="label">产品名称</span>
<el-select v-model="selected.productName" filterable clearable style="width: 240px">
<el-option v-for="name in productNameOptions" :key="name" :label="name" :value="name" />
</el-select>
</div>
<div class="product-top__right">
<span class="label">生产厂家</span>
<span class="value">{{ selected.factoryName }}</span>
</div>
</div>
<!-- 产品明细雏形表格支持行内编辑与汇总 -->
<el-table
:data="selected.items"
border
size="small"
class="detail-table"
show-summary
:summary-method="summaryMethod"
>
<el-table-column type="index" width="60" label="序号" align="center" />
<el-table-column label="规格mm" min-width="160">
<template #default="{ row }">
<el-input v-model="row.spec" size="small" />
</template>
</el-table-column>
<el-table-column label="材质" width="120">
<template #default="{ row }">
<el-input v-model="row.material" size="small" />
</template>
</el-table-column>
<el-table-column label="数量(吨)" width="140" align="right">
<template #default="{ row }">
<el-input-number v-model="row.qty" :controls="false" :min="0" size="small" style="width: 120px" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
<template #default="{ row }">
<el-input v-model="row.remark" size="small" />
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 业务 Tab雏形占位 -->
<el-tabs v-model="activeTab" type="border-card" class="mt-12">
<el-tab-pane label="订单信息" name="orderInfo">
<el-empty description="订单信息(待完善)" />
</el-tab-pane>
<el-tab-pane label="财务状态" name="finance">
<div class="section">
<div class="section-title">财务状态</div>
<div class="finance-strip">
<div class="finance-item">
<div class="finance-item__label">订单金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.orderAmount) }}</div>
</div>
<div class="finance-item">
<div class="finance-item__label">已收款金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.receivedAmount) }}</div>
</div>
<div class="finance-item">
<div class="finance-item__label">未收款金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.unreceivedAmount) }}</div>
</div>
</div>
<div class="section-title mt-12">收款明细</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddPayment">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" size="small" :disabled="true">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" size="small" :disabled="true">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" size="small" @click="handleExportPayments">导出</el-button>
</el-col>
</el-row>
<el-table :data="paymentList" border size="small">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="收款日期" prop="payDate" width="160" />
<el-table-column label="收款金额" prop="amount" width="160" align="right">
<template #default="{ row }">
{{ formatMoney(row.amount) }}
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" />
<el-table-column label="操作" width="140" align="center">
<template #default>
<el-button link type="primary" size="small" icon="Edit">修改</el-button>
<el-button link type="danger" size="small" icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="生产成本" name="cost">
<el-empty description="生产成本(待完善)" />
</el-tab-pane>
<el-tab-pane label="发货流程" name="ship">
<el-empty description="发货流程(待完善)" />
</el-tab-pane>
<el-tab-pane label="合同附件" name="files">
<el-empty description="合同附件(待完善)" />
</el-tab-pane>
<el-tab-pane label="操作记录" name="logs">
<el-empty description="操作记录(待完善)" />
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import klpList from "@/components/GearList/index.vue";
import FileUpload from "@/components/FileUpload/index.vue";
export default {
name: "MatSalesOrder",
components: { klpList, FileUpload },
data() {
return {
// 页面加载态:雏形直接用本地数据,保留 loading 方便后续接接口
loading: false,
// 左侧搜索关键字
keyword: "",
// 当前选中的合同/订单
selected: null,
// 右侧 Tab 当前激活项
activeTab: "finance",
// 产品名称下拉:雏形静态配置,后续可接产品字典/接口
productNameOptions: ["冷轧钢卷", "热轧钢卷", "镀锌卷", "不锈钢卷"],
// 左侧列表:雏形静态数据,后续替换为接口分页
list: [
{
contractId: 1,
contractTitle: "产品销售合同",
contractCode: "KLPYX260420-14",
sellerName: "嘉祥科伦管理有限公司",
buyerName: "洛阳一石科技有限公司",
signDate: "2026-05-12",
deliveryDate: "2026-06-15",
signPlace: "嘉祥",
status: "草稿",
contractStatus: "草稿",
productName: "冷轧钢卷",
factoryName: "嘉祥科伦管理有限公司",
remark: "",
// 合同Excel附件ossId串逗号分隔
contractExcelOssIds: "",
items: [
{ spec: "0.38*1200", material: "SPCC", qty: 10, remark: "净边料 角包 卷" }
]
},
{
contractId: 2,
contractTitle: "产品销售合同",
contractCode: "KLPYX260420-06",
sellerName: "嘉祥科伦管理有限公司",
buyerName: "湖北广化工贸有限公司",
signDate: "2026-04-20",
deliveryDate: "2026-05-25",
signPlace: "嘉祥",
status: "已生效",
contractStatus: "已生效",
productName: "热轧钢卷",
factoryName: "嘉祥科伦管理有限公司",
remark: "",
// 合同Excel附件ossId串逗号分隔
contractExcelOssIds: "",
items: [
{ spec: "2.0*1250", material: "Q235B", qty: 5, remark: "" }
]
}
],
// 财务状态:雏形数据
financeSummary: {
orderAmount: 30000,
receivedAmount: 0,
unreceivedAmount: 30000
},
// 收款明细:雏形为空数据
paymentList: []
};
},
computed: {
// 左侧列表过滤:关键字匹配合同号/甲乙方
filteredList() {
const kw = (this.keyword || "").trim();
if (!kw) return this.list;
return this.list.filter((it) => {
return (
(it.contractCode || "").includes(kw) ||
(it.sellerName || "").includes(kw) ||
(it.buyerName || "").includes(kw)
);
});
}
},
methods: {
// 搜索:雏形仅触发计算属性刷新,后续可接接口
handleQuery() {},
// 左侧选中:加载详情(雏形直接赋值)
handleSelect(item) {
this.selected = item || null;
this.activeTab = "finance";
this.syncFinanceSummary();
},
// 新增:雏形占位,后续可弹窗录入合同/订单信息
handleAdd() {
this.$modal && this.$modal.msgInfo && this.$modal.msgInfo("新增(雏形)");
},
// 置顶:雏形占位
handlePin() {},
// 导出:雏形占位
handleExport() {},
// 修改:雏形占位
handleEdit() {},
// 删除:雏形占位
handleDelete() {},
// 财务:新增收款明细(雏形占位)
handleAddPayment() {
this.$modal && this.$modal.msgInfo && this.$modal.msgInfo("新增收款明细(雏形)");
},
// 财务:导出收款明细(雏形占位)
handleExportPayments() {},
// 金额格式化:雏形使用 toFixed
formatMoney(val) {
const n = Number(val || 0);
if (Number.isNaN(n)) return "0.00";
return n.toFixed(2);
},
// 表格汇总:合计数量/金额(雏形)
summaryMethod({ columns, data }) {
const sums = [];
const totalQty = data.reduce((acc, it) => acc + Number(it.qty || 0), 0);
columns.forEach((col, index) => {
if (index === 0) {
sums[index] = "合计";
return;
}
if (col.label === "数量(吨)") {
sums[index] = totalQty;
return;
}
sums[index] = "";
});
return sums;
},
// 根据明细同步财务汇总(雏形:用含税总额当订单金额)
syncFinanceSummary() {
if (!this.selected) return;
const orderAmount = 0;
const receivedAmount = (this.paymentList || []).reduce((acc, it) => acc + Number(it.amount || 0), 0);
this.financeSummary.orderAmount = orderAmount;
this.financeSummary.receivedAmount = receivedAmount;
this.financeSummary.unreceivedAmount = orderAmount - receivedAmount;
},
// 合同Excel上传完成雏形仅做提示后续可在这里调用接口保存到数据库
handleContractExcelUploaded() {
this.$modal && this.$modal.msgSuccess && this.$modal.msgSuccess("合同Excel已上传");
}
},
mounted() {
// 默认选中第一条,方便看到右侧雏形效果
if (this.list.length > 0) {
this.handleSelect(this.list[0]);
}
}
};
</script>
<style scoped>
.sales-order-page {
height: calc(100vh - 120px);
}
.left-pane {
height: 100%;
}
.left-toolbar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.list-title {
font-weight: 600;
margin-right: 8px;
}
.list-code {
color: #909399;
}
.list-sub {
color: #606266;
font-size: 12px;
display: inline-flex;
flex-wrap: wrap;
}
.ml-8 {
margin-left: 8px;
}
.list-actions {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-end;
}
.right-pane {
height: 100%;
}
.empty-tip {
height: 520px;
display: flex;
align-items: center;
justify-content: center;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.detail-header__title {
display: inline-flex;
align-items: baseline;
gap: 10px;
}
.detail-title {
font-size: 16px;
font-weight: 700;
}
.detail-code {
color: #909399;
}
.contract-excel-preview {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
overflow: hidden;
margin-bottom: 12px;
}
.contract-excel-title {
padding: 8px 10px;
font-size: 13px;
font-weight: 600;
background: var(--el-fill-color-lighter);
border-bottom: 1px solid var(--el-border-color-light);
display: flex;
align-items: center;
justify-content: space-between;
}
.contract-excel-title__actions :deep(.el-button) {
height: 28px;
padding: 0 10px;
}
.contract-excel-title__actions :deep(.upload-file-list) {
display: none;
}
.contract-excel-body {
background: #fff;
}
.excel-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.excel-cell {
border: 1px solid var(--el-border-color-light);
padding: 8px 10px;
font-size: 12px;
color: #303133;
word-break: break-word;
background: #fff;
}
.excel-cell--label {
background: var(--el-fill-color-light);
color: #606266;
font-weight: 600;
}
.section {
margin-top: 12px;
}
.section-title {
font-weight: 600;
margin-bottom: 8px;
}
.section-body {
background: #fff;
}
.product-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.label {
color: #606266;
}
.value {
font-weight: 600;
}
.detail-table {
width: 100%;
}
.mt-12 {
margin-top: 12px;
}
.finance-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.finance-item {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 10px 12px;
background: #fafafa;
}
.finance-item__label {
color: #909399;
font-size: 12px;
margin-bottom: 6px;
}
.finance-item__value {
font-size: 16px;
font-weight: 700;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -26,17 +26,6 @@
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">总销售额</p>
<h3 class="text-2xl font-bold">¥{{ totalSales.toFixed(2) }}</h3>
</div>
<el-icon class="text-warning text-xl"><Money /></el-icon>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="flex justify-between items-center">
@@ -74,7 +63,6 @@
<span>产品销售排行</span>
<el-select v-model="productRankType" style="width: 100px;" placeholder="统计类型" size="small" @change="renderProductRankChart">
<el-option label="销量" value="quantity" />
<el-option label="销售额" value="amount" />
</el-select>
</div>
</template>
@@ -119,12 +107,6 @@
<el-table-column prop="orderCode" label="订单编号" />
<el-table-column prop="customerName" label="客户名称" />
<el-table-column prop="salesManager" label="销售经理" />
<el-table-column
prop="taxAmount"
label="订单金额"
align="right"
:formatter="(row) => `¥${Number(row.taxAmount).toFixed(2)}`"
/>
<el-table-column
prop="orderStatus"
label="订单状态"
@@ -155,7 +137,7 @@
import { ref, onMounted, computed } from 'vue'
import * as echarts from 'echarts'
import {
UserFilled, ShoppingCart, Money, RefreshRight
UserFilled, ShoppingCart, RefreshRight
} from '@element-plus/icons-vue'
import { listOrder } from '@/api/oms/order'
import { listOrderDetail } from '@/api/oms/orderDetail'
@@ -194,12 +176,6 @@ const followUpStatusLabel = {
// 指标计算computed确保响应式
const totalCustomers = computed(() => customers.value.length)
const totalOrders = computed(() => orders.value.length)
const totalSales = computed(() => {
return orders.value.reduce(
(sum, order) => sum + Number(order.taxAmount || 0),
0
)
})
const returnExchangeRate = computed(() => {
if (totalOrders.value === 0) return 0
return (returnExchanges.value.length / totalOrders.value) * 100
@@ -250,7 +226,7 @@ onMounted(async () => {
// ---------- 图表渲染函数 ----------
// 订单趋势图(双轴:订单数+销售额
// 订单趋势图(订单数
const renderOrderTrendChart = () => {
const chartDom = orderTrendChart.value
if (!chartDom) return
@@ -266,19 +242,16 @@ const renderOrderTrendChart = () => {
dates.push(date.toLocaleDateString())
}
// 按日期统计订单数和销售额
// 按日期统计订单数
const orderCountMap = {}
const salesMap = {}
dates.forEach(date => {
orderCountMap[date] = 0
salesMap[date] = 0
})
orders.value.forEach(order => {
// 假设订单有createTime字段需与dates格式匹配
const orderDate = new Date(order.createTime).toLocaleDateString()
if (orderCountMap[orderDate] !== undefined) {
orderCountMap[orderDate]++
salesMap[orderDate] += Number(order.taxAmount || 0)
}
})
@@ -287,35 +260,19 @@ const renderOrderTrendChart = () => {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
legend: { data: ['订单数', '销售额'] },
legend: { data: ['订单数'] },
xAxis: {
type: 'category',
data: dates,
axisLabel: { rotate: 30, fontSize: 12 }
},
yAxis: [
{ type: 'value', name: '订单数', min: 0 },
{
type: 'value',
name: '销售额',
min: 0,
axisLabel: { formatter: '¥{value}' }
}
],
yAxis: { type: 'value', name: '订单数', min: 0 },
series: [
{
name: '订单数',
type: 'bar',
data: dates.map(date => orderCountMap[date]),
barWidth: '40%'
},
{
name: '销售额',
type: 'line',
yAxisIndex: 1,
data: dates.map(date => salesMap[date]),
smooth: true,
lineStyle: { width: 2 }
}
]
}
@@ -340,7 +297,7 @@ const renderProductRankChart = () => {
}
productStats[productName].quantity += Number(detail.quantity || 0)
productStats[productName].amount +=
Number(detail.quantity || 0) * Number(detail.taxPrice || 0)
Number(detail.quantity || 0) * Number(detail.unitPrice || 0)
})
// 转换为图表数据取Top10
@@ -478,4 +435,4 @@ const viewOrderDetail = (row) => {
width: 100%;
height: 300px;
}
</style>
</style>

View File

@@ -16,26 +16,6 @@
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" size="small" @click="handleExport">导出</el-button>
</el-col>
<!-- 含税部分 -->
<el-col :span="8">
<span>含税金额</span>
<span style="margin-right: 10px;">{{ orderInfo?.taxAmount }}</span>
<span style="margin-right: 10px;">-</span>
<span style="margin-right: 10px;">{{ actualAmount }}</span>
<span style="margin-right: 10px;">=</span>
<span style="color: red; margin-right: 10px;">{{ amountDifference }}</span>
</el-col>
<!-- 无税部分 -->
<el-col :span="8">
<span>无税金额</span>
<span style="margin-right: 10px;">{{ orderInfo?.noTaxAmount }}</span>
<span style="margin-right: 10px;">-</span>
<span style="margin-right: 10px;">{{ noTaxAmount }}</span>
<span style="margin-right: 10px;">=</span>
<span style="color: red; margin-right: 10px;">{{ noTaxAmountDifference }}</span>
</el-col>
</el-row>
<el-table v-loading="loading" :data="orderDetailList">
@@ -56,8 +36,6 @@
</el-table-column>
<el-table-column label="产品数量" align="center" prop="quantity" />
<el-table-column label="单位" align="center" prop="unit" />
<el-table-column label="含税单价" align="center" prop="taxPrice" />
<el-table-column label="无税单价" align="center" prop="noTaxPrice" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" width="200">
<template #default="scope">
@@ -81,13 +59,6 @@
<el-form-item label="单位" prop="unit">
<el-input v-model="form.unit" placeholder="单位" />
</el-form-item>
<el-form-item label="含税单价" prop="taxPrice">
<el-input-number :controls=false controls-position="right" v-model="form.taxPrice" placeholder="请输入含税单价" />
</el-form-item>
<el-form-item label="无税单价" prop="noTaxPrice">
<el-input-number :controls=false controls-position="right" v-model="form.noTaxPrice" placeholder="请输入无税单价"
:min="0" :max="form.taxPrice" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
@@ -165,22 +136,6 @@ export default {
// 是否可以编辑(订单状态为新建时才能编辑)
canEdit() {
return this.orderInfo && this.orderInfo.orderStatus === EOrderStatus.NEW;
},
actualAmount() {
return this.orderDetailList?.reduce((total, item) => {
return total + (item.taxPrice || 0) * (item.quantity || 0);
}, 0) || 0;
},
amountDifference() {
return ((this.orderInfo?.taxAmount || 0) - this.actualAmount);
},
noTaxAmount() {
return this.orderDetailList?.reduce((total, item) => {
return total + (item.noTaxPrice || 0) * (item.quantity || 0);
}, 0) || 0;
},
noTaxAmountDifference() {
return ((this.orderInfo?.noTaxAmount || 0) - this.noTaxAmount);
}
},
watch: {
@@ -224,8 +179,6 @@ export default {
orderId: this.orderId,
productId: undefined,
quantity: undefined,
taxPrice: undefined,
noTaxPrice: undefined,
unit: undefined,
remark: undefined,
delFlag: undefined,

File diff suppressed because it is too large Load Diff

View File

@@ -94,8 +94,9 @@
<!-- 添加或修改应收款管理宽松版对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客户ID" prop="customerId">
<CustomerSelect v-model="form.customerId" />
<el-form-item label="客户" prop="customerId">
<el-input v-if="fixedCustomer" :model-value="fixedCustomerLabel" disabled />
<CustomerSelect v-else v-model="form.customerId" />
</el-form-item>
<el-form-item label="到期日" prop="dueDate">
<el-date-picker clearable
@@ -141,6 +142,7 @@
<script>
import { listReceivable, getReceivable, delReceivable, addReceivable, updateReceivable, updatePaidAmount } from "@/api/finance/receivable";
import CustomerSelect from '@/components/CustomerSelect/index.vue';
import { getCustomer } from "@/api/oms/customer";
export default {
name: "Receivable",
@@ -151,6 +153,25 @@ export default {
orderId: {
type: [String, Number],
required: true
},
customerId: {
type: [String, Number],
required: false,
default: undefined
},
customerName: {
type: String,
required: false,
default: ""
}
},
computed: {
fixedCustomer() {
return this.customerId != null && String(this.customerId) !== ""
},
fixedCustomerLabel() {
const name = String(this.customerName || this.fixedCustomerName || "").trim()
return name || "—"
}
},
data() {
@@ -179,7 +200,7 @@ export default {
queryParams: {
pageNum: 1,
pageSize: 20,
customerId: undefined,
customerId: this.customerId != null && String(this.customerId) !== "" ? this.customerId : undefined,
orderId: this.orderId,
dueDate: undefined,
amount: undefined,
@@ -195,7 +216,8 @@ export default {
// 收款表单参数
receiveForm: {},
// 是否显示收款弹出层
receiveOpen: false
receiveOpen: false,
fixedCustomerName: ""
};
},
created() {
@@ -205,9 +227,35 @@ export default {
orderId(newVal) {
this.queryParams.orderId = newVal;
this.getList();
},
customerId(newVal) {
this.queryParams.customerId = newVal != null && String(newVal) !== "" ? newVal : undefined
this.loadFixedCustomerName()
},
customerName() {
this.loadFixedCustomerName()
}
},
methods: {
loadFixedCustomerName() {
if (!this.fixedCustomer) {
this.fixedCustomerName = ""
return
}
const name = String(this.customerName || "").trim()
if (name) {
this.fixedCustomerName = name
return
}
const id = this.customerId
if (id == null || String(id) === "") return
getCustomer(id).then(res => {
const d = res && res.data ? res.data : null
this.fixedCustomerName = (d && d.name) ? String(d.name) : ""
}).catch(() => {
this.fixedCustomerName = ""
})
},
/** 查询应收款管理(宽松版)列表 */
getList() {
this.loading = true;
@@ -226,7 +274,7 @@ export default {
reset() {
this.form = {
receivableId: undefined,
customerId: undefined,
customerId: this.fixedCustomer ? this.customerId : undefined,
orderId: this.orderId,
dueDate: undefined,
amount: undefined,
@@ -263,6 +311,7 @@ export default {
this.reset();
this.open = true;
this.title = "添加应收款管理(宽松版)";
this.loadFixedCustomerName()
},
/** 修改按钮操作 */
handleUpdate(row) {
@@ -291,7 +340,11 @@ export default {
this.buttonLoading = false;
});
} else {
addReceivable(this.form).then(response => {
const payload = Object.assign({}, this.form, {
customerId: this.fixedCustomer ? this.customerId : this.form.customerId,
orderId: this.orderId
})
addReceivable(payload).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();

View File

@@ -0,0 +1,761 @@
<template>
<div class="app-container salesman-page">
<el-row :gutter="16">
<!-- 左侧销售员列表对齐示意图的左列表 + 右明细布局 -->
<el-col :span="6" class="salesman-left">
<el-form :model="queryParams" ref="queryRef" label-width="56px" size="small" class="left-filter">
<el-form-item label="姓名">
<el-input
v-model="queryParams.name"
clearable
placeholder="请输入销售员名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" clearable placeholder="数据状态">
<el-option label="正常" :value="0" />
<el-option label="停用" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="left-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
<el-button plain icon="Refresh" @click="getSalesmanList">刷新</el-button>
</div>
<div class="left-list" v-loading="leftLoading">
<el-empty v-if="!salesmanList.length && !leftLoading" description="暂无销售员" />
<el-scrollbar v-else max-height="calc(100vh - 290px)">
<div
v-for="item in salesmanList"
:key="item.salesmanId"
class="salesman-item"
:class="{ active: item.salesmanId === selectedSalesmanId }"
@click="handleSelect(item)"
>
<div class="salesman-item__left">
<el-tag v-if="item.status === 0" size="small" type="success">正常</el-tag>
<el-tag v-else size="small" type="danger">停用</el-tag>
<span class="salesman-item__name">{{ item.name }}</span>
</div>
<div class="salesman-item__actions">
<el-button link type="primary" icon="Edit" @click.stop="handleUpdate(item)">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleDelete(item)">删除</el-button>
</div>
</div>
</el-scrollbar>
</div>
</el-col>
<!-- 右侧跟进信息Tab 结构对齐示意图 -->
<el-col :span="18" class="salesman-right">
<el-card shadow="never">
<div class="right-title">
<span>销售员</span>
<span class="right-title__name">{{ selectedSalesmanName || "未选择" }}</span>
</div>
<el-tabs v-model="activeTab" class="right-tabs" @tab-change="handleTabChange">
<el-tab-pane label="跟进客户" name="followCustomer" />
<el-tab-pane label="跟进合同" name="followContract" />
<el-tab-pane label="发货单据" name="shippingDocs" />
<el-tab-pane label="生产成果" name="production" />
<el-tab-pane label="计划发货" name="planShipping" />
</el-tabs>
<div class="right-body">
<el-empty v-if="!selectedSalesmanId" description="请先从左侧选择销售员" />
<template v-else>
<div v-show="activeTab === 'followCustomer'">
<!-- 跟进客户从订单反查客户后端接口 /oa/salesman/{id}/customers -->
<el-table v-loading="customerLoading" :data="customerList">
<el-table-column label="公司名称" prop="name" min-width="220" />
<el-table-column label="联系方式" min-width="160">
<template #default="scope">
{{ scope.row.mobile || scope.row.telephone || "-" }}
</template>
</el-table-column>
<el-table-column label="税号" min-width="160">
<template #default>
-
</template>
</el-table-column>
<el-table-column label="行业" prop="industryId" width="120" />
<el-table-column label="客户等级" prop="level" width="120" />
<el-table-column label="地址" prop="detailAddress" min-width="260" show-overflow-tooltip />
</el-table>
<pagination
v-show="customerTotal > 0"
:total="customerTotal"
v-model:page="customerQuery.pageNum"
v-model:limit="customerQuery.pageSize"
@pagination="loadCustomers"
/>
</div>
<div v-show="activeTab === 'followContract'">
<!-- 跟进合同复用现有订单接口/oa/order/list以销售经理名称过滤 -->
<el-table v-loading="orderLoading" :data="orderList">
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="200" />
<el-table-column label="状态" prop="orderStatus" width="120" />
<el-table-column label="创建时间" prop="createTime" width="180" />
</el-table>
<pagination
v-show="orderTotal > 0"
:total="orderTotal"
v-model:page="orderQuery.pageNum"
v-model:limit="orderQuery.pageSize"
@pagination="loadOrders"
/>
</div>
<div v-show="activeTab === 'shippingDocs'">
<el-table v-loading="shippingLoading" :data="shippingList">
<el-table-column label="发货单号" prop="shippingNo" min-width="180" />
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="收货单位" prop="receiverCompany" min-width="220" />
<el-table-column label="发货时间" prop="shipTime" width="180" />
<el-table-column label="物流公司" prop="logisticsCompany" min-width="140" />
<el-table-column label="运单号" prop="logisticsNo" min-width="180" />
<el-table-column label="状态" prop="status" width="120" />
</el-table>
</div>
<div v-show="activeTab === 'production'">
<div class="production-wrap">
<div class="production-summary">
<div class="production-summary__item">订单数{{ productionSummary.orderCount }}</div>
<div class="production-summary__item">计划总量{{ formatQty(productionSummary.planQty) }}</div>
<div class="production-summary__item">已完成总量{{ formatQty(productionSummary.finishedQty) }}</div>
<div class="production-summary__item">完成率{{ productionSummary.rateText }}</div>
</div>
<el-table v-loading="productionLoading" :data="productionOrderSummaryList" size="small" border>
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="180" show-overflow-tooltip />
<el-table-column label="计划" prop="planQty" width="120" align="center" />
<el-table-column label="完成" prop="finishedQty" width="120" align="center" />
<el-table-column label="完成率" width="120" align="center">
<template #default="scope">
{{ formatRate(scope.row.finishedQty, scope.row.planQty) }}
</template>
</el-table-column>
<el-table-column label="最近更新" prop="lastUpdateTime" min-width="160" />
</el-table>
</div>
</div>
<div v-show="activeTab === 'planShipping'">
<el-card shadow="never">
<template #header>
<div class="plan-shipping__header">
计划发货
<span class="plan-shipping__sub">产品明细按该销售员全部订单汇总</span>
</div>
</template>
<div class="plan-shipping__summary">
<div class="plan-shipping__summary-item">总条数{{ planDetailSummary.totalLines }}</div>
<div class="plan-shipping__summary-item">总数量{{ planDetailSummary.totalQty }}</div>
<div class="plan-shipping__summary-item">订单数{{ planDetailSummary.totalOrders }}</div>
<div class="plan-shipping__summary-item">已发货订单{{ planDetailSummary.shippedOrders }}</div>
</div>
<el-table
v-loading="planDetailLoading"
:data="planOrderDetailList"
size="small"
border
>
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="160" show-overflow-tooltip />
<el-table-column label="产品编号" min-width="140">
<template #default="scope">
<el-popover placement="right" trigger="hover" width="460">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="订单编号">{{ scope.row.orderCode || "-" }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ scope.row.customerName || "-" }}</el-descriptions-item>
<el-descriptions-item label="产品编号">{{ scope.row.productCode || "-" }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ scope.row.productName || "-" }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ scope.row.spec || "-" }}</el-descriptions-item>
<el-descriptions-item label="型号">{{ scope.row.model || "-" }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ scope.row.unit || "-" }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ scope.row.quantity ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ scope.row.remark || "-" }}</el-descriptions-item>
</el-descriptions>
<template #reference>
<el-link type="primary" :underline="false">{{ scope.row.productCode || "-" }}</el-link>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="产品名称" prop="productName" min-width="180" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="140" show-overflow-tooltip />
<el-table-column label="型号" prop="model" min-width="140" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="90" />
<el-table-column label="数量" prop="quantity" width="90" />
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
<el-table-column label="发货状态" width="110">
<template #default="scope">
<el-tag v-if="scope.row.shippedFlag" type="success" size="small">已发货</el-tag>
<el-tag v-else type="info" size="small">未发货</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
</div>
</el-card>
</el-col>
</el-row>
<!-- 新增/修改销售员对话框 -->
<el-dialog :title="dialogTitle" v-model="dialogOpen" width="520px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入销售员姓名" />
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.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="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="submitLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogOpen = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import { listSalesman, addSalesman, updateSalesman, delSalesman, listSalesmanCustomers } from "@/api/oms/salesman";
import { listOrder } from "@/api/oms/order";
import { listShippingOrder } from "@/api/oms/shippingOrder";
import { listOrderDetail } from "@/api/oms/orderDetail";
import { listOrderProduction } from "@/api/oms/orderProduction";
export default {
name: "Salesman",
data() {
return {
leftLoading: false,
salesmanList: [],
selectedSalesmanId: undefined,
selectedSalesmanName: "",
activeTab: "followCustomer",
// 左侧查询条件(用于筛选销售员列表)
queryParams: {
pageNum: 1,
pageSize: 9999,
name: undefined,
status: undefined
},
// 跟进客户
customerLoading: false,
customerList: [],
customerTotal: 0,
customerQuery: {
pageNum: 1,
pageSize: 20
},
// 跟进合同(订单列表)
orderLoading: false,
orderList: [],
orderTotal: 0,
orderQuery: {
pageNum: 1,
pageSize: 20
},
// 发货单据
shippingLoading: false,
shippingList: [],
// 生产成果:按订单汇总(只读)
productionLoading: false,
productionOrderSummaryList: [],
// 计划发货:产品明细(按销售员全部订单汇总)
planDetailLoading: false,
planOrderDetailList: [],
// 新增/修改弹窗
dialogOpen: false,
dialogTitle: "",
submitLoading: false,
form: {},
rules: {
name: [{ required: true, message: "请输入销售员姓名", trigger: "blur" }]
}
};
},
computed: {
planDetailSummary() {
const rows = this.planOrderDetailList || [];
const totalLines = rows.length;
const totalQty = rows.reduce((sum, r) => sum + Number(r && r.quantity != null ? r.quantity : 0), 0);
const totalOrders = new Set(rows.map(r => r && r.orderId).filter(v => v != null)).size;
const shippedOrders = new Set(rows.filter(r => r && r.shippedFlag).map(r => r.orderId).filter(v => v != null)).size;
return {
totalLines,
totalQty,
totalOrders,
shippedOrders
};
},
productionSummary() {
const list = Array.isArray(this.productionOrderSummaryList) ? this.productionOrderSummaryList : [];
const orderCount = list.length;
const planQty = list.reduce((sum, r) => sum + Number(r && r.planQty != null ? r.planQty : 0), 0);
const finishedQty = list.reduce((sum, r) => sum + Number(r && r.finishedQty != null ? r.finishedQty : 0), 0);
const rateText = planQty > 0 ? `${((finishedQty / planQty) * 100).toFixed(1)}%` : "0%";
return { orderCount, planQty, finishedQty, rateText };
}
},
created() {
this.getSalesmanList();
},
methods: {
// 查询销售员列表
getSalesmanList() {
this.leftLoading = true;
listSalesman(this.queryParams)
.then(res => {
this.salesmanList = (res && res.rows) ? res.rows : [];
// 列表刷新后:若未选中,则默认选中第一条,方便右侧直接看到内容
if (!this.selectedSalesmanId && this.salesmanList.length) {
this.handleSelect(this.salesmanList[0]);
}
})
.finally(() => {
this.leftLoading = false;
});
},
// 搜索
handleQuery() {
this.queryParams.pageNum = 1;
this.getSalesmanList();
},
// 重置
resetQuery() {
this.queryParams.name = undefined;
this.queryParams.status = undefined;
this.handleQuery();
},
// 选中销售员:刷新右侧数据
handleSelect(item) {
if (!item || !item.salesmanId) return;
this.selectedSalesmanId = item.salesmanId;
this.selectedSalesmanName = item.name;
this.customerQuery.pageNum = 1;
this.orderQuery.pageNum = 1;
this.loadCustomers();
if (this.activeTab === "followContract") {
this.loadOrders();
}
if (this.activeTab === "shippingDocs") {
this.loadShippingOrders();
}
if (this.activeTab === "production") {
this.loadProductionSummary();
}
if (this.activeTab === "planShipping") {
this.loadPlanShipping();
}
},
// Tab切换按需加载避免每次都拉所有数据
handleTabChange() {
if (!this.selectedSalesmanId) return;
if (this.activeTab === "followCustomer") {
this.loadCustomers();
}
if (this.activeTab === "followContract") {
this.loadOrders();
}
if (this.activeTab === "shippingDocs") {
this.loadShippingOrders();
}
if (this.activeTab === "production") {
this.loadProductionSummary();
}
if (this.activeTab === "planShipping") {
this.loadPlanShipping();
}
},
// 跟进客户:后端反查客户
loadCustomers() {
if (!this.selectedSalesmanId) return;
this.customerLoading = true;
listSalesmanCustomers(this.selectedSalesmanId, this.customerQuery)
.then(res => {
this.customerList = (res && res.rows) ? res.rows : [];
this.customerTotal = res && typeof res.total === "number" ? res.total : 0;
})
.finally(() => {
this.customerLoading = false;
});
},
// 跟进合同:按 salesManager姓名过滤订单
loadOrders() {
if (!this.selectedSalesmanId) return;
this.orderLoading = true;
listOrder({
...this.orderQuery,
salesmanId: this.selectedSalesmanId
})
.then(res => {
this.orderList = (res && res.rows) ? res.rows : [];
this.orderTotal = res && typeof res.total === "number" ? res.total : 0;
})
.finally(() => {
this.orderLoading = false;
});
},
loadShippingOrders() {
if (!this.selectedSalesmanId) return;
this.shippingLoading = true;
listShippingOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 })
.then(res => {
this.shippingList = (res && res.rows) ? res.rows : [];
})
.finally(() => {
this.shippingLoading = false;
});
},
async loadProductionSummary() {
if (!this.selectedSalesmanId) return;
this.productionLoading = true;
try {
const orderRes = await listOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 });
const orders = (orderRes && orderRes.rows) ? orderRes.rows : [];
const promises = orders
.filter(o => o && o.orderId != null)
.map(async (o) => {
const prodRes = await listOrderProduction({ orderId: o.orderId, pageNum: 1, pageSize: 9999 });
const rows = (prodRes && prodRes.rows) ? prodRes.rows : [];
const planQty = rows.reduce((sum, r) => sum + Number(r && r.planQty != null ? r.planQty : 0), 0);
const finishedQty = rows.reduce((sum, r) => sum + Number(r && r.finishedQty != null ? r.finishedQty : 0), 0);
let lastUpdateTime = "";
rows.forEach(r => {
const t = r && r.updateTime ? String(r.updateTime) : "";
if (t && (!lastUpdateTime || new Date(t).getTime() > new Date(lastUpdateTime).getTime())) {
lastUpdateTime = t;
}
});
return {
orderId: o.orderId,
orderCode: o.orderCode,
customerName: o.customerName,
planQty,
finishedQty,
lastUpdateTime
};
});
this.productionOrderSummaryList = await Promise.all(promises);
} finally {
this.productionLoading = false;
}
},
loadPlanShipping() {
if (!this.selectedSalesmanId) return;
this.planDetailLoading = true;
Promise.all([
listOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 }),
listShippingOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 })
])
.then(async ([orderRes, shipRes]) => {
const orders = (orderRes && orderRes.rows) ? orderRes.rows : [];
const shipping = (shipRes && shipRes.rows) ? shipRes.rows : [];
const orderMap = new Map();
orders.forEach(o => {
if (o && o.orderId != null) orderMap.set(o.orderId, o);
});
const shipStatusMap = new Map();
shipping.forEach(s => {
const orderId = s && s.orderId != null ? s.orderId : null;
if (orderId == null) return;
const n = Number(s && s.status != null ? s.status : 0);
const prev = shipStatusMap.has(orderId) ? shipStatusMap.get(orderId) : 0;
shipStatusMap.set(orderId, Number.isFinite(n) ? Math.max(prev, n) : prev);
});
const detailPromises = orders
.filter(o => o && o.orderId)
.map(o =>
listOrderDetail({ orderId: o.orderId, pageNum: 1, pageSize: 9999 })
.then(res => ({
order: o,
rows: (res && res.rows) ? res.rows : []
}))
);
const detailResults = await Promise.all(detailPromises);
const merged = [];
detailResults.forEach(({ order, rows }) => {
const maxStatus = shipStatusMap.has(order.orderId) ? shipStatusMap.get(order.orderId) : 0;
const shippedFlag = Number(maxStatus) >= 2;
rows
.filter(r => String(r && r.productType ? r.productType : "").toLowerCase() === "product")
.forEach(r => {
merged.push({
...r,
orderId: order.orderId,
orderCode: order.orderCode,
customerName: order.customerName,
shippedFlag
});
});
});
this.planOrderDetailList = merged;
})
.finally(() => {
this.planDetailLoading = false;
});
},
formatQty(v) {
const n = Number(v);
const val = Number.isFinite(n) ? n : 0;
return val.toFixed(2);
},
formatRate(finished, plan) {
const a = Number(finished);
const b = Number(plan);
const fa = Number.isFinite(a) ? a : 0;
const fb = Number.isFinite(b) ? b : 0;
if (fb <= 0) return "0%";
return `${((fa / fb) * 100).toFixed(1)}%`;
},
// 新增
handleAdd() {
this.dialogTitle = "新增销售员";
this.form = {
salesmanId: undefined,
name: "",
mobile: "",
status: 0,
remark: ""
};
this.dialogOpen = true;
},
// 修改
handleUpdate(row) {
this.dialogTitle = "修改销售员";
this.form = { ...row };
if (this.form.status === undefined || this.form.status === null) {
this.form.status = 0;
}
this.dialogOpen = true;
},
// 删除
handleDelete(row) {
this.$modal
.confirm(`是否确认删除销售员"${row.name}"`)
.then(() => delSalesman([row.salesmanId]))
.then(() => {
this.$modal.msgSuccess("删除成功");
// 删除后清空选中,避免右侧仍显示已删除人员的数据
if (this.selectedSalesmanId === row.salesmanId) {
this.selectedSalesmanId = undefined;
this.selectedSalesmanName = "";
}
this.getSalesmanList();
})
.catch(() => {});
},
// 提交新增/修改
submitForm() {
this.$refs["formRef"].validate(valid => {
if (!valid) return;
this.submitLoading = true;
const req = this.form.salesmanId ? updateSalesman(this.form) : addSalesman(this.form);
req
.then(() => {
this.$modal.msgSuccess("保存成功");
this.dialogOpen = false;
this.getSalesmanList();
})
.finally(() => {
this.submitLoading = false;
});
});
}
}
};
</script>
<style scoped>
.salesman-left {
height: calc(100vh - 110px);
}
.left-filter {
padding: 10px 10px 0 10px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: #fff;
}
.left-actions {
display: flex;
gap: 8px;
margin: 10px 0;
}
.left-list {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: #fff;
padding: 10px;
}
.salesman-item {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
}
.salesman-item.active {
border-color: #409eff;
background: var(--el-color-primary-light-9);
}
.salesman-item__left {
display: flex;
align-items: center;
gap: 10px;
}
.salesman-item__name {
font-weight: 600;
color: #303133;
}
.salesman-item__actions {
display: flex;
align-items: center;
gap: 8px;
}
.right-title {
display: flex;
align-items: center;
gap: 6px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-light);
margin-bottom: 10px;
}
.right-title__name {
font-weight: 600;
color: #303133;
}
.right-body {
padding-top: 14px;
}
:deep(.right-tabs .el-tabs__content) {
display: none;
}
:deep(.right-tabs .el-tabs__header) {
margin: 0;
}
.plan-shipping__header {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #303133;
}
.plan-shipping__sub {
font-weight: 400;
color: #909399;
font-size: 12px;
}
.plan-shipping__summary {
display: flex;
align-items: center;
gap: 24px;
padding: 10px 12px;
margin-bottom: 10px;
background: #f5f7fa;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
color: #303133;
}
.plan-shipping__summary-item {
font-size: 13px;
}
.production-wrap {
padding-top: 6px;
}
.production-summary {
display: flex;
align-items: center;
gap: 24px;
padding: 10px 12px;
margin-bottom: 10px;
background: #f5f7fa;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
color: #303133;
}
.production-summary__item {
font-size: 13px;
}
</style>

File diff suppressed because it is too large Load Diff