From 70aa5f0a79d88892745d17ce1b07d24387193a02 Mon Sep 17 00:00:00 2001 From: 王彪总 Date: Thu, 15 Oct 2026 12:23:11 +0800 Subject: [PATCH] 业务线 小程序不用写死,从后端接口获取 --- pages-sub/daily/maintain-manage/finish-plan-detail.vue | 375 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- stats.html | 4949 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 5323 insertions(+), 1 deletion(-) create mode 100644 stats.html diff --git a/pages-sub/daily/maintain-manage/finish-plan-detail.vue b/pages-sub/daily/maintain-manage/finish-plan-detail.vue index 0bf3ab0..8ed937b 100644 --- a/pages-sub/daily/maintain-manage/finish-plan-detail.vue +++ b/pages-sub/daily/maintain-manage/finish-plan-detail.vue @@ -98,6 +98,52 @@ :border="false" > + + + + + + + @@ -162,7 +208,12 @@ const getOrderDetail = async (planNo: string) => { console.log('接口返回:', res); // 修复2:正确解析接口数据(res.data.list 而非 res.list) - orderDetail.value = res.data?.list || res.list || []; + const list = res.data?.list || res.list || []; + // 预解析动态字段,避免模板里重复 JSON.parse + orderDetail.value = list.map(item => ({ + ...item, + fieldRows: (item.fields || []).map(buildFieldRow) + })); // 调试:打印格式化后的图片URL console.log('格式化后图片URL:', formatImageUrls(orderDetail.value[0]?.beginImgList || [])); @@ -174,6 +225,288 @@ const getOrderDetail = async (planNo: string) => { } }; +// ========== 动态配置字段(fieldValue)展示 ========== +// 解析口径与 PC 端 FieldValueCell.vue + WorkRecordTab.vue 保持一致: +// 后端 valueText 是「类型无关直拷、提交无校验」,历史格式与脏数据都可能出现,故所有解析一律降级、不抛异常。 +/** 空值 token:与 PC 的 EMPTY_TOKENS 一致 */ +const EMPTY_TOKENS = ['', '[]', '{}', 'null', 'undefined']; +/** 走「新类型」渲染的字段类型(5布尔/6多选/7图片/8附件/9关联记录/10链接/11地理位置) */ +const NEW_RENDER_TYPES = [5, 6, 7, 8, 9, 10, 11]; +const TRUTHY = ['是', 'true', '1', 'y', 'yes']; +const FALSY = ['否', 'false', '0', 'n', 'no']; + +/** 取原始文本,空/占位值归一成空串 */ +const rawTextOf = (item) => { + const text = String(item?.valueText ?? '').trim(); + return EMPTY_TOKENS.includes(text) ? '' : text; +}; + +/** JSON 数组解析;失败退化为逗号分隔(兼容历史枚举多选的存储格式) */ +const parseArray = (raw) => { + const text = String(raw ?? '').trim(); + if (!text) return []; + try { + const parsed = JSON.parse(text); + if (Array.isArray(parsed)) return parsed; + return parsed !== null && typeof parsed === 'object' ? [parsed] : []; + } catch (e) { + return text.split(',').map(s => s.trim()).filter(Boolean); + } +}; + +/** JSON 对象解析,非法一律退化成空对象 */ +const parseObject = (raw) => { + if (!raw) return {}; + try { + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch (e) { + return {}; + } +}; + +/** 静态选项 value -> label(后端仅 option_source=1 时回传 options,控体积) */ +const optionsMapOf = (item) => { + const map = {}; + parseArray(item?.options).forEach(o => { + if (o && o.value != null) map[String(o.value)] = String(o.label ?? o.value); + }); + return map; +}; + +/** + * 枚举值 -> 展示文本:优先字典(option_source=2),其次静态选项表(option_source=1),都取不到返回空串。 + * 后端「仅静态选项回传 options」就是给客户端渲染标签用的;取不到时调用方退回裸值(与 PC 的 formatValue 一致)。 + */ +const enumLabelOf = (item, value) => { + if (!value) return ''; + if (item.optionSource === 2 && item.dictType) { + const label = uni.$dict.getDictLabel(item.dictType, value); + if (label) return label; + } + return optionsMapOf(item)[value] ?? ''; +}; + +/** 旧类型(1数值/2文本/3单选枚举/4日期)与物料的原文直出,口径同 PC 的 formatValue */ +const legacyText = (item) => { + // 物料以值行的 materialId 判定(物料字段实际是 fieldType=3 + option_source=3,并非独立类型) + if (item.materialId != null) { + return `${item.materialName || ''}${item.quantity != null ? ' × ' + item.quantity : ''}${item.unit ? ' ' + item.unit : ''}`; + } + const raw = rawTextOf(item); + if (raw) return item.unit ? `${raw} ${item.unit}` : raw; + if (item.valueNumber != null) return item.unit ? `${item.valueNumber} ${item.unit}` : String(item.valueNumber); + return '-'; +}; + +const isUrl = (v) => /^(https?:)?\/\//i.test(String(v)) || String(v).startsWith('/'); + +/** 文件名取 URL 末段(存储不保留原始文件名) */ +const fileNameOf = (url) => { + const path = String(url).split('?')[0].split('#')[0]; + const name = path.substring(path.lastIndexOf('/') + 1); + try { + return decodeURIComponent(name) || url; + } catch (e) { + return name || url; + } +}; + +/** 多值元素归一化:兼容裸值("1")与带数量对象({value,label,quantity}) */ +const toMultiItem = (v) => { + if (v === null || typeof v !== 'object') { + return { value: String(v), label: '', quantity: null }; + } + const raw = v.value ?? v.id ?? v.label; + const qty = Number(v.quantity); + return { + value: raw == null ? '' : String(raw), + label: v.label != null ? String(v.label) : '', + quantity: v.quantity != null && Number.isFinite(qty) ? qty : null + }; +}; + +/** + * 多值(6多选 / 3历史多选 / 9关联记录)转标签文本。 + * 标签优先级:值行快照 label(9关联记录、6+needQuantity 会带)→ 字典 → 静态选项表 → 裸值。 + * 与 PC 的 DictTag/multiLabel 等价,且对带数量的项同样能显示标签。 + */ +const buildTags = (item) => { + return parseArray(item.valueText) + .map(toMultiItem) + .filter(i => i.value) + .map(i => { + const label = i.label || enumLabelOf(item, i.value) || i.value; + return { text: i.quantity == null ? label : `${label} ×${i.quantity}` }; + }); +}; + +/** 11 地理位置:JSON 对象 {lng,lat,address},兼容 "lng,lat" 裸文本 */ +const parseGeo = (source, ext) => { + let lng = null; + let lat = null; + let address = ''; + try { + const parsed = JSON.parse(source); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + lng = Number(parsed.lng ?? parsed.longitude); + lat = Number(parsed.lat ?? parsed.latitude); + address = parsed.address || ''; + } + } catch (e) { + // 非法 JSON:继续尝试「经度,纬度」文本形态 + } + if (!Number.isFinite(lng) || !Number.isFinite(lat)) { + const parts = String(source).split(',').map(s => Number(s.trim())); + if (parts.length >= 2 && parts.every(n => Number.isFinite(n))) { + lng = parts[0]; + lat = parts[1]; + address = ''; + } else { + return null; + } + } + const precision = Math.min(Math.max(Number(ext.precision) || 6, 0), 8); + const coord = `${lng.toFixed(precision)}, ${lat.toFixed(precision)}`; + return { lng, lat, address, text: address ? `${coord}(${address})` : coord }; +}; + +/** + * 把一个字段值行归一化成展示描述 { name, kind, text, tags, images, files, geo }。 + * 分发顺序与 PC 的 isNewTypeValue 完全一致:物料 → 枚举(3) → 新类型(5..11) → 旧类型原文直出。 + */ +const buildFieldRow = (item) => { + const row = { name: item.fieldName || item.fieldCode || '-', kind: 'text', text: '-', tags: [], images: [], files: [], geo: null }; + const raw = rawTextOf(item); + // ① 物料:优先级最高,即便凑巧带 multiple=1 也不当多值渲染 + if (item.materialId != null || item.fieldType == null) { + row.text = legacyText(item); + return row; + } + // ② 枚举(3):multiple=1 是历史多选(逗号分隔)走多值;multiple=0 单选按值查标签(字典/静态选项) + if (item.fieldType === 3) { + if (item.multiple !== 1) { + // 单选存的是 option.value,直接直出会显示成 "2 把"(工具=锄头)这种无意义文本, + // 故先查标签;option_source 为 3物料/4数据表/5SQL/6接口 时后端不回传选项表,查不到则退回 legacyText(与 PC 一致) + const label = enumLabelOf(item, raw); + row.text = label ? (item.unit ? `${label} ${item.unit}` : label) : legacyText(item); + return row; + } + row.kind = 'multi'; + row.tags = buildTags(item); + if (!row.tags.length) { + row.kind = 'text'; + row.text = raw || '-'; + } + return row; + } + // ③ 旧类型 1数值/2文本/4日期:原文直出 + if (!NEW_RENDER_TYPES.includes(item.fieldType)) { + row.text = legacyText(item); + return row; + } + const ext = parseObject(item.extConfig); + switch (item.fieldType) { + case 5: { // 布尔:识别不出真/假时退回原文,不臆测 + const lower = raw.toLowerCase(); + row.text = TRUTHY.includes(lower) ? '是' : (FALSY.includes(lower) ? '否' : (raw || '-')); + break; + } + case 6: // 多选 + row.kind = 'multi'; + row.tags = buildTags(item); + if (!row.tags.length) { + row.kind = 'text'; + row.text = raw || '-'; + } + break; + case 7: // 图片:只接受 URL,空则退回原文 + row.images = parseArray(item.valueText).map(v => String(v)).filter(isUrl); + row.kind = row.images.length ? 'image' : 'text'; + if (!row.images.length) row.text = raw || '-'; + break; + case 8: // 附件 + row.files = parseArray(item.valueText).map(v => String(v)).filter(isUrl) + .map(u => ({ url: u, name: fileNameOf(u) })); + row.kind = row.files.length ? 'file' : 'text'; + if (!row.files.length) row.text = raw || '-'; + break; + case 9: // 关联记录:name 随值落库当快照,缺 name 时显示裸 id(不回查选项) + row.tags = parseArray(item.valueText).map(v => { + if (v == null) return null; + if (typeof v === 'object') { + const id = v.id ?? v.value; + if (id == null) return null; + return { text: String(v.name ?? v.label ?? id) }; + } + return { text: String(v) }; + }).filter(Boolean); + row.kind = row.tags.length ? 'multi' : 'text'; + if (!row.tags.length) row.text = raw || '-'; + break; + case 10: { // 链接:仅 http/https,避免 javascript: 之类伪协议 + const pattern = typeof ext.urlPattern === 'string' && ext.urlPattern.includes('{value}') + ? ext.urlPattern.replace(/\{value\}/g, encodeURIComponent(raw)) + : raw; + row.kind = 'link'; + row.text = /^https?:\/\//i.test(pattern) ? pattern : (raw || '-'); + break; + } + case 11: { // 地理位置 + row.geo = raw ? parseGeo(raw, ext) : null; + row.kind = row.geo ? 'geo' : 'text'; + row.text = row.geo ? row.geo.text : (raw || '-'); + break; + } + default: + row.text = legacyText(item); + } + return row; +}; + +/** 8 附件:下载后由系统应用打开(微信小程序无通用文件预览,失败给出提示) */ +const openFile = (file) => { + uni.showLoading({title: '正在打开...', mask: true}); + uni.downloadFile({ + url: file.url, + success: (res) => { + if (res.statusCode !== 200) { + uni.hideLoading(); + uni.showToast({title: '文件打开失败', icon: 'none'}); + return; + } + uni.openDocument({ + filePath: res.tempFilePath, + showMenu: true, + complete: () => uni.hideLoading() + }); + }, + fail: () => { + uni.hideLoading(); + uni.showToast({title: '文件打开失败', icon: 'none'}); + } + }); +}; + +/** 10 链接:小程序不能直接打开外部 URL,改为复制(最接近 PC 可点链接的能力) */ +const copyLink = (url) => { + uni.setClipboardData({ + data: url, + success: () => uni.showToast({title: '链接已复制', icon: 'none'}) + }); +}; + +/** 11 地理位置:调微信原生地图,无需地图 AK */ +const openGeo = (geo) => { + if (!geo) return; + uni.openLocation({ + latitude: Number(geo.lat), + longitude: Number(geo.lng), + name: geo.address || '作业位置', + fail: () => uni.showToast({title: '地图打开失败', icon: 'none'}) + }); +}; + // 页面加载 onLoad((options) => { const {planNo} = options; @@ -228,4 +561,44 @@ onLoad((options) => { font-size: 14px; line-height: 70px; // 和图片高度对齐 } + +// ===== 动态配置字段(填报明细) ===== +// 多值(多选/关联记录)标签平铺 +.field-tags { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8rpx; +} + +// 自绘标签:仓库未使用过 up-tag,避免引入未验证组件 +.field-tag { + padding: 2rpx 12rpx; + background: #f2f3f5; + border-radius: 6rpx; + color: #606266; + font-size: 24rpx; + line-height: 36rpx; +} + +// 附件(可点击打开) +.field-file { + color: #3c9cff; + font-size: 26rpx; + margin-left: 16rpx; + text-decoration: underline; +} + +// 链接 / 地理位置(可点击) +.field-link { + color: #3c9cff; + font-size: 26rpx; + word-break: break-all; +} + +// 文本直出(数值/文本/枚举/日期/布尔) +.field-text { + font-size: 26rpx; + word-break: break-all; +} \ No newline at end of file diff --git a/stats.html b/stats.html new file mode 100644 index 0000000..8cf865f --- /dev/null +++ b/stats.html @@ -0,0 +1,4949 @@ + + + + + + + + Rollup Visualizer + + + +
+ + + + + -- libgit2 0.21.4