feat: 调整了长文本组件的渲染

This commit is contained in:
2026-05-22 13:01:09 +08:00
parent 1a331d2ae2
commit 6e0988e85a
6 changed files with 206 additions and 84 deletions

View File

@@ -1,23 +1,20 @@
<template> <template>
<view class="parsed-value"> <view v-if="shouldRenderField" class="parsed-value">
<template v-if="isIgnoredField"></template> <template v-for="entry in renderFieldEntries" :key="entry.key">
<template v-else-if="isContentBody"> <view v-if="entry.type === 'text'" class="content-body-text">
<template v-for="entry in renderContentBodyEntries" :key="entry.key"> {{ entry.value }}
<view v-if="entry.type === 'text'" class="content-body-text"> </view>
{{ entry.value }} <view v-else-if="entry.type === 'list'" class="content-body-list-card">
</view> <view
<view v-else-if="entry.type === 'list'" class="content-body-list-card"> v-for="(item, index) in entry.value"
<view :key="index"
v-for="(item, index) in entry.value" class="content-body-list-item"
:key="index" >
class="content-body-list-item" <view class="content-body-list-text">
> {{ formatLeafValue(item) }}
<view class="content-body-list-text">
{{ formatLeafValue(item) }}
</view>
</view> </view>
</view> </view>
</template> </view>
</template> </template>
</view> </view>
</template> </template>
@@ -36,9 +33,7 @@ const props = defineProps({
}, },
}); });
const CONTENT_BODY_KEY = "content_body"; const IGNORED_FIELD_KEYS = ["container_type", "content", "components"];
const HIDDEN_CONTENT_BODY_KEYS = ["container_type"];
const IGNORED_FIELD_KEYS = ["container_type"];
const isArrayValue = (value) => Array.isArray(value); const isArrayValue = (value) => Array.isArray(value);
@@ -52,7 +47,7 @@ const sanitizeValue = (value) => {
} }
if (isObjectValue(value)) { if (isObjectValue(value)) {
return Object.keys(value).reduce((result, key) => { return Object.keys(value).reduce((result, key) => {
if (HIDDEN_CONTENT_BODY_KEYS.includes(key)) return result; if (IGNORED_FIELD_KEYS.includes(key)) return result;
result[key] = sanitizeValue(value[key]); result[key] = sanitizeValue(value[key]);
return result; return result;
}, {}); }, {});
@@ -60,6 +55,17 @@ const sanitizeValue = (value) => {
return value; return value;
}; };
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;
};
const formatLeafValue = (value) => { const formatLeafValue = (value) => {
if (value === undefined || value === null) return ""; if (value === undefined || value === null) return "";
if (typeof value === "boolean") return value ? "是" : "否"; if (typeof value === "boolean") return value ? "是" : "否";
@@ -73,35 +79,47 @@ const formatLeafValue = (value) => {
return String(value); return String(value);
}; };
const isContentBody = computed(() => props.fieldKey === CONTENT_BODY_KEY); const renderFieldEntries = computed(() => {
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey)); if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const renderContentBodyEntries = computed(() => { const value = sanitizeValue(props.value);
if (!isContentBody.value || !isObjectValue(props.value)) return []; if (isArrayValue(value)) {
return [{
key: props.fieldKey,
type: "list",
value: value.filter((item) => hasDisplayValue(item)),
}];
}
return Object.keys(props.value) if (isObjectValue(value)) {
.filter((key) => !HIDDEN_CONTENT_BODY_KEYS.includes(key)) return Object.keys(value)
.map((key) => { .filter((key) => hasDisplayValue(value[key]))
const value = props.value[key]; .map((key) => {
if (isArrayValue(value)) { const entryValue = value[key];
if (isArrayValue(entryValue)) {
return {
key,
type: "list",
value: entryValue.filter((item) => hasDisplayValue(item)),
};
}
return { return {
key, key,
type: "list", type: "text",
value: value.filter((item) => formatLeafValue(item)), value: formatLeafValue(entryValue),
}; };
} });
return { }
key,
type: "text", return [{
value: formatLeafValue(value), key: props.fieldKey,
}; type: "text",
}) value: formatLeafValue(value),
.filter((entry) => { }];
if (entry.type === "list") return entry.value.length > 0;
return !!entry.value;
});
}); });
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const shouldRenderField = computed(() => renderFieldEntries.value.length > 0);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -7,17 +7,22 @@
<!-- 滚动区域 --> <!-- 滚动区域 -->
<scroll-view class="flex-full overflow-hidden chat-scroll" scroll-y :scroll-into-view="scrollIntoViewId" scroll-with-animation @scroll="onScroll" @touchstart="onTouchStart" @touchend="onTouchEnd" @touchcancel="onTouchEnd"> <scroll-view class="flex-full overflow-hidden chat-scroll" scroll-y :scroll-into-view="scrollIntoViewId" scroll-with-animation @scroll="onScroll" @touchstart="onTouchStart" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
<view class="pt-12 px-12 pb-24 border-box"> <view class="flex flex-col pt-12 px-12 pb-24 border-box gap-10">
<template v-for="section in renderSections" :key="section.contentKey"> <view v-if="headerSections.title || headerSections.tag" class="long-answer-header">
<view v-if="section.contentKey === LONG_TEXT_KEYS.tag" class="long-answer-tag"> <view v-if="headerSections.title" class="long-answer-title">
{{ section.contentValue }} {{ headerSections.title.contentValue }}
</view> </view>
<view v-else-if="section.contentKey === LONG_TEXT_KEYS.title" class="long-answer-title"> <view v-if="headerSections.tag" class="long-answer-tag">
{{ section.contentValue }} {{ headerSections.tag.contentValue }}
</view>
<view v-else-if="shouldUseParsedValueView(section)" class="long-answer-block">
<ParsedValueView :field-key="section.contentKey" :value="section.parsedValue !== null ? section.parsedValue : section.contentValue" />
</view> </view>
</view>
<template v-for="section in contentSections" :key="section.contentKey">
<ParsedValueView
v-if="shouldUseParsedValueView(section)"
:field-key="section.contentKey"
:value="section.parsedValue !== null ? section.parsedValue : section.contentValue"
/>
<ChatMarkdown v-else-if="section.contentKey === LONG_TEXT_KEYS.content" :text="section.contentValue" /> <ChatMarkdown v-else-if="section.contentKey === LONG_TEXT_KEYS.content" :text="section.contentValue" />
@@ -56,16 +61,25 @@ const title = ref("");
const longTextData = ref(null); const longTextData = ref(null);
let unsubscribe = null; let unsubscribe = null;
const PARSED_VALUE_VIEW_KEYS = ["container_type"];
const shouldUseParsedValueView = (section) => { const shouldUseParsedValueView = (section) => {
return section.parsedValue !== null || PARSED_VALUE_VIEW_KEYS.includes(section.contentKey); return (
section.fromLongTextData &&
section.contentKey !== LONG_TEXT_KEYS.tag &&
section.contentKey !== LONG_TEXT_KEYS.title &&
section.contentKey !== LONG_TEXT_KEYS.content
);
}; };
const renderSections = computed(() => { const renderSections = computed(() => {
const data = longTextData.value; const data = longTextData.value;
if (data && data.values) { if (data && data.values) {
return getLongTextSections(data).filter((section) => section.contentValue); return getLongTextSections(data)
.filter((section) => section.contentValue)
.map((section) => ({
...section,
fromLongTextData: true,
}));
} }
return answerText.value return answerText.value
@@ -73,6 +87,22 @@ const renderSections = computed(() => {
: []; : [];
}); });
const headerSections = computed(() => {
const sections = renderSections.value;
return {
title: sections.find((section) => section.contentKey === LONG_TEXT_KEYS.title),
tag: sections.find((section) => section.contentKey === LONG_TEXT_KEYS.tag),
};
});
const contentSections = computed(() => {
return renderSections.value.filter(
(section) =>
section.contentKey !== LONG_TEXT_KEYS.title &&
section.contentKey !== LONG_TEXT_KEYS.tag
);
});
/** ✅ scroll-into-view 控制 */ /** ✅ scroll-into-view 控制 */
const scrollIntoViewId = ref(""); const scrollIntoViewId = ref("");
@@ -231,10 +261,17 @@ onUnload(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.long-answer-header {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.long-answer-tag { .long-answer-tag {
display: inline-flex; display: inline-flex;
flex-shrink: 0;
width: fit-content; width: fit-content;
margin-bottom: 8px;
padding: 3px 8px; padding: 3px 8px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba($theme-color-500, 0.2); border: 1px solid rgba($theme-color-500, 0.2);
@@ -244,12 +281,11 @@ onUnload(() => {
line-height: 18px; line-height: 18px;
} }
.long-answer-title { .long-answer-title {
flex: 1;
min-width: 0;
color: #111827; color: #111827;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 28px; line-height: 28px;
} }
.long-answer-block {
margin-bottom: 12px;
}
</style> </style>

View File

@@ -37,28 +37,4 @@
.flex-shrink-0 { .flex-shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
.gap-6 {
gap: 6px;
}
.gap-8 {
gap: 8px;
}
.gap-10 {
gap: 10px;
}
.gap-12 {
gap: 12px;
}
.gap-14 {
gap: 14px;
}
.gap-16 {
gap: 16px;
}

35
src/static/scss/gap.scss Normal file
View File

@@ -0,0 +1,35 @@
.gap-2 {
gap: 2px;
}
.gap-4 {
gap: 4px;
}
.gap-6 {
gap: 6px;
}
.gap-8 {
gap: 8px;
}
.gap-10 {
gap: 10px;
}
.gap-12 {
gap: 12px;
}
.gap-16 {
gap: 16px;
}
.gap-20 {
gap: 20px;
}
.gap-24 {
gap: 24px;
}

View File

@@ -23,3 +23,4 @@
@import "./word-break.scss"; @import "./word-break.scss";
@import "./min-height.scss"; @import "./min-height.scss";
@import "./min-width.scss"; @import "./min-width.scss";
@import "./gap.scss";

View File

@@ -1,7 +1,35 @@
export const LONG_TEXT_KEYS = { export const LONG_TEXT_KEYS = {
containerType: "container_type",
tag: "tag", tag: "tag",
title: "title", title: "title",
content: "content", content: "content",
guideConclusion: "guide_conclusion",
keyFacts: "key_facts",
sceneImage: "scene_image",
checklistOrSteps: "checklist_or_steps",
bestTimeOrPeople: "best_time_or_people",
avoidPitfalls: "avoid_pitfalls",
nextSuggestion: "next_suggestion",
poiDefinition: "poi_definition",
keyHighlights: "key_highlights",
heroImage: "hero_image",
backgroundStory: "background_story",
bestTime: "best_time",
visitSuggestion: "visit_suggestion",
routeSummary: "route_summary",
routeMeta: "route_meta",
routeSteps: "route_steps",
onsiteClues: "onsite_clues",
realtimeNotice: "realtime_notice",
routeWarning: "route_warning",
arrivalNextStep: "arrival_next_step",
photoConclusion: "photo_conclusion",
photoSpots: "photo_spots",
compositionTips: "composition_tips",
phoneSettings: "phone_settings",
lightReminder: "light_reminder",
sampleImage: "sample_image",
components: "components",
checklist: "checklist", checklist: "checklist",
suggest: "suggest", suggest: "suggest",
commodity: "commodity", commodity: "commodity",
@@ -9,9 +37,37 @@ export const LONG_TEXT_KEYS = {
}; };
export const LONG_TEXT_FIELD_CONFIG = [ export const LONG_TEXT_FIELD_CONFIG = [
{ key: LONG_TEXT_KEYS.containerType },
{ key: LONG_TEXT_KEYS.tag }, { key: LONG_TEXT_KEYS.tag },
{ key: LONG_TEXT_KEYS.title }, { key: LONG_TEXT_KEYS.title },
{ key: LONG_TEXT_KEYS.content }, { key: LONG_TEXT_KEYS.content },
{ key: LONG_TEXT_KEYS.guideConclusion },
{ key: LONG_TEXT_KEYS.keyFacts },
{ key: LONG_TEXT_KEYS.sceneImage },
{ key: LONG_TEXT_KEYS.checklistOrSteps },
{ key: LONG_TEXT_KEYS.bestTimeOrPeople },
{ key: LONG_TEXT_KEYS.avoidPitfalls },
{ key: LONG_TEXT_KEYS.nextSuggestion },
{ key: LONG_TEXT_KEYS.poiDefinition },
{ key: LONG_TEXT_KEYS.keyHighlights },
{ key: LONG_TEXT_KEYS.heroImage },
{ key: LONG_TEXT_KEYS.backgroundStory },
{ key: LONG_TEXT_KEYS.bestTime },
{ key: LONG_TEXT_KEYS.visitSuggestion },
{ key: LONG_TEXT_KEYS.routeSummary },
{ key: LONG_TEXT_KEYS.routeMeta },
{ key: LONG_TEXT_KEYS.routeSteps },
{ key: LONG_TEXT_KEYS.onsiteClues },
{ key: LONG_TEXT_KEYS.realtimeNotice },
{ key: LONG_TEXT_KEYS.routeWarning },
{ key: LONG_TEXT_KEYS.arrivalNextStep },
{ key: LONG_TEXT_KEYS.photoConclusion },
{ key: LONG_TEXT_KEYS.photoSpots },
{ key: LONG_TEXT_KEYS.compositionTips },
{ key: LONG_TEXT_KEYS.phoneSettings },
{ key: LONG_TEXT_KEYS.lightReminder },
{ key: LONG_TEXT_KEYS.sampleImage },
{ key: LONG_TEXT_KEYS.components },
{ key: LONG_TEXT_KEYS.checklist }, { key: LONG_TEXT_KEYS.checklist },
{ key: LONG_TEXT_KEYS.suggest }, { key: LONG_TEXT_KEYS.suggest },
{ key: LONG_TEXT_KEYS.commodity }, { key: LONG_TEXT_KEYS.commodity },