fix: 修复销售订单页面Bug(issues-0311-2)

- salesOrder.ts: 列表API添加normalizeOrderRow字段别名映射
  (salesUserName→salesmanName, salesUserId→salesmanId, saleType→salesType)
- salesOrder.ts: 详情API添加审核信息字段别名映射
  (checkUserName→auditorName, checkDate→auditDate等)
- form.vue: 销售人员下拉及保存优先使用nickName昵称
- form.vue: 主表数量/单价/质量要求/备注输入框去掉size=small,高度恢复默认
- form.vue: 主表物料编码列宽从130增至min-width=260
- view.vue: 详情页基础信息区域样式美化(卡片式布局、label加重、min-height)
- Progress.vue: 新建生产进度页面,展示订单信息及关联生产工单
- router/index.ts: 修复生产进度路由指向Progress.vue(原误指向index.vue)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
panchengyong
2026-03-11 14:04:32 +08:00
parent 80ce9ad8e2
commit c1110d6b08
5 changed files with 250 additions and 14 deletions

View File

@@ -158,10 +158,20 @@ export const BIZ_STATUS_OPTIONS = [
const BASE = '/erp/sl/order' const BASE = '/erp/sl/order'
/** 将列表行数据做字段别名映射 */
function normalizeOrderRow(row: any): any {
return {
...row,
salesmanId: row.salesmanId ?? row.salesUserId,
salesmanName: row.salesmanName ?? row.salesUserName ?? '',
salesType: row.salesType ?? row.saleType ?? '',
}
}
/** 获取销售订单列表(单据视图) */ /** 获取销售订单列表(单据视图) */
export function getSalesOrderList(params: SalesOrderQuery): Promise<SalesOrderListResponse> { export function getSalesOrderList(params: SalesOrderQuery): Promise<SalesOrderListResponse> {
return request.get(`${BASE}/list`, { params }).then((res: any) => { return request.get(`${BASE}/list`, { params }).then((res: any) => {
const rows = res.rows ?? res.data?.rows ?? [] const rows = (res.rows ?? res.data?.rows ?? []).map(normalizeOrderRow)
const total = res.total ?? res.data?.total ?? 0 const total = res.total ?? res.data?.total ?? 0
return { list: rows, total } return { list: rows, total }
}) })
@@ -170,7 +180,7 @@ export function getSalesOrderList(params: SalesOrderQuery): Promise<SalesOrderLi
/** 获取销售订单明细列表(明细视图,展开物料维度) */ /** 获取销售订单明细列表(明细视图,展开物料维度) */
export function getSalesOrderDetailList(params: SalesOrderQuery): Promise<SalesOrderDetailListResponse> { export function getSalesOrderDetailList(params: SalesOrderQuery): Promise<SalesOrderDetailListResponse> {
return request.get(`${BASE}/lineList`, { params }).then((res: any) => { return request.get(`${BASE}/lineList`, { params }).then((res: any) => {
const rows = res.rows ?? res.data?.rows ?? [] const rows = (res.rows ?? res.data?.rows ?? []).map(normalizeOrderRow)
const total = res.total ?? res.data?.total ?? 0 const total = res.total ?? res.data?.total ?? 0
return { list: rows, total } return { list: rows, total }
}) })
@@ -187,6 +197,11 @@ export function getSalesOrderDetail(orderId: number): Promise<SalesOrder> {
// 后端返回 salesUserId/salesUserName前端表单使用 salesmanId/salesmanName便于下拉回显 // 后端返回 salesUserId/salesUserName前端表单使用 salesmanId/salesmanName便于下拉回显
if (data.salesUserId !== undefined) data.salesmanId = data.salesUserId if (data.salesUserId !== undefined) data.salesmanId = data.salesUserId
if (data.salesUserName !== undefined) data.salesmanName = data.salesUserName if (data.salesUserName !== undefined) data.salesmanName = data.salesUserName
// 审核信息字段别名映射(后端可能使用 checkBy/checkDate 等命名)
// 使用 == null 而非 ! 判断,避免 auditorId=0 或 auditorName='' 被误判为缺失
if (data.auditorId == null) data.auditorId = data.checkUserId ?? data.checkById ?? data.auditById
if (data.auditorName == null) data.auditorName = data.checkUserName ?? data.checkByName ?? data.auditByName ?? data.auditUser
if (data.auditDate == null) data.auditDate = data.checkDate ?? data.auditTime ?? data.checkTime
return data return data
}) })
} }

View File

@@ -79,7 +79,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: 'order/progress/:id', path: 'order/progress/:id',
name: 'SalesOrderProgress', name: 'SalesOrderProgress',
component: () => import('@/views/Sales/Order/index.vue'), component: () => import('@/views/Sales/Order/Progress.vue'),
meta: { title: '生产进度' } meta: { title: '生产进度' }
}, },
{ {

View File

@@ -0,0 +1,216 @@
<template>
<div class="progress-container">
<!-- 顶部操作栏 -->
<div class="page-header">
<div class="header-left">
<el-button @click="handleBack" :icon="ArrowLeft">返回</el-button>
<span class="page-title">生产进度</span>
</div>
</div>
<!-- 订单基础信息 -->
<el-card shadow="never" class="info-card" v-loading="orderLoading">
<template #header>
<span class="card-title">销售订单信息</span>
</template>
<el-row :gutter="16">
<el-col :span="6">
<div class="info-item">
<span class="info-label">单据编码</span>
<span class="info-value">{{ orderInfo.orderCode || '-' }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<span class="info-label">客户名称</span>
<span class="info-value">{{ orderInfo.clientName || '-' }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<span class="info-label">单据状态</span>
<span class="info-value">
<el-tag :type="getStatusType(orderInfo.orderStatus)">
{{ getStatusLabel(orderInfo.orderStatus) }}
</el-tag>
</span>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<span class="info-label">销售人员</span>
<span class="info-value">{{ orderInfo.salesmanName || '-' }}</span>
</div>
</el-col>
</el-row>
</el-card>
<!-- 生产工单列表 -->
<el-card shadow="never" class="table-card" style="margin-top: 16px;">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="card-title">关联生产工单</span>
<el-button type="primary" size="small" @click="loadWorkOrders">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</template>
<el-table
v-loading="workOrderLoading"
:data="workOrders"
stripe
border
style="width: 100%"
empty-text="暂无关联的生产工单"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="workorderCode" label="工单编码" width="140" show-overflow-tooltip />
<el-table-column prop="workorderName" label="工单名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="productCode" label="产品编码" width="130" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="quantity" label="计划数量" width="100" align="right" />
<el-table-column prop="quantityProduced" label="完工数量" width="100" align="right" />
<el-table-column prop="status" label="工单状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getWorkOrderStatusType(row.status)">
{{ getWorkOrderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="productionDate" label="计划开始" width="110" />
<el-table-column prop="finishDate" label="完工日期" width="110" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleViewWorkOrder(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ArrowLeft, Refresh } from '@element-plus/icons-vue'
import { getSalesOrderDetail, ORDER_STATUS_MAP, type SalesOrder } from '@/api/salesOrder'
import { getWorkOrderList, STATUS_MAP, type WorkOrder } from '@/api/workOrder'
const route = useRoute()
const router = useRouter()
const orderLoading = ref(false)
const workOrderLoading = ref(false)
const orderInfo = ref<Partial<SalesOrder>>({})
const workOrders = ref<WorkOrder[]>([])
function getStatusType(status?: string): string {
return ORDER_STATUS_MAP[status || '']?.type || ''
}
function getStatusLabel(status?: string): string {
return ORDER_STATUS_MAP[status || '']?.label || status || '-'
}
function getWorkOrderStatusType(status?: string): string {
return STATUS_MAP[status || '']?.type || ''
}
function getWorkOrderStatusLabel(status?: string): string {
return STATUS_MAP[status || '']?.label || status || '-'
}
async function loadOrderInfo() {
const raw = route.params.id
const id = Array.isArray(raw) ? raw[0] : raw
if (!id) return
orderLoading.value = true
try {
const data = await getSalesOrderDetail(Number(id))
orderInfo.value = data
await loadWorkOrders()
} catch (e) {
ElMessage.error('加载订单信息失败')
} finally {
orderLoading.value = false
}
}
async function loadWorkOrders() {
if (!orderInfo.value.orderCode) return
workOrderLoading.value = true
try {
const res = await getWorkOrderList({
sourceCode: orderInfo.value.orderCode,
pageNum: 1,
pageSize: 200
})
workOrders.value = res.rows
} catch (e) {
ElMessage.error('加载生产工单失败')
} finally {
workOrderLoading.value = false
}
}
function handleBack() {
router.back()
}
function handleViewWorkOrder(row: WorkOrder) {
router.push(`/production/work-order/view/${row.workorderId}`)
}
onMounted(() => {
loadOrderInfo()
})
</script>
<style scoped>
.progress-container {
padding: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.page-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
.card-title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
}
.info-label {
font-size: 12px;
color: #909399;
}
.info-value {
font-size: 14px;
color: #303133;
}
</style>

View File

@@ -144,7 +144,7 @@
<el-option <el-option
v-for="item in salesmanOptions" v-for="item in salesmanOptions"
:key="item.userId" :key="item.userId"
:label="item.userName || item.nickName || ''" :label="item.nickName || item.userName || ''"
:value="item.userId" :value="item.userId"
/> />
</el-select> </el-select>
@@ -318,7 +318,7 @@
empty-text="暂无物料明细请点击上方按钮添加物料" empty-text="暂无物料明细请点击上方按钮添加物料"
> >
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column prop="itemCode" label="物料编码" width="130"> <el-table-column prop="itemCode" label="物料编码" min-width="260">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="!isView"> <template v-if="!isView">
<el-input <el-input
@@ -347,7 +347,6 @@
:min="0" :min="0"
:precision="2" :precision="2"
:controls="false" :controls="false"
size="small"
placeholder="请输入" placeholder="请输入"
style="width: 100%" style="width: 100%"
@change="calculateLineAmount(row)" @change="calculateLineAmount(row)"
@@ -364,7 +363,6 @@
:min="0" :min="0"
:precision="2" :precision="2"
:controls="false" :controls="false"
size="small"
placeholder="请输入" placeholder="请输入"
style="width: 100%" style="width: 100%"
@change="calculateLineAmount(row)" @change="calculateLineAmount(row)"
@@ -383,7 +381,6 @@
<el-input <el-input
v-if="!isView" v-if="!isView"
v-model="row.qualityReq" v-model="row.qualityReq"
size="small"
placeholder="质量要求" placeholder="质量要求"
/> />
<span v-else>{{ row.qualityReq || '-' }}</span> <span v-else>{{ row.qualityReq || '-' }}</span>
@@ -394,7 +391,6 @@
<el-input <el-input
v-if="!isView" v-if="!isView"
v-model="row.remark" v-model="row.remark"
size="small"
placeholder="备注" placeholder="备注"
/> />
<span v-else>{{ row.remark || '-' }}</span> <span v-else>{{ row.remark || '-' }}</span>
@@ -825,11 +821,11 @@ function onDeptChange(deptId: number | undefined) {
} }
} }
/** 销售人员变更:同步名称 */ /** 销售人员变更:同步昵称nickName 优先于 userName */
function onSalesmanChange(userId: number | undefined) { function onSalesmanChange(userId: number | undefined) {
if (userId) { if (userId) {
const user = userOptions.value.find(u => u.userId === userId) const user = userOptions.value.find(u => u.userId === userId)
formData.salesmanName = user?.userName ?? user?.nickName ?? '' formData.salesmanName = user?.nickName ?? user?.userName ?? ''
} else { } else {
formData.salesmanName = '' formData.salesmanName = ''
} }

View File

@@ -561,28 +561,37 @@ onMounted(() => {
} }
.info-row { .info-row {
margin-bottom: 16px; margin-bottom: 12px;
} }
.info-item { .info-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
padding: 10px 14px;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 6px;
min-height: 60px;
} }
.info-label { .info-label {
font-size: 12px; font-size: 12px;
font-weight: 500;
color: #909399; color: #909399;
letter-spacing: 0.02em;
} }
.info-value { .info-value {
font-size: 14px; font-size: 14px;
color: #303133; color: #303133;
min-height: 20px;
word-break: break-all;
} }
.info-value.highlight { .info-value.highlight {
color: #409eff; color: #409eff;
font-weight: 500; font-weight: 600;
} }
.material-section { .material-section {