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"
>
+
+
+
+
+
+
+
+
+
+
+ {{ t.text }}
+
+
+
+
+ {{ file.name }}
+
+
+
+ {{ f.text }}
+
+
+ {{ f.text }}
+
+
+ {{ f.text }}
+
+
+
@@ -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
+
+
+
+
+
+
+
+
+