field-formatter.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /**
  2. * H5版本的字段格式化工具函数
  3. * 基于小程序版本改造,适配H5环境
  4. */
  5. /**
  6. * 获取字典翻译 - H5版本
  7. * @param {string} cbName - 字典名称
  8. * @param {string|number} value - 值
  9. * @param {string} cacheKey - 缓存键
  10. * @param {Map} dictCache - 字典缓存
  11. * @param {Function} updateCallback - 更新回调函数
  12. */
  13. const getDictTranslation = async (
  14. cbName,
  15. value,
  16. cacheKey,
  17. dictCache,
  18. updateCallback
  19. ) => {
  20. try {
  21. console.log(`🔍 获取字典翻译 [${cbName}:${value}]`);
  22. // 使用 loadObjpOpt 接口调用字典
  23. const result = await request.post(
  24. "/service?ssServ=loadObjpOpt&objectpickerdropdown1=1",
  25. {
  26. objectpickerparam: JSON.stringify({
  27. input: "false",
  28. codebook: cbName,
  29. }),
  30. objectpickertype: 2, // 2表示获取要回显的一项
  31. objectpickervalue: value, // 需回显的值
  32. },
  33. { loading: false, formData: true }
  34. );
  35. console.log(`字典翻译 [${cbName}:${value}] API返回:`, result);
  36. // 处理返回数据格式:{"result":{"622182":"会议室A"}}
  37. let translatedValue = value; // 默认返回原值
  38. if (result && result.data && result.data.result) {
  39. // 从result对象中根据value查找对应的翻译
  40. translatedValue = result.data.result[value] || value;
  41. console.log(`字典翻译结果: ${value} -> ${translatedValue}`);
  42. }
  43. dictCache.set(cacheKey, translatedValue);
  44. // 触发页面更新
  45. if (updateCallback) {
  46. updateCallback();
  47. }
  48. } catch (error) {
  49. console.error(`字典翻译失败 [${cbName}:${value}]:`, error);
  50. dictCache.set(cacheKey, value);
  51. }
  52. };
  53. /**
  54. * 格式化字段值(处理字典翻译、日期格式等)- H5版本
  55. * @param {Object} fieldObj - 字段对象 {field, value}
  56. * @param {Map} dictCache - 字典缓存
  57. * @param {Function} getDictTranslationFn - 字典翻译函数
  58. * @returns {string} 格式化后的值
  59. */
  60. const formatFieldValue = (fieldObj, dictCache, getDictTranslationFn) => {
  61. if (!fieldObj || !fieldObj.field || fieldObj.value === undefined) {
  62. return "";
  63. }
  64. const { field, value } = fieldObj;
  65. // 空值处理
  66. if (value === null || value === undefined || value === "") {
  67. return "";
  68. }
  69. // 如果字段有字典配置,进行字典翻译
  70. if (field.cbName) {
  71. const cacheKey = `${field.cbName}_${value}`;
  72. // 先从缓存中查找
  73. if (dictCache.has(cacheKey)) {
  74. return dictCache.get(cacheKey);
  75. }
  76. // 缓存中没有,异步获取翻译
  77. if (getDictTranslationFn) {
  78. getDictTranslationFn(field.cbName, value, cacheKey, dictCache);
  79. }
  80. // 返回原值作为临时显示
  81. return value;
  82. }
  83. // 时间格式化(优先检查fmt字段)
  84. if (field.fmt) {
  85. return h5FormatDate(value, field.fmt);
  86. }
  87. // 日期格式化(兼容旧逻辑)
  88. if (
  89. field.type === "date" ||
  90. field.name.toLowerCase().includes("date") ||
  91. field.name.toLowerCase().includes("time")
  92. ) {
  93. return h5FormatDate(value);
  94. }
  95. // 数字格式化
  96. if (field.type === "number" && typeof value === "number") {
  97. return value.toLocaleString();
  98. }
  99. // 布尔值格式化
  100. if (field.type === "boolean" || typeof value === "boolean") {
  101. return value ? "是" : "否";
  102. }
  103. // 默认返回字符串
  104. return String(value);
  105. };
  106. /**
  107. * 规范化日期字符串,兼容异常空白符和乱码分隔符
  108. * @param {string|Date|number} value - 原始日期值
  109. * @returns {string} 规范化后的日期字符串
  110. */
  111. const normalizeDateInput = (value) => {
  112. if (value === null || value === undefined) return "";
  113. return (
  114. String(value)
  115. // 功能说明:兼容接口里被错误转码的窄空格(如  PM),避免标题时间无法格式化 by xu 2026-03-06
  116. .replace(/ /g, " ")
  117. .replace(/[\u00A0\u1680\u180E\u2000-\u200D\u202F\u205F\u3000]/g, " ")
  118. .replace(/(\d)(AM|PM)$/i, "$1 $2")
  119. .replace(/\s+/g, " ")
  120. .trim()
  121. );
  122. };
  123. /**
  124. * 解析特殊日期格式:Jul 4, 2025, 6:00:00 AM
  125. * @param {string} dateStr - 日期字符串
  126. * @returns {object|null} 解析后的日期对象或null
  127. */
  128. const parseSpecialDateFormat = (dateStr) => {
  129. try {
  130. const normalizedDateStr = normalizeDateInput(dateStr);
  131. // 匹配格式:Jul 4, 2025, 6:00:00 AM
  132. const regex =
  133. /^(\w{3})\s+(\d{1,2}),\s+(\d{4}),\s+(\d{1,2}):(\d{2}):(\d{2})\s+(AM|PM)$/i;
  134. const match = normalizedDateStr.match(regex);
  135. if (!match) return null;
  136. const [, monthStr, day, year, hour, minute, second, ampmRaw] = match;
  137. const ampm = String(ampmRaw).toUpperCase();
  138. // 月份映射
  139. const months = {
  140. Jan: 0,
  141. Feb: 1,
  142. Mar: 2,
  143. Apr: 3,
  144. May: 4,
  145. Jun: 5,
  146. Jul: 6,
  147. Aug: 7,
  148. Sep: 8,
  149. Oct: 9,
  150. Nov: 10,
  151. Dec: 11,
  152. };
  153. const month = months[monthStr];
  154. if (month === undefined) return null;
  155. // 处理12小时制
  156. let hour24 = parseInt(hour);
  157. if (ampm === "PM" && hour24 !== 12) {
  158. hour24 += 12;
  159. } else if (ampm === "AM" && hour24 === 12) {
  160. hour24 = 0;
  161. }
  162. return {
  163. year: parseInt(year),
  164. month: month,
  165. day: parseInt(day),
  166. hour: hour24,
  167. minute: parseInt(minute),
  168. second: parseInt(second),
  169. };
  170. } catch (error) {
  171. console.warn("解析特殊日期格式失败:", dateStr, error);
  172. return null;
  173. }
  174. };
  175. /**
  176. * 格式化日期对象
  177. * @param {object} dateObj - 日期对象 {year, month, day, hour, minute, second}
  178. * @param {string} format - 格式字符串
  179. * @returns {string} 格式化后的字符串
  180. */
  181. const formatDateObject = (dateObj, format) => {
  182. const { year, month, day, hour, minute, second } = dateObj;
  183. return format
  184. .replace(/YYYY/g, year)
  185. .replace(/yyyy/g, year)
  186. .replace(/MM/g, String(month + 1).padStart(2, "0"))
  187. .replace(/DD/g, String(day).padStart(2, "0"))
  188. .replace(/dd/g, String(day).padStart(2, "0"))
  189. .replace(/HH/g, String(hour).padStart(2, "0"))
  190. .replace(/hh/g, String(hour).padStart(2, "0"))
  191. .replace(/mm/g, String(minute).padStart(2, "0"))
  192. .replace(/ss/g, String(second).padStart(2, "0"));
  193. };
  194. /**
  195. * 增强的日期格式化函数 - H5版本
  196. * @param {string|Date} date - 日期,支持多种格式如 "Jul 4, 2025, 6:00:00 AM"
  197. * @param {string} format - 格式,如 'YYYY-MM-DD HH:mm:ss'
  198. * @returns {string} 格式化后的日期
  199. */
  200. const h5FormatDate = (date, format = "YYYY-MM-DD HH:mm:ss") => {
  201. if (!date) return "";
  202. try {
  203. const normalizedDate = normalizeDateInput(date);
  204. console.log(
  205. `🔧 h5FormatDate 开始处理:`,
  206. normalizedDate,
  207. "目标格式:",
  208. format
  209. );
  210. // 手动解析特殊格式:Jul 4, 2025, 6:00:00 AM
  211. const parseResult = parseSpecialDateFormat(normalizedDate);
  212. if (parseResult) {
  213. console.log(`🔧 手动解析成功:`, parseResult);
  214. const result = formatDateObject(parseResult, format);
  215. console.log(`✅ 手动格式化结果:`, result);
  216. return result;
  217. }
  218. // 使用 Day.js 解析日期(更强大的日期解析能力)
  219. if (typeof window !== "undefined" && window.dayjs) {
  220. const dayObj = window.dayjs(normalizedDate);
  221. console.log(`🔧 使用 Day.js 解析:`, dayObj.isValid(), dayObj.toString());
  222. if (dayObj.isValid()) {
  223. const result = dayObj.format(format);
  224. console.log(`✅ Day.js 格式化结果:`, result);
  225. return result;
  226. }
  227. }
  228. // 降级到原生 Date(如果 Day.js 未加载)
  229. console.log(`🔧 Day.js 未加载,使用原生 Date`);
  230. const d = new Date(normalizedDate);
  231. console.log(`🔧 解析后的日期对象:`, d, "是否有效:", !isNaN(d.getTime()));
  232. // 检查日期是否有效
  233. if (isNaN(d.getTime())) {
  234. console.warn("❌ 无效日期格式:", normalizedDate);
  235. return normalizedDate; // 无效日期返回原值
  236. }
  237. const year = d.getFullYear();
  238. const month = String(d.getMonth() + 1).padStart(2, "0");
  239. const day = String(d.getDate()).padStart(2, "0");
  240. const hours = String(d.getHours()).padStart(2, "0");
  241. const minutes = String(d.getMinutes()).padStart(2, "0");
  242. const seconds = String(d.getSeconds()).padStart(2, "0");
  243. // 支持多种格式模式
  244. let result = format
  245. .replace(/YYYY/g, year)
  246. .replace(/yyyy/g, year)
  247. .replace(/MM/g, month)
  248. .replace(/DD/g, day)
  249. .replace(/dd/g, day)
  250. .replace(/HH/g, hours)
  251. .replace(/hh/g, hours)
  252. .replace(/mm/g, minutes)
  253. .replace(/ss/g, seconds);
  254. console.log(`日期格式化: ${date} -> ${result} (格式: ${format})`);
  255. return result;
  256. } catch (error) {
  257. console.error("日期格式化异常:", date, format, error);
  258. return date; // 异常时返回原值
  259. }
  260. };
  261. /**
  262. * 异步格式化字段值 - 支持字典转换
  263. * @param {object} fieldData - 字段数据 {field, value}
  264. * @param {Map} dictCache - 字典缓存
  265. * @returns {Promise<string>} 格式化后的值
  266. */
  267. const formatFieldValueAsync = async (fieldData, dictCache) => {
  268. if (!fieldData || !fieldData.field) {
  269. return fieldData?.value || "";
  270. }
  271. const { field, value } = fieldData;
  272. // 空值处理
  273. if (value === null || value === undefined || value === "") {
  274. return "";
  275. }
  276. // 优先处理时间格式化(如果字段有fmt属性)
  277. if (field.fmt) {
  278. try {
  279. console.log(
  280. `🕐 格式化时间字段 [${field.name}]:`,
  281. value,
  282. "格式:",
  283. field.fmt
  284. );
  285. console.log(`🔧 调用 h5FormatDate 前...`);
  286. const formattedTime = h5FormatDate(value, field.fmt);
  287. console.log(`🔧 调用 h5FormatDate 后,结果:`, formattedTime);
  288. console.log(`✅ 时间格式化结果:`, formattedTime);
  289. return formattedTime;
  290. } catch (error) {
  291. console.error("❌ 时间格式化失败:", field.name, value, error);
  292. // 格式化失败,返回原值
  293. return value;
  294. }
  295. }
  296. // 如果字段有字典配置,进行字典转换
  297. if (field.cbName) {
  298. const cacheKey = `${field.cbName}_${value}`;
  299. // 先从缓存中查找
  300. if (dictCache && dictCache.has(cacheKey)) {
  301. return dictCache.get(cacheKey);
  302. }
  303. // 缓存中没有,异步获取翻译
  304. try {
  305. const result = await request.post(
  306. `/service?ssServ=loadObjpOpt&objectpickerdropdown1=1`,
  307. {
  308. objectpickerparam: JSON.stringify({
  309. input: "false",
  310. codebook: field.cbName,
  311. }),
  312. objectpickertype: 2,
  313. objectpickervalue: value,
  314. },
  315. {
  316. loading: false,
  317. formData: true,
  318. }
  319. );
  320. if (
  321. result &&
  322. result.data &&
  323. result.data.result &&
  324. result.data.result[value]
  325. ) {
  326. const translatedValue = result.data.result[value];
  327. // 缓存结果
  328. if (dictCache) {
  329. dictCache.set(cacheKey, translatedValue);
  330. }
  331. return translatedValue;
  332. }
  333. } catch (error) {
  334. console.warn("字典转换失败:", field.cbName, value, error);
  335. }
  336. // 转换失败,返回原值
  337. return value;
  338. }
  339. // 日期格式化
  340. if (field.type === 3 && field.fmt) {
  341. return h5FormatDate(value, field.fmt);
  342. }
  343. // 数字格式化
  344. if (field.type === 2 && typeof value === "number") {
  345. return value.toLocaleString();
  346. }
  347. // 默认返回字符串
  348. return value.toString();
  349. };
  350. /**
  351. * 获取字典所有选项 - 用于下拉菜单
  352. * @param {string} cbName - 字典名称
  353. * @param {Map} dictCache - 字典缓存
  354. * @returns {Promise<Array>} 选项列表 [{n: '显示名', v: '值'}]
  355. */
  356. const getDictOptions = async (cbName, dictCache) => {
  357. const cacheKey = `options_${cbName}`;
  358. // 检查缓存
  359. if (dictCache && dictCache.has(cacheKey)) {
  360. return dictCache.get(cacheKey);
  361. }
  362. try {
  363. console.log(`🔍 获取字典选项 [${cbName}]`);
  364. const result = await request.post(
  365. `/service?ssServ=loadObjpOpt&objectpickerdropdown1=1`,
  366. {
  367. objectpickerparam: JSON.stringify({
  368. input: "false",
  369. codebook: cbName,
  370. }),
  371. objectpickertype: 1,
  372. objectpickersearchAll: 1,
  373. },
  374. {
  375. loading: false,
  376. formData: true,
  377. }
  378. );
  379. console.log(`字典选项 [${cbName}] API返回:`, result);
  380. if (result && result.data && result.data.result) {
  381. const options = Object.entries(result.data.result).map(
  382. ([value, label]) => ({
  383. n: label,
  384. v: value,
  385. })
  386. );
  387. // 缓存结果
  388. if (dictCache) {
  389. dictCache.set(cacheKey, options);
  390. }
  391. return options;
  392. }
  393. return [];
  394. } catch (error) {
  395. console.error(`❌ 获取字典选项失败 [${cbName}]:`, error);
  396. return [];
  397. }
  398. };
  399. /**
  400. * 格式化对象列表数据 - 处理API返回的objectList
  401. * @param {Array} objectList - 原始对象列表
  402. * @param {Map} dictCache - 字典缓存
  403. * @returns {Promise<Array>} 格式化后的列表
  404. */
  405. const formatObjectList = async (objectList, dictCache) => {
  406. if (!Array.isArray(objectList)) return [];
  407. const formattedList = [];
  408. for (const item of objectList) {
  409. const formattedItem = { ...item };
  410. // 格式化 first 字段
  411. if (item.first) {
  412. formattedItem.firstDisplay = await formatFieldValueAsync(
  413. item.first,
  414. dictCache
  415. );
  416. } else if (
  417. item.title &&
  418. typeof item.title === "object" &&
  419. item.title.val !== undefined
  420. ) {
  421. // 功能说明:兼容 title={val,fmt} 结构,标题字段为日期时间时按 fmt 格式化,避免卡片标题直接显示原始时间串 by xu 2026-03-06
  422. if (item.title.fmt) {
  423. formattedItem.firstDisplay = h5FormatDate(
  424. item.title.val,
  425. item.title.fmt
  426. );
  427. } else {
  428. formattedItem.firstDisplay =
  429. item.title.val === undefined || item.title.val === null
  430. ? ""
  431. : String(item.title.val);
  432. }
  433. } else if (typeof item.title === "string") {
  434. // 兼容 title 直出字符串结构 by xu 2026-03-03
  435. formattedItem.firstDisplay = item.title;
  436. }
  437. // 格式化 second 字段
  438. if (item.second) {
  439. formattedItem.secondDisplay = await formatFieldValueAsync(
  440. item.second,
  441. dictCache
  442. );
  443. } else {
  444. // 兼容 abs/desc/summary 结构(移动端接口常见) by xu 2026-03-03
  445. const secondFallback = item.abs ?? item.desc ?? item.summary;
  446. if (secondFallback !== undefined && secondFallback !== null) {
  447. formattedItem.secondDisplay = String(secondFallback);
  448. }
  449. }
  450. // 格式化 third 字段组
  451. if (item.third && Array.isArray(item.third)) {
  452. formattedItem.thirdDisplay = [];
  453. for (const group of item.third) {
  454. const formattedGroup = [];
  455. for (const fieldData of group) {
  456. const displayValue = await formatFieldValueAsync(
  457. fieldData,
  458. dictCache
  459. );
  460. formattedGroup.push({
  461. ...fieldData,
  462. displayValue,
  463. });
  464. }
  465. formattedItem.thirdDisplay.push(formattedGroup);
  466. }
  467. } else if (Array.isArray(item.catList)) {
  468. // 功能说明:兼容 catList 结构,若有 fmt 则按日期格式化,避免列表属性时间字段直接显示 Oracle 原始串 by xu 2026-03-06
  469. const formattedCatList = [];
  470. for (const catItem of item.catList) {
  471. const fieldData = {
  472. field: {
  473. desc: String(catItem?.desc || ""),
  474. fmt: catItem?.fmt || "",
  475. },
  476. value: catItem?.val,
  477. };
  478. const displayValue = await formatFieldValueAsync(fieldData, dictCache);
  479. formattedCatList.push({
  480. field: {
  481. desc: String(catItem?.desc || ""),
  482. },
  483. value: catItem?.val,
  484. displayValue,
  485. });
  486. }
  487. formattedItem.thirdDisplay = [formattedCatList];
  488. }
  489. formattedList.push(formattedItem);
  490. }
  491. return formattedList;
  492. };
  493. // 导出到全局
  494. window.H5FieldFormatter = {
  495. getDictTranslation,
  496. formatFieldValue,
  497. formatFieldValueAsync,
  498. formatDate: h5FormatDate,
  499. getDictOptions,
  500. formatObjectList,
  501. };
  502. // 兼容性:也导出为全局函数
  503. window.getDictTranslation = getDictTranslation;
  504. window.formatFieldValue = formatFieldValue;
  505. window.formatFieldValueAsync = formatFieldValueAsync;
  506. window.getDictOptions = getDictOptions;
  507. window.formatObjectList = formatObjectList;
  508. console.log("✅ H5字段格式化工具加载完成");