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>
<view class="parsed-value">
<template v-if="isIgnoredField"></template>
<template v-else-if="isContentBody">
<template v-for="entry in renderContentBodyEntries" :key="entry.key">
<view v-if="entry.type === 'text'" class="content-body-text">
{{ entry.value }}
</view>
<view v-else-if="entry.type === 'list'" class="content-body-list-card">
<view
v-for="(item, index) in entry.value"
:key="index"
class="content-body-list-item"
>
<view class="content-body-list-text">
{{ formatLeafValue(item) }}
</view>
<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 === 'list'" class="content-body-list-card">
<view
v-for="(item, index) in entry.value"
:key="index"
class="content-body-list-item"
>
<view class="content-body-list-text">
{{ formatLeafValue(item) }}
</view>
</view>
</template>
</view>
</template>
</view>
</template>
@@ -36,9 +33,7 @@ const props = defineProps({
},
});
const CONTENT_BODY_KEY = "content_body";
const HIDDEN_CONTENT_BODY_KEYS = ["container_type"];
const IGNORED_FIELD_KEYS = ["container_type"];
const IGNORED_FIELD_KEYS = ["container_type", "content", "components"];
const isArrayValue = (value) => Array.isArray(value);
@@ -52,7 +47,7 @@ const sanitizeValue = (value) => {
}
if (isObjectValue(value)) {
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]);
return result;
}, {});
@@ -60,6 +55,17 @@ const sanitizeValue = (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) => {
if (value === undefined || value === null) return "";
if (typeof value === "boolean") return value ? "是" : "否";
@@ -73,35 +79,47 @@ const formatLeafValue = (value) => {
return String(value);
};
const isContentBody = computed(() => props.fieldKey === CONTENT_BODY_KEY);
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const renderFieldEntries = computed(() => {
if (isIgnoredField.value || !hasDisplayValue(props.value)) return [];
const renderContentBodyEntries = computed(() => {
if (!isContentBody.value || !isObjectValue(props.value)) return [];
const value = sanitizeValue(props.value);
if (isArrayValue(value)) {
return [{
key: props.fieldKey,
type: "list",
value: value.filter((item) => hasDisplayValue(item)),
}];
}
return Object.keys(props.value)
.filter((key) => !HIDDEN_CONTENT_BODY_KEYS.includes(key))
.map((key) => {
const value = props.value[key];
if (isArrayValue(value)) {
if (isObjectValue(value)) {
return Object.keys(value)
.filter((key) => hasDisplayValue(value[key]))
.map((key) => {
const entryValue = value[key];
if (isArrayValue(entryValue)) {
return {
key,
type: "list",
value: entryValue.filter((item) => hasDisplayValue(item)),
};
}
return {
key,
type: "list",
value: value.filter((item) => formatLeafValue(item)),
type: "text",
value: formatLeafValue(entryValue),
};
}
return {
key,
type: "text",
value: formatLeafValue(value),
};
})
.filter((entry) => {
if (entry.type === "list") return entry.value.length > 0;
return !!entry.value;
});
});
}
return [{
key: props.fieldKey,
type: "text",
value: formatLeafValue(value),
}];
});
const isIgnoredField = computed(() => IGNORED_FIELD_KEYS.includes(props.fieldKey));
const shouldRenderField = computed(() => renderFieldEntries.value.length > 0);
</script>
<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">
<view class="pt-12 px-12 pb-24 border-box">
<template v-for="section in renderSections" :key="section.contentKey">
<view v-if="section.contentKey === LONG_TEXT_KEYS.tag" class="long-answer-tag">
{{ section.contentValue }}
<view class="flex flex-col pt-12 px-12 pb-24 border-box gap-10">
<view v-if="headerSections.title || headerSections.tag" class="long-answer-header">
<view v-if="headerSections.title" class="long-answer-title">
{{ headerSections.title.contentValue }}
</view>
<view v-else-if="section.contentKey === LONG_TEXT_KEYS.title" class="long-answer-title">
{{ section.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 v-if="headerSections.tag" class="long-answer-tag">
{{ headerSections.tag.contentValue }}
</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" />
@@ -56,16 +61,25 @@ const title = ref("");
const longTextData = ref(null);
let unsubscribe = null;
const PARSED_VALUE_VIEW_KEYS = ["container_type"];
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 data = longTextData.value;
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
@@ -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 控制 */
const scrollIntoViewId = ref("");
@@ -231,10 +261,17 @@ onUnload(() => {
</script>
<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 {
display: inline-flex;
flex-shrink: 0;
width: fit-content;
margin-bottom: 8px;
padding: 3px 8px;
border-radius: 12px;
border: 1px solid rgba($theme-color-500, 0.2);
@@ -244,12 +281,11 @@ onUnload(() => {
line-height: 18px;
}
.long-answer-title {
flex: 1;
min-width: 0;
color: #111827;
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.long-answer-block {
margin-bottom: 12px;
}
</style>

View File

@@ -37,28 +37,4 @@
.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 "./min-height.scss";
@import "./min-width.scss";
@import "./gap.scss";

View File

@@ -1,7 +1,35 @@
export const LONG_TEXT_KEYS = {
containerType: "container_type",
tag: "tag",
title: "title",
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",
suggest: "suggest",
commodity: "commodity",
@@ -9,9 +37,37 @@ export const LONG_TEXT_KEYS = {
};
export const LONG_TEXT_FIELD_CONFIG = [
{ key: LONG_TEXT_KEYS.containerType },
{ key: LONG_TEXT_KEYS.tag },
{ key: LONG_TEXT_KEYS.title },
{ 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.suggest },
{ key: LONG_TEXT_KEYS.commodity },