You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

394 lines
12 KiB

<template>
<div class="po-order-detail">
<!--
订单详情动态渲染区(按字段权限 schema 控制)
规则:
1) 普通字段固定 1 格(24 栅格中的 4)=> 一行 6 格;
2) Vendor Name 固定 2 格(8);
3) Comments 固定全行(24)。
-->
<el-form label-position="top" class="detail-form">
<el-row :gutter="20">
<el-col
v-for="field in displayFields"
:key="getFieldKey(field)"
:span="getFieldSpan(field)">
<el-form-item :label="getFieldLabel(field)">
<el-input :value="getFieldDisplayValue(field)" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script>
import { formatPoOrderTypeLabel } from './poOrderTypeOptions.js'
export default {
name: 'PoOrderDetail',
props: {
detailData: {
type: Object,
default: () => ({})
},
schemaFields: {
type: Array,
default: () => []
},
locale: {
type: String,
default: 'zh'
}
},
data () {
return {
labelMap: {
'PO No': 'PO No',
'PO号': 'PO No',
'订单类型': 'Order Type',
'类型': 'Type',
'Customer': 'Customer',
'客户': 'Customer',
'Buyer': 'Buyer',
'采购员': 'Buyer',
'Vendor Name': 'Vendor Name',
'供应商名称': 'Vendor Name',
'V_Short_Office': 'V_Short_Office',
'V_Short_NPC': 'V_Short_NPC',
'Part No': 'Part No',
'产品编码': 'Part No',
'Item': 'Item',
'项目': 'Item',
'Category': 'Category',
'品类': 'Category',
'SKU': 'SKU',
'Unit Price': 'Unit Price',
'单价': 'Unit Price',
'PO Qty': 'PO Qty',
'数量': 'PO Qty',
'PO Value': 'PO Value',
'金额': 'PO Value',
'A-Qty': 'A-Qty',
'A-Value': 'A-Value',
'Departure Port': 'Departure Port',
'出发港': 'Departure Port',
'Destination Port': 'Destination Port',
'目的港': 'Destination Port',
'Method': 'Method',
'运输方式': 'Method',
'Buyer Release Date': 'Buyer Release Date',
'采购发布日期': 'Buyer Release Date',
'Planner Release Date': 'Planner Release Date',
'计划发布日期': 'Planner Release Date',
'Status': 'Status',
'状态': 'Status',
'PI Confirmed Date': 'PI Confirmed Date',
'PI确认日期': 'PI Confirmed Date',
'Original Arrival Date': 'Original Arrival Date',
'原始到货日期': 'Original Arrival Date',
'Final Confirmed Arrival Date': 'Final Confirmed Arrival Date',
'最终确认到货日期': 'Final Confirmed Arrival Date',
'PI Confirmed CRD': 'PI Confirmed CRD',
'Original CRD': 'Original CRD',
'Inspection Date': 'Inspection Date',
'验货日期': 'Inspection Date',
'Actual Completion Date': 'Actual Completion Date',
'实际完成日期': 'Actual Completion Date',
'ETA': 'ETA',
'Flexport Id': 'Flexport Id',
'On Board Date': 'On Board Date',
'装船日期': 'On Board Date',
'Doc Sent': 'Doc Sent',
'Telex Release': 'Telex Release',
'Advance Payment': 'Advance Payment',
'预付款': 'Advance Payment',
'Advanc Paid': 'Advanc Paid',
'Create Date': 'Create Date',
'创建日期': 'Create Date',
'M-Planner': 'M-Planner',
'计划员': 'M-Planner',
'Edit Date': 'Edit Date',
'编辑日期': 'Edit Date',
'Last Editor': 'Last Editor',
'最后编辑人': 'Last Editor',
'Remark Delay': 'Remark Delay',
'延迟备注': 'Remark Delay',
'Comments': 'Comments',
'备注': 'Comments',
'Inspection Completed Qty': 'Inspection Completed Qty',
'已验货数量': 'Inspection Completed Qty'
}
}
},
computed: {
/**
* 详情页签显示字段(基于权限 schema 动态决定)。
* 处理逻辑:
* 1) 仅保留 visibleFlag=Y 的字段;
* 2) 同一 field_code 去重,防止异常配置导致重复渲染;
* 3) 按 sortNo 升序,保证与列表页/权限配置顺序一致。
* 返回值:用于页面渲染的字段数组(已过滤、去重、排序)。
* 边界处理:schemaFields 非数组时直接返回空数组,避免页面渲染报错。
*/
displayFields () {
if (!Array.isArray(this.schemaFields)) {
return []
}
const seen = new Set()
const visible = this.schemaFields.filter(field => {
if (!field) {
return false
}
const visibleFlag = String(field.visibleFlag || 'Y').toUpperCase()
if (visibleFlag !== 'Y') {
return false
}
const key = this.getFieldKey(field)
if (!key || seen.has(key)) {
return false
}
seen.add(key)
return true
})
return visible.sort((a, b) => {
const left = Number(a && a.sortNo)
const right = Number(b && b.sortNo)
const la = Number.isFinite(left) ? left : Number.MAX_SAFE_INTEGER
const rb = Number.isFinite(right) ? right : Number.MAX_SAFE_INTEGER
if (la !== rb) {
return la - rb
}
return this.getFieldKey(a).localeCompare(this.getFieldKey(b))
})
}
},
methods: {
/**
* 生成字段唯一键。
* 优先级:fieldCode > fieldProp > fieldLabel。
* 入参:field(单个字段 schema 对象,允许为空)。
* 返回值:稳定字符串 key;若字段信息缺失则返回空字符串。
* 边界处理:字段为空时走空字符串回退,避免 v-for key 异常。
*/
getFieldKey (field) {
const code = this.getFieldCode(field)
if (code) {
return code
}
const prop = this.getFieldProp(field)
if (prop) {
return prop
}
return String((field && field.fieldLabel) || '').trim()
},
/**
* 解析字段编码(snake_case)。
* 入参兼容:
* 1) 后端直接返回 fieldCode;
* 2) 无 fieldCode 时由 fieldProp 反推 snake_case。
* 入参:field(字段 schema 对象)。
* 返回值:字段编码字符串;无法解析时返回空字符串。
* 边界处理:field 为空或关键属性缺失时不抛错,统一回空串。
*/
getFieldCode (field) {
if (!field) {
return ''
}
const code = String(field.fieldCode || '').trim()
if (code) {
return code.toLowerCase()
}
const prop = String(field.fieldProp || '').trim()
return prop ? this.camelToSnake(prop) : ''
},
/**
* 解析字段属性(camelCase),用于读取当前行对象值。
* 入参兼容:
* 1) 后端直接返回 fieldProp;
* 2) 无 fieldProp 时由 fieldCode 反推 camelCase。
* 入参:field(字段 schema 对象)。
* 返回值:字段属性名;无法解析时返回空字符串。
* 边界处理:field 为空或编码异常时统一回空串,避免取值报错。
*/
getFieldProp (field) {
if (!field) {
return ''
}
const prop = String(field.fieldProp || '').trim()
if (prop) {
return prop
}
const code = String(field.fieldCode || '').trim()
return code ? this.snakeToCamel(code) : ''
},
/**
* 获取展示标签。
* 优先 fieldLabel;若异常缺失则回退字段属性名,避免出现空标签。
* 入参:field(字段 schema 对象)。
* 返回值:展示标签字符串。
* 边界处理:label/prop/code 都缺失时返回空串,不中断渲染流程。
*/
getFieldLabel (field) {
const label = String((field && field.fieldLabel) || '').trim()
if (label) {
if (this.locale === 'en' && this.labelMap[label]) {
return this.labelMap[label]
}
return label
}
return this.getFieldProp(field) || this.getFieldCode(field)
},
/**
* 按业务布局规则计算栅格宽度:
* - Vendor Name(supplier_name): 2格 => span=8
* - Comments(remark): 全行 => span=24
* - 其他字段: 1格 => span=4
* 入参:field(字段 schema 对象)。
* 返回值:Element 栅格 span 数值。
* 边界处理:未匹配特殊字段时统一走默认 1 格,保证布局稳定。
*/
getFieldSpan (field) {
const code = this.getFieldCode(field)
const label = this.getFieldLabel(field).trim().toLowerCase()
if (code === 'supplier_name' || label === 'vendor name') {
return 8
}
if (code === 'remark' || code === 'comments' || label === 'comments') {
return 24
}
return 4
},
/**
* 读取详情字段原始值。
* 读取顺序:
* 1) detailData[fieldProp]
* 2) detailData[fieldCode](兼容少量 snake_case 回传)
* 3) detailData.extValues[fieldCode/fieldProp](兼容扩展字段)
* 入参:field(字段 schema 对象)。
* 返回值:原始字段值;取不到值时返回空字符串。
* 边界处理:detailData/extValues 为空或结构异常时安全降级为空串。
*/
getFieldRawValue (field) {
const row = this.detailData || {}
const prop = this.getFieldProp(field)
const code = this.getFieldCode(field)
if (prop && row[prop] !== undefined && row[prop] !== null) {
return row[prop]
}
if (code && row[code] !== undefined && row[code] !== null) {
return row[code]
}
const extValues = row.extValues && typeof row.extValues === 'object' ? row.extValues : null
if (extValues) {
if (code && extValues[code] !== undefined && extValues[code] !== null) {
return extValues[code]
}
if (prop && extValues[prop] !== undefined && extValues[prop] !== null) {
return extValues[prop]
}
}
return ''
},
/**
* 获取最终显示值。
* 特殊处理:
* 1) order_type/orderType 走统一枚举格式化;
* 2) 其余字段按原值输出,空值统一返回空字符串。
* 入参:field(字段 schema 对象)。
* 返回值:可直接绑定到输入框的展示值。
* 边界处理:空值统一归一为空串,避免界面出现 undefined/null。
*/
getFieldDisplayValue (field) {
const raw = this.getFieldRawValue(field)
const code = this.getFieldCode(field)
const prop = this.getFieldProp(field)
// rqrq - 详情仅展示英文,保留 detailData.status 原值供业务使用。
if (code === 'status' || prop === 'status') {
const row = this.detailData || {}
return row.statusDisplay != null ? row.statusDisplay : raw
}
if (raw === null || raw === undefined) {
return ''
}
if (code === 'order_type' || prop === 'orderType') {
return formatPoOrderTypeLabel(raw)
}
return raw
},
/**
* snake_case 转 camelCase。
* 入参:snake(下划线字段名)。
* 返回值:camelCase 字段名;入参为空时原样返回。
*/
snakeToCamel (snake) {
if (!snake || !snake.includes('_')) {
return snake
}
return snake.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())
},
/**
* camelCase 转 snake_case。
* 入参:camel(驼峰字段名)。
* 返回值:snake_case 字段名;入参为空时返回空字符串。
*/
camelToSnake (camel) {
if (!camel) {
return ''
}
return camel.replace(/([A-Z])/g, '_$1').toLowerCase()
}
}
}
</script>
<style scoped lang="scss">
.po-order-detail {
width: 100%;
}
.detail-form {
width: 100%;
}
.el-form-item {
margin-bottom: 6px;
}
/deep/ .el-form-item__label {
font-size: 14px;
color: #606266;
line-height: 1.5;
padding-bottom: 6px;
font-weight: normal;
}
/deep/ .el-input__inner {
height: 32px;
line-height: 32px;
font-size: 13px;
border-radius: 4px;
border: 1px solid #dcdfe6;
background-color: #fff;
}
/deep/ .el-input.is-disabled .el-input__inner {
background-color: #f5f7fa;
border-color: #e4e7ed;
color: #c0c4cc;
cursor: not-allowed;
}
/deep/ .el-input__inner:focus {
border-color: #409eff;
outline: none;
}
</style>