feat: 改造了组件的渲染逻辑

This commit is contained in:
2026-06-04 10:57:28 +08:00
parent 19b97fc326
commit e5ec442500
5 changed files with 267 additions and 219 deletions

View File

@@ -1,52 +1,53 @@
<template>
<view v-if="shouldRenderField" class="parsed-value">
<template v-for="entry in renderFieldEntries" :key="entry.key">
<view v-if="entry.type === 'text'" class="content-body-text">
{{ entry.value }}
</view>
<view v-else-if="entry.type === 'image'" class="content-body-image-card">
<image class="content-body-image" :src="entry.value.image_id" mode="widthFix"
@click="handlePreviewClick(entry.value.image_id)" />
<view v-if="entry.value.caption" class="content-body-image-caption">
{{ entry.value.caption }}
</view>
</view>
<image v-if="shouldRenderContentImage" class="content-body-image" :src="contentImageUrl"
mode="widthFix" @click="handlePreviewClick(contentImageUrl)" />
<view v-else-if="entry.type === 'list'" class="content-body-list-card" :style="contentBodyListCardStyle">
<view v-for="(item, index) in entry.value" :key="index" class="content-body-list-item">
<view class="content-body-list-text" :style="contentBodyListTextStyle">
{{ formatLeafValue(item) }}
<template v-else-if="shouldRenderSpotLocate">
<view class="detail-action-zone">
<view class="detail-action-label">查看景点详情</view>
<view class="detail-action-card is-raised">
<view v-if="spotLocateValue.tag" class="poi-mini-tag">{{ spotLocateValue.tag }}</view>
<view class="poi-mini-body">
<view v-if="spotLocateValue.name" class="poi-mini-title">{{ spotLocateValue.name }}</view>
<view v-if="spotLocateValue.description" class="poi-mini-desc">{{ spotLocateValue.description }}</view>
<button v-if="hasMapLocation(spotLocateValue)" class="detail-solid-button" @click.stop="openMap(spotLocateValue)">带我去</button>
</view>
</view>
</view>
</template>
<image v-else-if="entry.type === 'content-image'" class="content-body-image" :src="entry.value.url"
mode="widthFix" @click="handlePreviewClick(entry.value.url)" />
<template v-else-if="shouldRenderQuestionSuggest">
<view class="detail-action-label">继续追问</view>
<view class="detail-faq-wrap">
<view v-for="question in questionSuggestItems" :key="question" class="detail-faq-chip" @click="sendReply(question)">
{{ question }}
</view>
</view>
</template>
<template v-else-if="entry.type === 'spot-locate'">
<view class="detail-action-zone">
<view class="detail-action-label">查看景点详情</view>
<view class="detail-action-card is-raised">
<view v-if="entry.value.tag" class="poi-mini-tag">{{ entry.value.tag }}</view>
<view class="poi-mini-body">
<view v-if="entry.value.name" class="poi-mini-title">{{ entry.value.name }}</view>
<view v-if="entry.value.description" class="poi-mini-desc">{{ entry.value.description }}</view>
<button v-if="hasMapLocation(entry.value)" class="detail-solid-button" @click.stop="openMap(entry.value)">带我去</button>
<template v-else>
<template v-for="entry in renderFieldEntries" :key="entry.key">
<view v-if="entry.type === 'text'" class="content-body-text">
{{ entry.value }}
</view>
<view v-else-if="entry.type === 'image'" class="content-body-image-card">
<image class="content-body-image" :src="entry.value.image_id" mode="widthFix"
@click="handlePreviewClick(entry.value.image_id)" />
<view v-if="entry.value.caption" class="content-body-image-caption">
{{ entry.value.caption }}
</view>
</view>
<view v-else-if="entry.type === 'list'" class="content-body-list-card" :style="contentBodyListCardStyle">
<view v-for="(item, index) in entry.value" :key="index" class="content-body-list-item">
<view class="content-body-list-text" :style="contentBodyListTextStyle">
{{ formatLeafValue(item) }}
</view>
</view>
</view>
</template>
<template v-else-if="entry.type === 'question-suggest'">
<view class="detail-action-label">继续追问</view>
<view class="detail-faq-wrap">
<view v-for="question in entry.value" :key="question" class="detail-faq-chip" @click="sendReply(question)">
{{ question }}
</view>
</view>
</template>
</template>
</view>
</template>
@@ -57,9 +58,9 @@ import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
import { getRandomTagToneStyle } from "@/utils/tagTone";
import {
LONG_TEXT_KEYS,
normalizeLongTextContentImage,
normalizeLongTextQuestionSuggest,
normalizeLongTextSpotLocate,
formatLongTextDisplayValue,
hasLongTextDisplayValue,
sanitizeLongTextDisplayValue,
} from "@/utils/longTextCard";
const props = defineProps({
@@ -84,63 +85,34 @@ const contentBodyListTextStyle = computed(() => ({
const IGNORED_FIELD_KEYS = ["container_type", "content", "components"];
/// ======== Value Processing Helpers ========
const isArrayValue = (value) => Array.isArray(value);
const isObjectValue = (value) => {
return value !== null && typeof value === "object" && !Array.isArray(value);
};
const parseJsonStringValue = (value) => {
if (typeof value !== "string") return value;
const text = value.trim();
if (!/^[\[{]/.test(text)) return value;
try {
return JSON.parse(text);
} catch (e) {
return value;
}
};
const sanitizeValue = (value) => {
const parsedValue = parseJsonStringValue(value);
if (isArrayValue(parsedValue)) {
return parsedValue.map((item) => sanitizeValue(item));
}
if (isObjectValue(parsedValue)) {
return Object.keys(parsedValue).reduce((result, key) => {
if (IGNORED_FIELD_KEYS.includes(key)) return result;
result[key] = sanitizeValue(parsedValue[key]);
return result;
}, {});
}
return parsedValue;
return sanitizeLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const hasDisplayValue = (value) => {
if (value === undefined || value === null) return false;
if (typeof value === "string") return !!value.trim();
if (isArrayValue(value)) return value.some((item) => hasDisplayValue(item));
if (isObjectValue(value)) {
const valueObj = sanitizeValue(value);
return Object.keys(valueObj).some((key) => hasDisplayValue(valueObj[key]));
}
return true;
return hasLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const formatLeafValue = (value) => {
return formatLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const toTrimmedText = (value) => {
if (value === undefined || value === null) return "";
if (typeof value === "boolean") return value ? "是" : "否";
if (typeof value === "object") {
try {
return JSON.stringify(sanitizeValue(value));
} catch (e) {
return String(value);
}
}
return String(value);
return String(value).trim();
};
const toFiniteNumber = (value) => {
if (value === undefined || value === null || value === "") return null;
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : null;
};
const isImageValue = (value) => {
@@ -156,50 +128,55 @@ const createImageEntry = (key, value) => ({
},
});
const createSpecialFieldEntry = (key, value) => {
if (key === LONG_TEXT_KEYS.contentImage) {
const image = normalizeLongTextContentImage(value);
return image.url
? [{
key,
type: "content-image",
value: image,
}]
: [];
}
if (key === LONG_TEXT_KEYS.spotLocate) {
const spot = normalizeLongTextSpotLocate(value);
return hasDisplayValue(spot)
? [{
key,
type: "spot-locate",
value: spot,
}]
: [];
}
/// ======== Render Logic ========
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const isContentImageField = computed(() => props.fieldKey === LONG_TEXT_KEYS.contentImage);
const isSpotLocateField = computed(() => props.fieldKey === LONG_TEXT_KEYS.spotLocate);
const isQuestionSuggestField = computed(() => props.fieldKey === LONG_TEXT_KEYS.questionSuggest);
if (key === LONG_TEXT_KEYS.questionSuggest) {
const questions = normalizeLongTextQuestionSuggest(value);
return questions.length
? [{
key,
type: "question-suggest",
value: questions,
}]
: [];
}
const displayValue = computed(() => sanitizeValue(props.value));
return null;
};
/// 特殊字段的直接渲染数据
const contentImageUrl = computed(() => {
return typeof displayValue.value === "string" ? toTrimmedText(displayValue.value) : "";
});
const spotLocateValue = computed(() => {
const value = isObjectValue(displayValue.value) ? displayValue.value : {};
return {
name: toTrimmedText(value.spot_name),
description: toTrimmedText(value.spot_description),
longitude: toFiniteNumber(value.spot_longitude),
latitude: toFiniteNumber(value.spot_latitude),
tag: toTrimmedText(value.spot_tag),
};
});
const questionSuggestItems = computed(() => {
return isArrayValue(displayValue.value)
? displayValue.value.map((item) => toTrimmedText(item)).filter(Boolean)
: [];
});
const shouldRenderContentImage = computed(() => {
return isContentImageField.value && !!contentImageUrl.value;
});
const shouldRenderSpotLocate = computed(() => {
return isSpotLocateField.value && hasDisplayValue(spotLocateValue.value);
});
const shouldRenderQuestionSuggest = computed(() => {
return isQuestionSuggestField.value && questionSuggestItems.value.length > 0;
});
/// 其他字段走通用渲染逻辑
const renderFieldEntries = computed(() => {
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const specialFieldEntry = createSpecialFieldEntry(props.fieldKey, props.value);
if (specialFieldEntry) return specialFieldEntry;
const value = sanitizeValue(props.value);
const value = displayValue.value;
if (isImageValue(value)) {
return [createImageEntry(props.fieldKey, value)];
}
@@ -242,13 +219,27 @@ const renderFieldEntries = computed(() => {
}];
});
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const shouldRenderField = computed(() => renderFieldEntries.value.length > 0);
/// 是否有任何内容可以渲染(特殊字段优先)
const shouldRenderField = computed(() => {
if (isIgnoredField.value) return false;
if (
shouldRenderContentImage.value ||
shouldRenderSpotLocate.value ||
shouldRenderQuestionSuggest.value
) {
return true;
}
return renderFieldEntries.value.length > 0;
});
const hasMapLocation = (spot) => {
return Number.isFinite(spot?.latitude) && Number.isFinite(spot?.longitude);
};
/// ======== Action Handlers ========
const openMap = (spot) => {
if (!hasMapLocation(spot)) {
uni.showToast({