fix(订单详情): 自定义表单图片字段按类型回显并支持点击预览

This commit is contained in:
From-wh 2026-10-23 16:09:21 +08:00 committed by 吴昊天
parent ec156de42c
commit f19125f7ae
4 changed files with 123 additions and 19 deletions

View File

@ -1365,15 +1365,19 @@ class CenterOrderController
$detailExtend = $handler->getDetailExtend($productId);
$productCustomForm = $detailExtend['custom_form'] ?? [];
$fieldTitleMap = [];
$fieldTypeMap = [];
// 循环变量不得使用 $key:外层 $key 是订单确认缓存凭证,覆盖后 consumeOrderCache 会因类型错误崩溃
foreach ($productCustomForm as $fieldKey => $fieldItem) {
$fieldTitleMap['custom_field_' . $fieldKey] = $fieldItem['title'] ?? ('自定义字段' . ($fieldKey + 1));
// 记录字段类型(管理端类型值 text/number/data/time/img 等),订单回显按类型渲染(图片字段展示缩略图)
$fieldTypeMap['custom_field_' . $fieldKey] = $fieldItem['label'] ?? '';
}
$structured = [];
foreach ($submittedCustomForm as $field => $value) {
$structured[] = [
'label' => $field,
'title' => $fieldTitleMap[$field] ?? $field,
'type' => $fieldTypeMap[$field] ?? '',
'value' => $value,
];
}

View File

@ -211,13 +211,13 @@
<div class="section" v-if="data.custom_form && data.custom_form.length">
<div class="title">表单信息</div>
<ul class="list">
<li class="item" :class="{ pic: item.label == 'img' }" v-for="(item, index) in data.custom_form" :key="index">
<template v-if="item.label !== 'img'">
<li class="item" :class="{ pic: isImageItem(item) }" v-for="(item, index) in data.custom_form" :key="index">
<template v-if="!isImageItem(item)">
<div>{{ item.title }}:{{ item.value }}</div>
</template>
<template v-else>
<div>{{ item.title }}:</div>
<div v-for="(img, i) in item.value" :key="i" class="img">
<div v-for="(img, i) in formImageUrls(item)" :key="i" class="img">
<img v-viewer :src="img" alt="" />
</div>
</template>
@ -288,6 +288,28 @@ function buildAddress(addr) {
if (addr.postal_code) parts.push(`(${addr.postal_code})`);
return parts.join('');
}
// 图片扩展名判断
const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|bmp)(\?.*)?$/i;
/** 自定义表单字段值规范化为图片 URL 数组(兼容字符串单图 / 数组多图) */
function formImageUrls(item) {
if (!item || !item.value) return [];
const list = Array.isArray(item.value) ? item.value : [item.value];
return list.filter((v) => typeof v === 'string' && v.indexOf('http') === 0);
}
/**
* 图片字段判断:新订单 type=img,或存量订单无类型但值是图片链接
* (新数据 label 是 custom_field_N,不能再按 label == 'img' 判断)
*/
function isImageItem(item) {
if (!item) return false;
if (item.type === 'img') return true;
if (item.type) return false;
const urls = formImageUrls(item);
return urls.length > 0 && urls.every((v) => IMAGE_EXT_RE.test(v) || v.indexOf('/uploads/') !== -1);
}
</script>
<style lang="scss" scoped>

View File

@ -6,14 +6,27 @@
<template>
<view class="info-card custom-form-card" v-if="items && items.length > 0">
<view class="card-title">{{ $t('自定义信息') }}</view>
<view
class="info-row acea-row row-between"
v-for="(item, idx) in items"
:key="idx"
>
<text class="info-label">{{ item.title || item.label }}:</text>
<text class="info-value">{{ item.value }}</text>
</view>
<template v-for="(item, idx) in items" :key="idx">
<!-- 图片字段:类型为 img,或存量订单无类型但值是图片链接 -->
<view class="info-row info-row--block" v-if="isImageItem(item)">
<view class="info-label">{{ item.title || item.label }}:</view>
<view class="info-images">
<image
v-for="(url, i) in imageList(item)"
:key="i"
class="info-image"
:src="url"
mode="aspectFill"
@click="previewImages(item, i)"
/>
</view>
</view>
<!-- 文本字段:值跟随标签左对齐,长文本自动换行 -->
<view class="info-row" v-else>
<text class="info-label">{{ item.title || item.label }}:</text>
<text class="info-value">{{ item.value }}</text>
</view>
</template>
</view>
</template>
@ -21,7 +34,7 @@
/**
* 自定义表单回显组件
* 展示用户下单时填写的自定义字段
* 字段结构:{title/label, value}
* 字段结构:{title/label, type, value};type=img 为图片字段(存量订单无 type,按图片链接特征兜底)
* 通过 componentKey='custom_form' 匹配 section.component
*/
export default {
@ -45,12 +58,50 @@ const p = computed(() => (props.section && props.section.props) || {});
const ctx = computed(() => (props.section && props.section._ctx) || {});
/** 表单字段数组(后端下发在 section.props.items) */
const items = computed(() => p.value.items || []);
/** 图片扩展名判断 */
var IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|bmp)(\?.*)?$/i;
/** 规范化字段值为图片 URL 数组(字符串 / 数组均可) */
function toUrlList(value) {
if (!value) return [];
var list = Array.isArray(value) ? value : [value];
return list.filter(function (v) {
return typeof v === 'string' && v.indexOf('http') === 0;
});
}
/** 是否图片字段:显式 type=img,或存量订单无类型但值为图片链接 */
function isImageItem(item) {
if (item.type === 'img') return true;
if (item.type) return false;
var urls = toUrlList(item.value);
return urls.length > 0 && urls.every(function (v) {
return IMAGE_EXT_RE.test(v) || v.indexOf('/uploads/') !== -1;
});
}
/** 图片字段的可展示 URL 列表 */
function imageList(item) {
return toUrlList(item.value);
}
/** 点击预览大图 */
function previewImages(item, index) {
var urls = imageList(item);
if (!urls.length) return;
uni.previewImage({ urls: urls, current: index });
}
</script>
<style scoped>
.custom-form-card { margin: 20rpx; background: #fff; border-radius: 16rpx; padding: 30rpx; }
.card-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; font-size: 26rpx; }
.info-row { display: flex; align-items: baseline; padding: 12rpx 0; font-size: 26rpx; }
.info-label { color: #999; flex-shrink: 0; }
.info-value { color: #333; text-align: right; }
.info-value { color: #333; flex: 1; margin-left: 16rpx; line-height: 1.5; word-break: break-all; text-align: left; }
/* 图片字段:标签在上、图片缩略图在下 */
.info-row--block { display: block; padding: 12rpx 0; }
.info-images { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
.info-image { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin: 0 12rpx 12rpx 0; background: #f5f5f5; }
</style>

View File

@ -338,14 +338,17 @@
</view>
<view class="wrapper" v-if="customForm && customForm.length">
<view class="item acea-row row-between" v-for="(item, index) in customForm" :key="index">
<view class="upload" v-if="item.label == 'img'">
<!-- 图片字段:新格式 type=img,或存量订单无类型但值是图片链接 -->
<view class="upload" v-if="isImageFormItem(item)">
<view class="diy-from-title">{{ item.title }}:</view>
<view class="pictrue" v-for="(img, index) in item.value" :key="index">
<image :src="img"></image>
<view class="pictrue" v-for="(img, imgIndex) in formImageUrls(item)" :key="imgIndex">
<image :src="img" @click="previewFormImages(item, imgIndex)"></image>
</view>
</view>
<view v-if="item.label !== 'img'" class="diy-from-title">{{ item.title }}:</view>
<view v-if="item.label !== 'img'" class="conter">{{ item.value }}</view>
<template v-else>
<view class="diy-from-title">{{ item.title }}:</view>
<view class="conter">{{ item.value }}</view>
</template>
</view>
<view class="copy-text" @click="copyText()">{{ $t(`复制`) }}</view>
</view>
@ -607,6 +610,30 @@ const { isLogin } = storeToRefs(appStore);
// ---------- data ----------
const imgHost = HTTP_REQUEST_URL;
const customForm = ref(''); //自定义留言
/**
* 自定义表单图片字段判断:新格式 type=img,或存量订单无类型但值是图片链接
* (旧数据 label 存类型值,新数据 label 是 custom_field_N,不能再用 label 判断)
*/
const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|bmp)(\?.*)?$/i;
function formImageUrls(item) {
if (!item || !item.value) return [];
const list = Array.isArray(item.value) ? item.value : [item.value];
return list.filter((v) => typeof v === 'string' && v.indexOf('http') === 0);
}
function isImageFormItem(item) {
if (!item) return false;
if (item.type === 'img') return true;
if (item.type) return false;
const urls = formImageUrls(item);
return urls.length > 0 && urls.every((v) => IMAGE_EXT_RE.test(v) || v.indexOf('/uploads/') !== -1);
}
function previewFormImages(item, index) {
const urls = formImageUrls(item);
if (!urls.length) return;
uni.previewImage({ urls, current: index });
}
//二维码参数
const codeShow = ref(false);
const cid = ref('1');