Files
YGChatCS/src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue
2026-06-04 14:28:47 +08:00

366 lines
12 KiB
Vue

<template>
<view v-if="shouldRenderField" class="parsed-value">
<image v-if="shouldRenderContentImage" class="content-body-image content-body-image-bottom" :src="contentImageUrl"
mode="widthFix" @click="handlePreviewClick(contentImageUrl)" />
<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>
<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="shouldRenderCommodityList">
<view class="detail-action-zone">
<view class="detail-action-label">相关票务</view>
<scroll-view class="detail-product-scroll" scroll-x>
<view class="detail-product-row">
<view
v-for="commodity in commodityItems"
:key="commodity.commodity_id || commodity.commodity_name"
class="detail-product-card"
>
<image
v-if="commodity.commodity_photo"
class="detail-product-image"
:src="commodity.commodity_photo"
mode="aspectFill"
@click="handlePreviewClick(commodity.commodity_photo)"
/>
<view class="detail-product-body">
<view v-if="commodity.commodity_tag" class="detail-product-tag">
{{ commodity.commodity_tag }}
</view>
<view v-if="commodity.commodity_name" class="detail-product-name">
{{ commodity.commodity_name }}
</view>
<view v-if="commodity.commodity_price" class="detail-product-price">
<text class="detail-product-currency">¥</text>{{ commodity.commodity_price }}
</view>
<button
v-if="commodity.commodity_id"
class="detail-buy-button"
@click.stop="openCommodityDetail(commodity)"
>
立即购买
</button>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<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 v-if="entry.value.length > 1" class="content-body-list-marker" :style="contentBodyListTextStyle">
{{ formatListMarker(index) }}
</view>
<view class="content-body-list-text" :style="contentBodyListTextStyle">
{{ formatLeafValue(item) }}
</view>
</view>
</view>
</template>
</template>
</view>
</template>
<script setup>
import { computed, defineProps, ref } from "vue";
import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
import { getRandomTagToneStyle } from "@/utils/tagTone";
import {
LONG_TEXT_KEYS,
formatLongTextDisplayValue,
hasLongTextDisplayValue,
sanitizeLongTextDisplayValue,
} from "@/utils/longTextCard";
const props = defineProps({
fieldKey: {
type: String,
default: "",
},
value: {
type: [Object, Array, String, Number, Boolean],
default: null,
},
});
const contentBodyListToneStyle = ref(getRandomTagToneStyle({ borderAlpha: 1, borderWidth: 4 }));
const contentBodyListCardStyle = computed(() => ({
borderLeft: contentBodyListToneStyle.value.border,
background: contentBodyListToneStyle.value.background,
}));
const contentBodyListTextStyle = computed(() => ({
color: contentBodyListToneStyle.value.color,
}));
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 sanitizeValue = (value) => {
return sanitizeLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const hasDisplayValue = (value) => {
return hasLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const formatLeafValue = (value) => {
return formatLongTextDisplayValue(value, IGNORED_FIELD_KEYS);
};
const LIST_MARKERS = [
"①", "②", "③", "④", "⑤",
"⑥", "⑦", "⑧", "⑨", "⑩",
"⑪", "⑫", "⑬", "⑭", "⑮",
"⑯", "⑰", "⑱", "⑲", "⑳",
];
const formatListMarker = (index) => {
return LIST_MARKERS[index] || `${index + 1}.`;
};
const toTrimmedText = (value) => {
if (value === undefined || value === null) return "";
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) => {
return isObjectValue(value) && hasDisplayValue(value.image_id);
};
const createImageEntry = (key, value) => ({
key,
type: "image",
value: {
image_id: formatLeafValue(value.image_id).trim(),
caption: hasDisplayValue(value.caption) ? formatLeafValue(value.caption) : "",
},
});
/// ======== Render Logic ========
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const isContentImageField = computed(() =>
props.fieldKey === LONG_TEXT_KEYS.contentImage ||
props.fieldKey === LONG_TEXT_KEYS.sceneImage
);
const isSpotLocateField = computed(() => props.fieldKey === LONG_TEXT_KEYS.spotLocate);
const isQuestionSuggestField = computed(() => props.fieldKey === LONG_TEXT_KEYS.questionSuggest);
const isCommodityListField = computed(() => props.fieldKey === LONG_TEXT_KEYS.commodityList);
const displayValue = computed(() => sanitizeValue(props.value));
/// 特殊字段的直接渲染数据
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 commodityItems = computed(() => {
if (!isArrayValue(displayValue.value)) return [];
return displayValue.value
.filter((item) => isObjectValue(item))
.map((commodity) => ({
commodity_id: toTrimmedText(commodity.commodity_id),
commodity_name: toTrimmedText(commodity.commodity_name),
commodity_price: toTrimmedText(commodity.commodity_price),
commodity_tag: toTrimmedText(commodity.commodity_tag),
commodity_photo: toTrimmedText(commodity.commodity_photo),
}))
.filter((commodity) => hasDisplayValue(commodity));
});
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 shouldRenderCommodityList = computed(() => {
return isCommodityListField.value && commodityItems.value.length > 0;
});
/// 其他字段走通用渲染逻辑
const renderFieldEntries = computed(() => {
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const value = displayValue.value;
if (isImageValue(value)) {
return [createImageEntry(props.fieldKey, value)];
}
if (isArrayValue(value)) {
return [{
key: props.fieldKey,
type: "list",
value: value.filter((item) => hasDisplayValue(item)),
}];
}
if (isObjectValue(value)) {
return Object.keys(value)
.filter((key) => hasDisplayValue(value[key]))
.map((key) => {
const entryValue = value[key];
if (isImageValue(entryValue)) {
return createImageEntry(key, entryValue);
}
if (isArrayValue(entryValue)) {
return {
key,
type: "list",
value: entryValue.filter((item) => hasDisplayValue(item)),
};
}
return {
key,
type: "text",
value: formatLeafValue(entryValue),
};
});
}
return [{
key: props.fieldKey,
type: "text",
value: formatLeafValue(value),
}];
});
/// 是否有任何内容可以渲染(特殊字段优先)
const shouldRenderField = computed(() => {
if (isIgnoredField.value) return false;
if (
shouldRenderContentImage.value ||
shouldRenderSpotLocate.value ||
shouldRenderQuestionSuggest.value ||
shouldRenderCommodityList.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({
title: "暂无位置信息",
icon: "none",
});
return;
}
const latitude = spot.latitude;
const longitude = spot.longitude;
const name = spot.name || "";
const address = spot.description || name;
uni.getLocation({ type: 'gcj02', success: () => {
uni.openLocation({ latitude, longitude, name, address });
}, fail: () => {
uni.openLocation({ latitude, longitude, name, address });
}});
};
const sendReply = (item) => {
uni.navigateBack();
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item);
};
const openCommodityDetail = (commodity) => {
if (!commodity.commodity_id) return;
uni.navigateTo({
url: `/pages/goods/index?commodityId=${commodity.commodity_id}`,
});
};
const handlePreviewClick = (imageUrl) => {
uni.previewImage({
current: imageUrl,
urls: [imageUrl],
});
};
</script>
<style scoped lang="scss">
@import "./styles/ParsedValueView.scss";
</style>