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
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>
|