feat: 调整了长文本组件的渲染
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
35
src/static/scss/gap.scss
Normal 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;
|
||||
}
|
||||
@@ -23,3 +23,4 @@
|
||||
@import "./word-break.scss";
|
||||
@import "./min-height.scss";
|
||||
@import "./min-width.scss";
|
||||
@import "./gap.scss";
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user