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

​
2 months ago
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 days ago
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
2 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <template>
  2. <div class="po-order-detail">
  3. <!--
  4. 订单详情动态渲染区(按字段权限 schema 控制)
  5. 规则:
  6. 1) 普通字段固定 1 格(24 栅格中的 4)=> 一行 6 格;
  7. 2) Vendor Name 固定 2 格(8);
  8. 3) Comments 固定全行(24)。
  9. -->
  10. <el-form label-position="top" class="detail-form">
  11. <el-row :gutter="20">
  12. <el-col
  13. v-for="field in displayFields"
  14. :key="getFieldKey(field)"
  15. :span="getFieldSpan(field)">
  16. <el-form-item :label="getFieldLabel(field)">
  17. <el-input :value="getFieldDisplayValue(field)" disabled />
  18. </el-form-item>
  19. </el-col>
  20. </el-row>
  21. </el-form>
  22. </div>
  23. </template>
  24. <script>
  25. import { formatPoOrderTypeLabel } from './poOrderTypeOptions.js'
  26. export default {
  27. name: 'PoOrderDetail',
  28. props: {
  29. detailData: {
  30. type: Object,
  31. default: () => ({})
  32. },
  33. schemaFields: {
  34. type: Array,
  35. default: () => []
  36. },
  37. locale: {
  38. type: String,
  39. default: 'zh'
  40. }
  41. },
  42. data () {
  43. return {
  44. labelMap: {
  45. 'PO No': 'PO No',
  46. 'PO号': 'PO No',
  47. '订单类型': 'Order Type',
  48. '类型': 'Type',
  49. 'Customer': 'Customer',
  50. '客户': 'Customer',
  51. 'Buyer': 'Buyer',
  52. '采购员': 'Buyer',
  53. 'Vendor Name': 'Vendor Name',
  54. '供应商名称': 'Vendor Name',
  55. 'V_Short_Office': 'V_Short_Office',
  56. 'V_Short_NPC': 'V_Short_NPC',
  57. 'Part No': 'Part No',
  58. '产品编码': 'Part No',
  59. 'Item': 'Item',
  60. '项目': 'Item',
  61. 'Category': 'Category',
  62. '品类': 'Category',
  63. 'SKU': 'SKU',
  64. 'Unit Price': 'Unit Price',
  65. '单价': 'Unit Price',
  66. 'PO Qty': 'PO Qty',
  67. '数量': 'PO Qty',
  68. 'PO Value': 'PO Value',
  69. '金额': 'PO Value',
  70. 'A-Qty': 'A-Qty',
  71. 'A-Value': 'A-Value',
  72. 'Departure Port': 'Departure Port',
  73. '出发港': 'Departure Port',
  74. 'Destination Port': 'Destination Port',
  75. '目的港': 'Destination Port',
  76. 'Method': 'Method',
  77. '运输方式': 'Method',
  78. 'Buyer Release Date': 'Buyer Release Date',
  79. '采购发布日期': 'Buyer Release Date',
  80. 'Planner Release Date': 'Planner Release Date',
  81. '计划发布日期': 'Planner Release Date',
  82. 'Status': 'Status',
  83. '状态': 'Status',
  84. 'PI Confirmed Date': 'PI Confirmed Date',
  85. 'PI确认日期': 'PI Confirmed Date',
  86. 'Original Arrival Date': 'Original Arrival Date',
  87. '原始到货日期': 'Original Arrival Date',
  88. 'Final Confirmed Arrival Date': 'Final Confirmed Arrival Date',
  89. '最终确认到货日期': 'Final Confirmed Arrival Date',
  90. 'PI Confirmed CRD': 'PI Confirmed CRD',
  91. 'Original CRD': 'Original CRD',
  92. 'Inspection Date': 'Inspection Date',
  93. '验货日期': 'Inspection Date',
  94. 'Actual Completion Date': 'Actual Completion Date',
  95. '实际完成日期': 'Actual Completion Date',
  96. 'ETA': 'ETA',
  97. 'Flexport Id': 'Flexport Id',
  98. 'On Board Date': 'On Board Date',
  99. '装船日期': 'On Board Date',
  100. 'Doc Sent': 'Doc Sent',
  101. 'Telex Release': 'Telex Release',
  102. 'Advance Payment': 'Advance Payment',
  103. '预付款': 'Advance Payment',
  104. 'Advanc Paid': 'Advanc Paid',
  105. 'Create Date': 'Create Date',
  106. '创建日期': 'Create Date',
  107. 'M-Planner': 'M-Planner',
  108. '计划员': 'M-Planner',
  109. 'Edit Date': 'Edit Date',
  110. '编辑日期': 'Edit Date',
  111. 'Last Editor': 'Last Editor',
  112. '最后编辑人': 'Last Editor',
  113. 'Remark Delay': 'Remark Delay',
  114. '延迟备注': 'Remark Delay',
  115. 'Comments': 'Comments',
  116. '备注': 'Comments',
  117. 'Inspection Completed Qty': 'Inspection Completed Qty',
  118. '已验货数量': 'Inspection Completed Qty'
  119. }
  120. }
  121. },
  122. computed: {
  123. /**
  124. * 详情页签显示字段(基于权限 schema 动态决定)。
  125. * 处理逻辑:
  126. * 1) 仅保留 visibleFlag=Y 的字段;
  127. * 2) 同一 field_code 去重,防止异常配置导致重复渲染;
  128. * 3) 按 sortNo 升序,保证与列表页/权限配置顺序一致。
  129. * 返回值:用于页面渲染的字段数组(已过滤、去重、排序)。
  130. * 边界处理:schemaFields 非数组时直接返回空数组,避免页面渲染报错。
  131. */
  132. displayFields () {
  133. if (!Array.isArray(this.schemaFields)) {
  134. return []
  135. }
  136. const seen = new Set()
  137. const visible = this.schemaFields.filter(field => {
  138. if (!field) {
  139. return false
  140. }
  141. const visibleFlag = String(field.visibleFlag || 'Y').toUpperCase()
  142. if (visibleFlag !== 'Y') {
  143. return false
  144. }
  145. const key = this.getFieldKey(field)
  146. if (!key || seen.has(key)) {
  147. return false
  148. }
  149. seen.add(key)
  150. return true
  151. })
  152. return visible.sort((a, b) => {
  153. const left = Number(a && a.sortNo)
  154. const right = Number(b && b.sortNo)
  155. const la = Number.isFinite(left) ? left : Number.MAX_SAFE_INTEGER
  156. const rb = Number.isFinite(right) ? right : Number.MAX_SAFE_INTEGER
  157. if (la !== rb) {
  158. return la - rb
  159. }
  160. return this.getFieldKey(a).localeCompare(this.getFieldKey(b))
  161. })
  162. }
  163. },
  164. methods: {
  165. /**
  166. * 生成字段唯一键。
  167. * 优先级:fieldCode > fieldProp > fieldLabel。
  168. * 入参:field(单个字段 schema 对象,允许为空)。
  169. * 返回值:稳定字符串 key;若字段信息缺失则返回空字符串。
  170. * 边界处理:字段为空时走空字符串回退,避免 v-for key 异常。
  171. */
  172. getFieldKey (field) {
  173. const code = this.getFieldCode(field)
  174. if (code) {
  175. return code
  176. }
  177. const prop = this.getFieldProp(field)
  178. if (prop) {
  179. return prop
  180. }
  181. return String((field && field.fieldLabel) || '').trim()
  182. },
  183. /**
  184. * 解析字段编码(snake_case)。
  185. * 入参兼容:
  186. * 1) 后端直接返回 fieldCode;
  187. * 2) 无 fieldCode 时由 fieldProp 反推 snake_case。
  188. * 入参:field(字段 schema 对象)。
  189. * 返回值:字段编码字符串;无法解析时返回空字符串。
  190. * 边界处理:field 为空或关键属性缺失时不抛错,统一回空串。
  191. */
  192. getFieldCode (field) {
  193. if (!field) {
  194. return ''
  195. }
  196. const code = String(field.fieldCode || '').trim()
  197. if (code) {
  198. return code.toLowerCase()
  199. }
  200. const prop = String(field.fieldProp || '').trim()
  201. return prop ? this.camelToSnake(prop) : ''
  202. },
  203. /**
  204. * 解析字段属性(camelCase),用于读取当前行对象值。
  205. * 入参兼容:
  206. * 1) 后端直接返回 fieldProp;
  207. * 2) 无 fieldProp 时由 fieldCode 反推 camelCase。
  208. * 入参:field(字段 schema 对象)。
  209. * 返回值:字段属性名;无法解析时返回空字符串。
  210. * 边界处理:field 为空或编码异常时统一回空串,避免取值报错。
  211. */
  212. getFieldProp (field) {
  213. if (!field) {
  214. return ''
  215. }
  216. const prop = String(field.fieldProp || '').trim()
  217. if (prop) {
  218. return prop
  219. }
  220. const code = String(field.fieldCode || '').trim()
  221. return code ? this.snakeToCamel(code) : ''
  222. },
  223. /**
  224. * 获取展示标签。
  225. * 优先 fieldLabel;若异常缺失则回退字段属性名,避免出现空标签。
  226. * 入参:field(字段 schema 对象)。
  227. * 返回值:展示标签字符串。
  228. * 边界处理:label/prop/code 都缺失时返回空串,不中断渲染流程。
  229. */
  230. getFieldLabel (field) {
  231. const label = String((field && field.fieldLabel) || '').trim()
  232. if (label) {
  233. if (this.locale === 'en' && this.labelMap[label]) {
  234. return this.labelMap[label]
  235. }
  236. return label
  237. }
  238. return this.getFieldProp(field) || this.getFieldCode(field)
  239. },
  240. /**
  241. * 按业务布局规则计算栅格宽度:
  242. * - Vendor Name(supplier_name): 2格 => span=8
  243. * - Comments(remark): 全行 => span=24
  244. * - 其他字段: 1格 => span=4
  245. * 入参:field(字段 schema 对象)。
  246. * 返回值:Element 栅格 span 数值。
  247. * 边界处理:未匹配特殊字段时统一走默认 1 格,保证布局稳定。
  248. */
  249. getFieldSpan (field) {
  250. const code = this.getFieldCode(field)
  251. const label = this.getFieldLabel(field).trim().toLowerCase()
  252. if (code === 'supplier_name' || label === 'vendor name') {
  253. return 8
  254. }
  255. if (code === 'remark' || code === 'comments' || label === 'comments') {
  256. return 24
  257. }
  258. return 4
  259. },
  260. /**
  261. * 读取详情字段原始值。
  262. * 读取顺序:
  263. * 1) detailData[fieldProp]
  264. * 2) detailData[fieldCode](兼容少量 snake_case 回传)
  265. * 3) detailData.extValues[fieldCode/fieldProp](兼容扩展字段)
  266. * 入参:field(字段 schema 对象)。
  267. * 返回值:原始字段值;取不到值时返回空字符串。
  268. * 边界处理:detailData/extValues 为空或结构异常时安全降级为空串。
  269. */
  270. getFieldRawValue (field) {
  271. const row = this.detailData || {}
  272. const prop = this.getFieldProp(field)
  273. const code = this.getFieldCode(field)
  274. if (prop && row[prop] !== undefined && row[prop] !== null) {
  275. return row[prop]
  276. }
  277. if (code && row[code] !== undefined && row[code] !== null) {
  278. return row[code]
  279. }
  280. const extValues = row.extValues && typeof row.extValues === 'object' ? row.extValues : null
  281. if (extValues) {
  282. if (code && extValues[code] !== undefined && extValues[code] !== null) {
  283. return extValues[code]
  284. }
  285. if (prop && extValues[prop] !== undefined && extValues[prop] !== null) {
  286. return extValues[prop]
  287. }
  288. }
  289. return ''
  290. },
  291. /**
  292. * 获取最终显示值。
  293. * 特殊处理:
  294. * 1) order_type/orderType 走统一枚举格式化;
  295. * 2) 其余字段按原值输出,空值统一返回空字符串。
  296. * 入参:field(字段 schema 对象)。
  297. * 返回值:可直接绑定到输入框的展示值。
  298. * 边界处理:空值统一归一为空串,避免界面出现 undefined/null。
  299. */
  300. getFieldDisplayValue (field) {
  301. const raw = this.getFieldRawValue(field)
  302. const code = this.getFieldCode(field)
  303. const prop = this.getFieldProp(field)
  304. // rqrq - 详情仅展示英文,保留 detailData.status 原值供业务使用。
  305. if (code === 'status' || prop === 'status') {
  306. const row = this.detailData || {}
  307. return row.statusDisplay != null ? row.statusDisplay : raw
  308. }
  309. if (raw === null || raw === undefined) {
  310. return ''
  311. }
  312. if (code === 'order_type' || prop === 'orderType') {
  313. return formatPoOrderTypeLabel(raw)
  314. }
  315. return raw
  316. },
  317. /**
  318. * snake_case 转 camelCase。
  319. * 入参:snake(下划线字段名)。
  320. * 返回值:camelCase 字段名;入参为空时原样返回。
  321. */
  322. snakeToCamel (snake) {
  323. if (!snake || !snake.includes('_')) {
  324. return snake
  325. }
  326. return snake.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())
  327. },
  328. /**
  329. * camelCase 转 snake_case。
  330. * 入参:camel(驼峰字段名)。
  331. * 返回值:snake_case 字段名;入参为空时返回空字符串。
  332. */
  333. camelToSnake (camel) {
  334. if (!camel) {
  335. return ''
  336. }
  337. return camel.replace(/([A-Z])/g, '_$1').toLowerCase()
  338. }
  339. }
  340. }
  341. </script>
  342. <style scoped lang="scss">
  343. .po-order-detail {
  344. width: 100%;
  345. }
  346. .detail-form {
  347. width: 100%;
  348. }
  349. .el-form-item {
  350. margin-bottom: 6px;
  351. }
  352. /deep/ .el-form-item__label {
  353. font-size: 14px;
  354. color: #606266;
  355. line-height: 1.5;
  356. padding-bottom: 6px;
  357. font-weight: normal;
  358. }
  359. /deep/ .el-input__inner {
  360. height: 32px;
  361. line-height: 32px;
  362. font-size: 13px;
  363. border-radius: 4px;
  364. border: 1px solid #dcdfe6;
  365. background-color: #fff;
  366. }
  367. /deep/ .el-input.is-disabled .el-input__inner {
  368. background-color: #f5f7fa;
  369. border-color: #e4e7ed;
  370. color: #c0c4cc;
  371. cursor: not-allowed;
  372. }
  373. /deep/ .el-input__inner:focus {
  374. border-color: #409eff;
  375. outline: none;
  376. }
  377. </style>