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