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